File: Hosting\ObjectFormatter\CommonObjectFormatter.Visitor.cs
Web Access
Project: src\src\Scripting\Core\Microsoft.CodeAnalysis.Scripting.csproj (Microsoft.CodeAnalysis.Scripting)
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Cci;
using Roslyn.Utilities;
using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer;
 
namespace Microsoft.CodeAnalysis.Scripting.Hosting
{
    using static ObjectFormatterHelpers;
    using TypeInfo = System.Reflection.TypeInfo;
 
    internal abstract partial class CommonObjectFormatter
    {
        private sealed partial class Visitor
        {
            private readonly CommonObjectFormatter _formatter;
 
            private readonly BuilderOptions _builderOptions;
            private CommonPrimitiveFormatterOptions _primitiveOptions;
            private readonly CommonTypeNameFormatterOptions _typeNameOptions;
            private MemberDisplayFormat _memberDisplayFormat;
 
            private HashSet<object> _lazyVisitedObjects;
 
            private HashSet<object> VisitedObjects
            {
                get
                {
                    _lazyVisitedObjects ??= new HashSet<object>(ReferenceEqualityComparer.Instance);
 
                    return _lazyVisitedObjects;
                }
            }
 
            public Visitor(
                CommonObjectFormatter formatter,
                BuilderOptions builderOptions,
                CommonPrimitiveFormatterOptions primitiveOptions,
                CommonTypeNameFormatterOptions typeNameOptions,
                MemberDisplayFormat memberDisplayFormat)
            {
                _formatter = formatter;
                _builderOptions = builderOptions;
                _primitiveOptions = primitiveOptions;
                _typeNameOptions = typeNameOptions;
                _memberDisplayFormat = memberDisplayFormat;
            }
 
            private Builder MakeMemberBuilder(int limit)
            {
                return new Builder(_builderOptions.WithMaximumOutputLength(Math.Min(_builderOptions.MaximumLineLength, limit)), suppressEllipsis: true);
            }
 
            public string FormatObject(object obj)
            {
                try
                {
                    var builder = new Builder(_builderOptions, suppressEllipsis: false);
                    string _;
                    return FormatObjectRecursive(builder, obj, isRoot: true, debuggerDisplayName: out _).ToString();
                }
                catch (InsufficientExecutionStackException)
                {
                    return ScriptingResources.StackOverflowWhileEvaluating;
                }
            }
 
