Dnspy/dnSpy/Roslyn/dnSpy.Roslyn/Debugger/Formatters/DebuggerDisplayAttributeFormatter.cs
2021-09-20 18:20:01 +02:00

306 lines
11 KiB
C#

/*
Copyright (C) 2014-2019 de4dot@gmail.com
This file is part of dnSpy
dnSpy is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
dnSpy is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using dnSpy.Contracts.Debugger.DotNet.Evaluation;
using dnSpy.Contracts.Debugger.DotNet.Evaluation.Formatters;
using dnSpy.Contracts.Debugger.Evaluation;
using dnSpy.Contracts.Debugger.Text;
using dnSpy.Debugger.DotNet.Metadata;
namespace dnSpy.Roslyn.Debugger.Formatters {
readonly struct DebuggerDisplayAttributeFormatter {
readonly DbgEvaluationInfo evalInfo;
readonly LanguageFormatter languageFormatter;
readonly IDbgTextWriter output;
readonly DbgValueFormatterOptions options;
readonly CultureInfo? cultureInfo;
public DebuggerDisplayAttributeFormatter(DbgEvaluationInfo evalInfo, LanguageFormatter languageFormatter, IDbgTextWriter output, DbgValueFormatterOptions options, CultureInfo? cultureInfo) {
Debug.Assert((options & DbgValueFormatterOptions.NoDebuggerDisplay) == 0);
this.evalInfo = evalInfo;
this.languageFormatter = languageFormatter;
this.output = output;
this.options = options;
this.cultureInfo = cultureInfo;
}
[Flags]
enum DisplayPartFlags : byte {
None = 0,
EvaluateText = 0x01,
}
readonly struct DisplayPart {
public readonly DisplayPartFlags Flags;
public readonly string Text;
DisplayPart(DisplayPartFlags flags, string text) {
Flags = flags;
Text = text ?? throw new ArgumentNullException(nameof(text));
}
public static DisplayPart CreateText(string text) => new DisplayPart(DisplayPartFlags.None, text);
public static DisplayPart CreateEvaluate(string text) => new DisplayPart(DisplayPartFlags.EvaluateText, text);
public override string ToString() => $"{Flags}: '{Text}'";
}
sealed class TypeState : IDisposable {
public static readonly TypeState Empty = new TypeState(null, Array.Empty<DisplayPart>(), Array.Empty<DisplayPart>(), Array.Empty<DisplayPart>());
public readonly DbgEvaluationContext? TypeContext;
public readonly DisplayPart[] NameParts;
public readonly DisplayPart[] ValueParts;
public readonly DisplayPart[] TypeParts;
#pragma warning disable CS8618 // Non-nullable field is uninitialized.
public TypeState(DbgEvaluationContext? typeContext, DisplayPart[] nameParts, DisplayPart[] valueParts, DisplayPart[] typeParts) {
#pragma warning restore CS8618 // Non-nullable field is uninitialized.
TypeContext = typeContext;
NameParts = nameParts ?? throw new ArgumentNullException(nameof(nameParts));
ValueParts = valueParts ?? throw new ArgumentNullException(nameof(valueParts));
TypeParts = typeParts ?? throw new ArgumentNullException(nameof(typeParts));
if (typeContext is not null) {
lockObj = new object();
eeStates = new Dictionary<string, object?>(StringComparer.Ordinal);
}
}
readonly object lockObj;
readonly Dictionary<string, object?> eeStates;
public object? GetExpressionEvaluatorState(DbgExpressionEvaluator expressionEvaluator, string expression) {
lock (lockObj) {
if (eeStates.TryGetValue(expression, out var state))
return state;
state = expressionEvaluator.CreateExpressionEvaluatorState();
eeStates.Add(expression, state);
return state;
}
}
public void Dispose() {
TypeContext?.Close();
eeStates?.Clear();
}
}
public bool FormatName(DbgDotNetValue value) {
var typeState = GetOrCreateTypeState(value.Type, evalInfo.Context.Language);
return Format(value, typeState, typeState.NameParts);
}
public bool FormatValue(DbgDotNetValue value) {
var typeState = GetOrCreateTypeState(value.Type, evalInfo.Context.Language);
return Format(value, typeState, typeState.ValueParts);
}
public bool FormatType(DbgDotNetValue value) {
var typeState = GetOrCreateTypeState(value.Type, evalInfo.Context.Language);
return Format(value, typeState, typeState.TypeParts);
}
bool Format(DbgDotNetValue value, TypeState typeState, DisplayPart[] displayParts) {
if (displayParts.Length == 0)
return false;
var evaluator = evalInfo.Context.GetDebuggerDisplayAttributeEvaluator();
foreach (var part in displayParts) {
if ((part.Flags & DisplayPartFlags.EvaluateText) == 0)
output.Write(DbgTextColor.DebuggerDisplayAttributeEval, part.Text);
else {
var eeState = typeState.GetExpressionEvaluatorState(evalInfo.Context.Language.ExpressionEvaluator, part.Text);
DbgDotNetEvalResult evalRes = default;
try {
var evalInfo2 = new DbgEvaluationInfo(typeState.TypeContext!, evalInfo.Frame, evalInfo.CancellationToken);
evalRes = evaluator.Evaluate(evalInfo2, value, part.Text, DbgEvaluationOptions.Expression, eeState);
if (evalRes.Error is not null) {
output.Write(DbgTextColor.Error, "<<<");
output.Write(DbgTextColor.Error, evalRes.Error);
output.Write(DbgTextColor.Error, ">>>");
}
else {
// Prevent recursive calls
var options = this.options | DbgValueFormatterOptions.NoDebuggerDisplay;
options &= ~DbgValueFormatterOptions.NoStringQuotes;
options = PredefinedFormatSpecifiers.GetValueFormatterOptions(evalRes.FormatSpecifiers, options);
languageFormatter.FormatValue(evalInfo, output, evalRes.Value!, options, cultureInfo);
}
}
finally {
if (evalRes.Value != value)
evalRes.Value?.Dispose();
}
}
}
return true;
}
TypeState GetOrCreateTypeState(DmdType type, DbgLanguage language) {
var state = StateWithKey<TypeState>.TryGet(type, language);
if (state is not null)
return state;
return GetOrCreateTypeStateCore(type, language);
}
TypeState GetOrCreateTypeStateCore(DmdType type, DbgLanguage language) {
var state = CreateTypeState(type, language);
return StateWithKey<TypeState>.GetOrCreate(type, language, () => state);
}
TypeState CreateTypeState(DmdType type, DbgLanguage language) {
var info = GetDisplayParts(type);
if (info.nameParts.Length == 0 && info.valueParts.Length == 0 && info.typeParts.Length == 0)
return TypeState.Empty;
var context = language.CreateContext(evalInfo.Runtime, null);
var state = new TypeState(context, info.nameParts, info.valueParts, info.typeParts);
context.Runtime.CloseOnExit(state);
return state;
}
static bool ShouldIgnoreDebuggerDisplayAttribute(DmdType type) {
// We have special support for formatting KeyValuePair<K, V> and DictionaryEntry, so ignore all DebuggerDisplayAttributes.
// (Only Unity and older Mono versions have a DebuggerDisplayAttribute on them)
if (type.IsConstructedGenericType) {
if (type.MetadataName == "KeyValuePair`2" && type.MetadataNamespace == "System.Collections.Generic")
return type.GetGenericTypeDefinition() == type.AppDomain.GetWellKnownType(DmdWellKnownType.System_Collections_Generic_KeyValuePair_T2, isOptional: true);
return false;
}
else {
if (type.MetadataName == "DictionaryEntry" && type.MetadataNamespace == "System.Collections")
return type == type.AppDomain.GetWellKnownType(DmdWellKnownType.System_Collections_DictionaryEntry, isOptional: true);
return false;
}
}
(DisplayPart[] nameParts, DisplayPart[] valueParts, DisplayPart[] typeParts) GetDisplayParts(DmdType type) {
var ddaType = type.AppDomain.GetWellKnownType(DmdWellKnownType.System_Diagnostics_DebuggerDisplayAttribute, isOptional: true);
Debug2.Assert(ddaType is not null);
bool forceNoAttr = ShouldIgnoreDebuggerDisplayAttribute(type);
string? nameDisplayString = null, valueDisplayString = null, typeDisplayString = null;
if (!forceNoAttr && ddaType is not null) {
var attr = type.FindCustomAttribute(ddaType, inherit: true);
if (attr is null) {
if (type.CanCastTo(type.AppDomain.System_Type)) {
// Show the same thing VS shows
valueDisplayString = @"\{Name = {Name} FullName = {FullName}\}";
}
}
else {
if (attr.ConstructorArguments.Count == 1)
valueDisplayString = attr.ConstructorArguments[0].Value as string;
nameDisplayString = GetString(attr, nameof(DebuggerDisplayAttribute.Name));
typeDisplayString = GetString(attr, nameof(DebuggerDisplayAttribute.Type));
}
}
var nameParts = CreateDisplayParts(nameDisplayString);
var valueParts = CreateDisplayParts(valueDisplayString);
var typeParts = CreateDisplayParts(typeDisplayString);
return (nameParts, valueParts, typeParts);
}
static string? GetString(DmdCustomAttributeData ca, string propertyName) {
if (ca is null)
return null;
foreach (var arg in ca.NamedArguments) {
if (arg.IsField)
continue;
if (arg.MemberName != propertyName)
continue;
if (arg.TypedValue.ArgumentType != arg.TypedValue.ArgumentType.AppDomain.System_String)
continue;
return arg.TypedValue.Value as string;
}
return null;
}
static DisplayPart[] CreateDisplayParts(string? debuggerDisplayString) {
if (debuggerDisplayString is null)
return Array.Empty<DisplayPart>();
if (debuggerDisplayString.Length == 0)
return new[] { DisplayPart.CreateText(string.Empty) };
var list = ListCache<DisplayPart>.AllocList();
var sb = ObjectCache.AllocStringBuilder();
int pos = 0;
for (;;) {
sb.Clear();
var text = ReadText(debuggerDisplayString, ref pos, sb);
if (text.Length != 0)
list.Add(DisplayPart.CreateText(text));
sb.Clear();
var expression = ReadEvalText(debuggerDisplayString, ref pos, sb);
if (expression.Length != 0)
list.Add(DisplayPart.CreateEvaluate(expression));
if (pos >= debuggerDisplayString.Length)
break;
}
ObjectCache.Free(ref sb);
return ListCache<DisplayPart>.FreeAndToArray(ref list);
}
static string ReadText(string s, ref int pos, StringBuilder sb) {
while (pos < s.Length) {
var c = s[pos++];
if (c == '{')
break;
if (c == '\\' && pos < s.Length) {
c = s[pos++];
switch (c) {
case '{':
case '}':
case '\\':
break;
default:
sb.Append('\\');
break;
}
}
sb.Append(c);
}
return sb.ToString();
}
static string ReadEvalText(string s, ref int pos, StringBuilder sb) {
int braceCount = 1;
while (pos < s.Length) {
var c = s[pos++];
if (c == '}') {
if (braceCount <= 1)
break;
braceCount--;
}
else if (c == '{')
braceCount++;
sb.Append(c);
}
return sb.ToString();
}
}
}