            private Builder FormatObjectRecursive(Builder result, object obj, bool isRoot, out string debuggerDisplayName)
            {
                // TODO (https://github.com/dotnet/roslyn/issues/6689): remove this
                if (!isRoot && _memberDisplayFormat == MemberDisplayFormat.SeparateLines)
                {
                    _memberDisplayFormat = MemberDisplayFormat.SingleLine;
                }
 
                debuggerDisplayName = null;
                string primitive = _formatter.PrimitiveFormatter.FormatPrimitive(obj, _primitiveOptions);
                if (primitive != null)
                {
                    result.Append(primitive);
                    return result;
                }
 
                Type type = obj.GetType();
                TypeInfo typeInfo = type.GetTypeInfo();
 
                //
                // Override KeyValuePair<,>.ToString() to get better dictionary elements formatting:
                //
                // { { format(key), format(value) }, ... }
                // instead of
                // { [key.ToString(), value.ToString()], ... } 
                //
                // This is more general than overriding Dictionary<,> debugger proxy attribute since it applies on all
                // types that return an array of KeyValuePair in their DebuggerDisplay to display items.
                //
                if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
                {
                    if (isRoot)
                    {
                        result.Append(_formatter.TypeNameFormatter.FormatTypeName(type, _typeNameOptions));
                        result.Append(' ');
                    }
 
                    FormatKeyValuePair(result, obj);
                    return result;
                }
 
                if (typeInfo.IsArray)
                {
                    if (VisitedObjects.Add(obj))
                    {
                        FormatArray(result, (Array)obj);
 
                        VisitedObjects.Remove(obj);
                    }
                    else
                    {
                        result.AppendInfiniteRecursionMarker();
                    }
 
                    return result;
                }
 
                DebuggerDisplayAttribute debuggerDisplay = GetApplicableDebuggerDisplayAttribute(typeInfo);
                if (debuggerDisplay != null)
                {
                    debuggerDisplayName = debuggerDisplay.Name;
                }
 
                // Suppresses members if inlineMembers is true,
                // does nothing otherwise.
                bool suppressInlineMembers = false;
 
                //
                // TypeName(count) for ICollection implementers
                // or
                // TypeName([[DebuggerDisplay.Value]])        // Inline
                // [[DebuggerDisplay.Value]]                  // Inline && !isRoot
                // or
                // [[ToString()]] if ToString overridden
                // or
                // TypeName 
                // 
                ICollection collection;
                if ((collection = obj as ICollection) != null)
                {
                    FormatCollectionHeader(result, collection);
                }
                else if (debuggerDisplay != null && !string.IsNullOrEmpty(debuggerDisplay.Value))
                {
                    if (isRoot)
                    {
                        result.Append(_formatter.TypeNameFormatter.FormatTypeName(type, _typeNameOptions));
                        result.Append('(');
                    }
 
                    FormatWithEmbeddedExpressions(result, debuggerDisplay.Value, obj);
 
                    if (isRoot)
                    {
                        result.Append(')');
                    }
 
                    suppressInlineMembers = true;
                }
                else if (HasOverriddenToString(typeInfo))
                {
                    ObjectToString(result, obj);
                    suppressInlineMembers = true;
                }
                else
                {
                    result.Append(_formatter.TypeNameFormatter.FormatTypeName(type, _typeNameOptions));
                }
 
                MemberDisplayFormat memberFormat = _memberDisplayFormat;
 
                if (memberFormat == MemberDisplayFormat.Hidden)
                {
                    if (collection != null)
                    {
                        // NB: Collections specifically ignore MemberDisplayFormat.Hidden.
                        memberFormat = MemberDisplayFormat.SingleLine;
                    }
                    else
                    {
                        return result;
                    }
                }
 
                bool includeNonPublic = memberFormat == MemberDisplayFormat.SeparateLines;
                bool inlineMembers = memberFormat == MemberDisplayFormat.SingleLine;
 
                object proxy = GetDebuggerTypeProxy(obj);
                if (proxy != null)
                {
                    includeNonPublic = false;
                    suppressInlineMembers = false;
                }
 
                if (!suppressInlineMembers || !inlineMembers)
                {
                    FormatMembers(result, obj, proxy, includeNonPublic, inlineMembers);
                }
 
                return result;
            }
 
            #region Members
 
            private void FormatMembers(Builder result, object obj, object proxy, bool includeNonPublic, bool inlineMembers)
            {
                // TODO (tomat): we should not use recursion
                RuntimeHelpers.EnsureSufficientExecutionStack();
 
                result.Append(' ');
 
                // Note: Even if we've seen it before, we show a header 
                if (!VisitedObjects.Add(obj))
                {
                    result.AppendInfiniteRecursionMarker();
                    return;
                }
 
                bool membersFormatted = false;
 
                // handle special types only if a proxy isn't defined
                if (proxy == null)
                {
                    IDictionary dictionary;
                    IEnumerable enumerable;
                    if ((dictionary = obj as IDictionary) != null)
                    {
                        FormatDictionaryMembers(result, dictionary, inlineMembers);
                        membersFormatted = true;
                    }
                    else if ((enumerable = obj as IEnumerable) != null)
                    {
                        FormatSequenceMembers(result, enumerable, inlineMembers);
                        membersFormatted = true;
                    }
                }
 
                if (!membersFormatted)
                {
                    FormatObjectMembers(result, proxy ?? obj, obj.GetType().GetTypeInfo(), includeNonPublic, inlineMembers);
                }
 
                VisitedObjects.Remove(obj);
            }
 
            /// <summary>
            /// Formats object members to a list.
            /// 
            /// Inline == false:
            /// <code>
            /// { A=true, B=false, C=new int[3] { 1, 2, 3 } }
            /// </code>
            /// 
            /// Inline == true:
            /// <code>
            /// {
            ///   A: true,
            ///   B: false,
            ///   C: new int[3] { 1, 2, 3 }
            /// }
            /// </code>
            /// </summary>
            private void FormatObjectMembers(Builder result, object obj, TypeInfo preProxyTypeInfo, bool includeNonPublic, bool inline)
            {
                int lengthLimit = result.Remaining;
                if (lengthLimit < 0)
                {
                    return;
                }
 
                var members = new List<FormattedMember>();
 
                // Limits the number of members added into the result. Some more members may be added than it will fit into the result
                // and will be thrown away later but not many more.
                FormatObjectMembersRecursive(members, obj, includeNonPublic, ref lengthLimit);
                bool useCollectionFormat = UseCollectionFormat(members, preProxyTypeInfo);
 
                result.AppendGroupOpening();
 
                for (int i = 0; i < members.Count; i++)
                {
                    result.AppendCollectionItemSeparator(isFirst: i == 0, inline: inline);
                    if (useCollectionFormat)
                    {
                        members[i].AppendAsCollectionEntry(result);
                    }
                    else
                    {
                        members[i].Append(result, inline ? "=" : ": ");
                    }
 
                    if (result.Remaining <= 0)
                    {
                        break;
                    }
                }
 
                result.AppendGroupClosing(inline);
            }
 
            private static bool UseCollectionFormat(IEnumerable<FormattedMember> members, TypeInfo originalType)
            {
                return typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(originalType) && members.All(member => member.Index >= 0);
            }
 
            /// <summary>
            /// Enumerates sorted object members to display.
            /// </summary>
            private void FormatObjectMembersRecursive(List<FormattedMember> result, object obj, bool includeNonPublic, ref int lengthLimit)
            {
                Debug.Assert(obj != null);
 
                var members = new List<MemberInfo>();
 
                var type = obj.GetType().GetTypeInfo();
                while (type != null)
                {
                    members.AddRange(type.DeclaredFields.Where(f => !f.IsStatic));
                    members.AddRange(type.DeclaredProperties.Where(f => f.GetMethod != null && !f.GetMethod.IsStatic));
                    type = type.BaseType?.GetTypeInfo();
                }
 
                members.Sort((x, y) =>
                {
                    // Need case-sensitive comparison here so that the order of members is
                    // always well-defined (members can differ by case only). And we don't want to
                    // depend on that order.
                    int comparisonResult = StringComparer.OrdinalIgnoreCase.Compare(x.Name, y.Name);
                    if (comparisonResult == 0)
                    {
                        comparisonResult = StringComparer.Ordinal.Compare(x.Name, y.Name);
                    }
 
                    return comparisonResult;
                });
 
                foreach (var member in members)
                {
                    if (!_formatter.Filter.Include(member))
                    {
                        continue;
                    }
 
                    bool rootHidden = false, ignoreVisibility = false;
                    var browsable = (DebuggerBrowsableAttribute)member.GetCustomAttributes(typeof(DebuggerBrowsableAttribute), false).FirstOrDefault();
                    if (browsable != null)
                    {
                        if (browsable.State == DebuggerBrowsableState.Never)
                        {
                            continue;
                        }
 
                        ignoreVisibility = true;
                        rootHidden = browsable.State == DebuggerBrowsableState.RootHidden;
                    }
 
                    if (member is FieldInfo field)
                    {
                        if (!(includeNonPublic || ignoreVisibility || field.IsPublic || field.IsFamily || field.IsFamilyOrAssembly))
                        {
                            continue;
                        }
                    }
                    else
                    {
                        PropertyInfo property = (PropertyInfo)member;
 
                        var getter = property.GetMethod;
                        if (getter == null)
                        {
                            continue;
                        }
 
                        var setter = property.SetMethod;
 
                        // If not ignoring visibility include properties that has a visible getter or setter.
                        if (!(includeNonPublic || ignoreVisibility ||
                            getter.IsPublic || getter.IsFamily || getter.IsFamilyOrAssembly ||
                            (setter != null && (setter.IsPublic || setter.IsFamily || setter.IsFamilyOrAssembly))))
                        {
                            continue;
                        }
 
                        if (getter.GetParameters().Length > 0)
                        {
                            continue;
                        }
                    }
 
                    var debuggerDisplay = GetApplicableDebuggerDisplayAttribute(member);
                    if (debuggerDisplay != null)
                    {
                        string k = FormatWithEmbeddedExpressions(lengthLimit, debuggerDisplay.Name, obj) ?? member.Name;
                        string v = FormatWithEmbeddedExpressions(lengthLimit, debuggerDisplay.Value, obj) ?? string.Empty; // TODO: ?
                        if (!AddMember(result, new FormattedMember(-1, k, v), ref lengthLimit))
                        {
                            return;
                        }
 
                        continue;
                    }
 
                    Exception exception;
                    object value = GetMemberValue(member, obj, out exception);
                    if (exception != null)
                    {
                        var memberValueBuilder = MakeMemberBuilder(lengthLimit);
                        FormatException(memberValueBuilder, exception);
                        if (!AddMember(result, new FormattedMember(-1, member.Name, memberValueBuilder.ToString()), ref lengthLimit))
                        {
                            return;
                        }
 
                        continue;
                    }
 
                    if (rootHidden)
                    {
                        if (value != null && !VisitedObjects.Contains(value))
                        {
                            Array array;
                            if ((array = value as Array) != null)  // TODO (tomat): n-dim arrays
                            {
                                int i = 0;
                                foreach (object item in array)
                                {
                                    string name;
                                    Builder valueBuilder = MakeMemberBuilder(lengthLimit);
                                    FormatObjectRecursive(valueBuilder, item, isRoot: false, debuggerDisplayName: out name);
 
                                    if (!string.IsNullOrEmpty(name))
                                    {
                                        name = FormatWithEmbeddedExpressions(MakeMemberBuilder(lengthLimit), name, item).ToString();
                                    }
 
                                    if (!AddMember(result, new FormattedMember(i, name, valueBuilder.ToString()), ref lengthLimit))
                                    {
                                        return;
                                    }
 
                                    i++;
                                }
                            }
                            else if (_formatter.PrimitiveFormatter.FormatPrimitive(value, _primitiveOptions) == null && VisitedObjects.Add(value))
                            {
                                FormatObjectMembersRecursive(result, value, includeNonPublic, ref lengthLimit);
                                VisitedObjects.Remove(value);
                            }
                        }
                    }
                    else
                    {
                        string name;
                        Builder valueBuilder = MakeMemberBuilder(lengthLimit);
                        FormatObjectRecursive(valueBuilder, value, isRoot: false, debuggerDisplayName: out name);
 
                        if (string.IsNullOrEmpty(name))
                        {
                            name = member.Name;
                        }
                        else
                        {
                            name = FormatWithEmbeddedExpressions(MakeMemberBuilder(lengthLimit), name, value).ToString();
                        }
 
                        if (!AddMember(result, new FormattedMember(-1, name, valueBuilder.ToString()), ref lengthLimit))
                        {
                            return;
                        }
                    }
                }
            }
 
            private bool AddMember(List<FormattedMember> members, FormattedMember member, ref int remainingLength)
            {
                // Add this item even if we exceed the limit - its prefix might be appended to the result.
                members.Add(member);
 
                // We don't need to calculate an exact length, just a lower bound on the size.
                // We can add more members to the result than it will eventually fit, we shouldn't add less.
                // Add 2 more, even if only one or half of it fit, so that the separator is included in edge cases.
 
                if (remainingLength == int.MinValue)
                {
                    return false;
                }
 
                remainingLength -= member.MinimalLength;
                if (remainingLength <= 0)
                {
                    remainingLength = int.MinValue;
                }
 
                return true;
            }
 
            private void FormatException(Builder result, Exception exception)
            {
                result.Append("!<");
                result.Append(_formatter.TypeNameFormatter.FormatTypeName(exception.GetType(), _typeNameOptions));
                result.Append('>');
            }
 
            #endregion
 
            #region Collections
 
            private void FormatKeyValuePair(Builder result, object obj)
            {
                TypeInfo type = obj.GetType().GetTypeInfo();
                object key = type.GetDeclaredProperty("Key").GetValue(obj, Array.Empty<object>());
                object value = type.GetDeclaredProperty("Value").GetValue(obj, Array.Empty<object>());
                string _;
                result.AppendGroupOpening();
                result.AppendCollectionItemSeparator(isFirst: true, inline: true);
                FormatObjectRecursive(result, key, isRoot: false, debuggerDisplayName: out _);
                result.AppendCollectionItemSeparator(isFirst: false, inline: true);
                FormatObjectRecursive(result, value, isRoot: false, debuggerDisplayName: out _);
                result.AppendGroupClosing(inline: true);
            }
 
            private void FormatCollectionHeader(Builder result, ICollection collection)
            {
                if (collection is Array array)
                {
                    result.Append(_formatter.TypeNameFormatter.FormatArrayTypeName(array.GetType(), array, _typeNameOptions));
                    return;
                }
 
                result.Append(_formatter.TypeNameFormatter.FormatTypeName(collection.GetType(), _typeNameOptions));
                try
                {
                    result.Append('(');
                    result.Append(collection.Count.ToString());
                    result.Append(')');
                }
                catch (Exception)
                {
                    // skip
                }
            }
 
            private void FormatArray(Builder result, Array array)
            {
                FormatCollectionHeader(result, array);
 
                // NB: Arrays specifically ignore MemberDisplayFormat.Hidden.
 
                if (array.Rank > 1)
                {
                    FormatMultidimensionalArrayElements(result, array, inline: _memberDisplayFormat != MemberDisplayFormat.SeparateLines);
                }
                else
                {
                    result.Append(' ');
                    FormatSequenceMembers(result, array, inline: _memberDisplayFormat != MemberDisplayFormat.SeparateLines);
                }
            }
 
            private void FormatDictionaryMembers(Builder result, IDictionary dict, bool inline)
            {
                result.AppendGroupOpening();
 
                int i = 0;
                try
                {
                    IDictionaryEnumerator enumerator = dict.GetEnumerator();
                    IDisposable disposable = enumerator as IDisposable;
                    try
                    {
                        while (enumerator.MoveNext())
                        {
                            var entry = enumerator.Entry;
                            string _;
                            result.AppendCollectionItemSeparator(isFirst: i == 0, inline: inline);
                            result.AppendGroupOpening();
                            result.AppendCollectionItemSeparator(isFirst: true, inline: true);
                            FormatObjectRecursive(result, entry.Key, isRoot: false, debuggerDisplayName: out _);
                            result.AppendCollectionItemSeparator(isFirst: false, inline: true);
                            FormatObjectRecursive(result, entry.Value, isRoot: false, debuggerDisplayName: out _);
                            result.AppendGroupClosing(inline: true);
                            i++;
                        }
                    }
                    finally
                    {
                        disposable?.Dispose();
                    }
                }
                catch (Exception e)
                {
                    result.AppendCollectionItemSeparator(isFirst: i == 0, inline: inline);
                    FormatException(result, e);
                    result.Append(' ');
                    result.Append(_builderOptions.Ellipsis);
                }
 
                result.AppendGroupClosing(inline);
            }
 
            private void FormatSequenceMembers(Builder result, IEnumerable sequence, bool inline)
            {
                result.AppendGroupOpening();
                int i = 0;
 
                try
                {
                    foreach (var item in sequence)
                    {
                        string _;
                        result.AppendCollectionItemSeparator(isFirst: i == 0, inline: inline);
                        FormatObjectRecursive(result, item, isRoot: false, debuggerDisplayName: out _);
                        i++;
                    }
                }
                catch (Exception e)
                {
                    result.AppendCollectionItemSeparator(isFirst: i == 0, inline: inline);
                    FormatException(result, e);
                    result.Append(" ...");
                }
 
                result.AppendGroupClosing(inline);
            }
 
            private void FormatMultidimensionalArrayElements(Builder result, Array array, bool inline)
            {
                Debug.Assert(array.Rank > 1);
 
                if (array.Length == 0)
                {
                    result.AppendCollectionItemSeparator(isFirst: true, inline: true);
                    result.AppendGroupOpening();
                    result.AppendGroupClosing(inline: true);
                    return;
                }
 
                int[] indices = new int[array.Rank];
                for (int i = array.Rank - 1; i >= 0; i--)
                {
                    indices[i] = array.GetLowerBound(i);
                }
 
                int nesting = 0;
                int flatIndex = 0;
                while (true)
                {
                    // increment indices (lower index overflows to higher):
                    int i = indices.Length - 1;
                    while (indices[i] > array.GetUpperBound(i))
                    {
                        indices[i] = array.GetLowerBound(i);
                        result.AppendGroupClosing(inline: inline || nesting != 1);
                        nesting--;
 
                        i--;
                        if (i < 0)
                        {
                            return;
                        }
 
                        indices[i]++;
                    }
 
                    result.AppendCollectionItemSeparator(isFirst: flatIndex == 0, inline: inline || nesting != 1);
 
                    i = indices.Length - 1;
                    while (i >= 0 && indices[i] == array.GetLowerBound(i))
                    {
                        result.AppendGroupOpening();
                        nesting++;
 
                        // array isn't empty, so there is always an element following this separator
                        result.AppendCollectionItemSeparator(isFirst: true, inline: inline || nesting != 1);
 
                        i--;
                    }
 
                    string _;
                    FormatObjectRecursive(result, array.GetValue(indices), isRoot: false, debuggerDisplayName: out _);
 
                    indices[^1]++;
                    flatIndex++;
                }
            }
 
            #endregion
 
            #region Scalars
 
            private bool IsTuple(object obj)
            {
#if NETSTANDARD2_0
                if (obj is null)
                {
                    return false;
                }
 
                var type = obj.GetType();
                if (!type.IsGenericType)
                {
                    return false;
                }
 
                int backtick = type.FullName.IndexOf('`');
                if (backtick < 0)
                {
                    return false;
                }
 
                var nonGenericName = type.FullName[0..backtick];
                return nonGenericName == "System.ValueTuple" || nonGenericName == "System.Tuple";
#else
                return obj is ITuple;
#endif
            }
 
            private void ObjectToString(Builder result, object obj)
            {
                try
                {
                    string str = obj.ToString();
                    if (IsTuple(obj))
                    {
                        result.Append(str);
                    }
                    else
                    {
                        result.Append('[');
                        result.Append(str);
                        result.Append(']');
                    }
                }
                catch (Exception e)
                {
                    FormatException(result, e);
                }
            }
 
            #endregion
 
            #region DebuggerDisplay Embedded Expressions
 
            /// <summary>
            /// Evaluate a format string with possible member references enclosed in braces. 
            /// E.g. "goo = {GetGooString(),nq}, bar = {Bar}".
            /// </summary>
            /// <remarks>
            /// Although in theory any expression is allowed to be embedded in the string such behavior is in practice fundamentally broken.
            /// The attribute doesn't specify what language (VB, C#, F#, etc.) to use to parse these expressions. Even if it did all languages 
            /// would need to be able to evaluate each other language's expressions, which is not viable and the Expression Evaluator doesn't 
            /// work that way today. Instead it evaluates the embedded expressions in the language of the current method frame. When consuming 
            /// VB objects from C#, for example, the evaluation might fail due to language mismatch (evaluating VB expression using C# parser).
            /// 
            /// Therefore we limit the expressions to a simple language independent syntax: {clr-member-name} '(' ')' ',nq', 
            /// where parentheses and ,nq suffix (no-quotes) are optional and the name is an arbitrary CLR field, property, or method name.
            /// We then resolve the member by name using case-sensitive lookup first with fallback to case insensitive and evaluate it.
            /// If parentheses are present we only look for methods.
            /// Only parameterless members are considered.
            /// </remarks>
            private string FormatWithEmbeddedExpressions(int lengthLimit, string format, object obj)
            {
                if (string.IsNullOrEmpty(format))
                {
                    return null;
                }
 
                var builder = new Builder(_builderOptions.WithMaximumOutputLength(lengthLimit), suppressEllipsis: true);
                return FormatWithEmbeddedExpressions(builder, format, obj).ToString();
            }
 
            private Builder FormatWithEmbeddedExpressions(Builder result, string format, object obj)
            {
                int i = 0;
                while (i < format.Length)
                {
                    char c = format[i++];
                    if (c == '{')
                    {
                        if (i >= 2 && format[i - 2] == '\\')
                        {
                            result.Append('{');
                        }
                        else
                        {
                            int expressionEnd = format.IndexOf('}', i);
 
                            bool noQuotes, callableOnly;
                            string memberName;
                            if (expressionEnd == -1 || (memberName = ParseSimpleMemberName(format, i, expressionEnd, out noQuotes, out callableOnly)) == null)
                            {
                                // the expression isn't properly formatted
                                result.Append(format, i - 1, format.Length - i + 1);
                                break;
                            }
 
                            MemberInfo member = ResolveMember(obj, memberName, callableOnly);
                            if (member == null)
                            {
                                result.AppendFormat(callableOnly ? "!<Method '{0}' not found>" : "!<Member '{0}' not found>", memberName);
                            }
                            else
                            {
                                Exception exception;
                                object value = GetMemberValue(member, obj, out exception);
 
                                if (exception != null)
                                {
                                    FormatException(result, exception);
                                }
                                else
                                {
                                    MemberDisplayFormat oldMemberDisplayFormat = _memberDisplayFormat;
                                    CommonPrimitiveFormatterOptions oldPrimitiveOptions = _primitiveOptions;
 
                                    _memberDisplayFormat = MemberDisplayFormat.Hidden;
                                    _primitiveOptions = new CommonPrimitiveFormatterOptions(
                                        _primitiveOptions.NumberRadix,
                                        _primitiveOptions.IncludeCharacterCodePoints,
                                        quoteStringsAndCharacters: !noQuotes,
                                        escapeNonPrintableCharacters: _primitiveOptions.EscapeNonPrintableCharacters,
                                        cultureInfo: _primitiveOptions.CultureInfo);
 
                                    string _;
                                    FormatObjectRecursive(result, value, isRoot: false, debuggerDisplayName: out _);
 
                                    _primitiveOptions = oldPrimitiveOptions;
                                    _memberDisplayFormat = oldMemberDisplayFormat;
                                }
                            }
                            i = expressionEnd + 1;
                        }
                    }
                    else
                    {
                        result.Append(c);
                    }
                }
 
                return result;
            }
 
            #endregion
        }
    }
}