diff --git a/src/debugger/variables.cpp b/src/debugger/variables.cpp index 8e9efb94..888cd3d0 100644 --- a/src/debugger/variables.cpp +++ b/src/debugger/variables.cpp @@ -76,7 +76,155 @@ struct VariableMember VariableMember(const VariableMember &that) = delete; }; -static HRESULT FillValueAndType(VariableMember &member, Variable &var) +static bool IsTypeDisplayFallback(const std::string &value, const std::string &typeName) +{ + return value.size() == typeName.size() + 2 && + value.front() == '{' && + value.back() == '}' && + value.compare(1, typeName.size(), typeName) == 0; +} + +static bool IsZeroArgStringToString(IMetaDataImport *pMD, mdTypeDef typeDef, mdMethodDef methodDef) +{ + mdTypeDef memTypeDef; + ULONG nameLen; + WCHAR methodName[mdNameLen] = {0}; + DWORD methodAttr = 0; + PCCOR_SIGNATURE pSig = nullptr; + ULONG cbSig = 0; + if (FAILED(pMD->GetMethodProps(methodDef, &memTypeDef, + methodName, _countof(methodName), &nameLen, + &methodAttr, &pSig, &cbSig, nullptr, nullptr))) + return false; + + if (memTypeDef != typeDef || (methodAttr & mdStatic) || !(methodAttr & mdVirtual) || + (methodAttr & mdNewSlot) || !str_equal(methodName, W("ToString"))) + return false; + + ULONG convFlags = 0; + ULONG elementSize = CorSigUncompressData(pSig, &convFlags); + pSig += elementSize; + + if ((convFlags & IMAGE_CEE_CS_CALLCONV_MASK) == IMAGE_CEE_CS_CALLCONV_VARARG) + return false; + + if (convFlags & IMAGE_CEE_CS_CALLCONV_GENERIC) + { + ULONG gParams = 0; + elementSize = CorSigUncompressData(pSig, &gParams); + pSig += elementSize; + } + + ULONG cParams = 0; + elementSize = CorSigUncompressData(pSig, &cParams); + pSig += elementSize; + + CorElementType returnType = CorSigUncompressElementType(pSig); + return cParams == 0 && returnType == ELEMENT_TYPE_STRING; +} + +static bool TypeDeclaresToStringOverride(IMetaDataImport *pMD, mdTypeDef typeDef) +{ + ULONG numMethods = 0; + HCORENUM hEnum = nullptr; + mdMethodDef methodDef = mdMethodDefNil; + while (SUCCEEDED(pMD->EnumMethodsWithName(&hEnum, typeDef, W("ToString"), &methodDef, 1, &numMethods)) && + numMethods != 0) + { + if (IsZeroArgStringToString(pMD, typeDef, methodDef)) + { + pMD->CloseEnum(hEnum); + return true; + } + } + pMD->CloseEnum(hEnum); + return false; +} + +static bool HasOverriddenToString(ICorDebugValue *pInputValue) +{ + BOOL isNull = TRUE; + ToRelease pValue; + if (FAILED(DereferenceAndUnboxValue(pInputValue, &pValue, &isNull))) + return false; + if (isNull) + return false; + + ToRelease pValue2; + ToRelease pType; + if (FAILED(pValue->QueryInterface(IID_ICorDebugValue2, (LPVOID*) &pValue2)) || + FAILED(pValue2->GetExactType(&pType))) + return false; + + while (pType) + { + std::string typeName; + if (FAILED(TypePrinter::GetTypeOfValue(pType, typeName))) + return false; + // Exception.ToString() includes stack traces; keep existing exception variable display. + if (typeName == "System.Object" || typeName == "System.ValueType" || typeName == "System.Exception") + return false; + + ToRelease pClass; + mdTypeDef typeDef = mdTypeDefNil; + ToRelease pModule; + ToRelease pMDUnknown; + ToRelease pMD; + if (FAILED(pType->GetClass(&pClass)) || + FAILED(pClass->GetToken(&typeDef)) || + FAILED(pClass->GetModule(&pModule)) || + FAILED(pModule->GetMetaDataInterface(IID_IMetaDataImport, &pMDUnknown)) || + FAILED(pMDUnknown->QueryInterface(IID_IMetaDataImport, (LPVOID*) &pMD))) + { + return false; + } + + if (TypeDeclaresToStringOverride(pMD, typeDef)) + return true; + + ToRelease pBaseType; + if (FAILED(pType->GetBase(&pBaseType)) || pBaseType == nullptr) + return false; + pType = pBaseType.Detach(); + } + + return false; +} + +static HRESULT PrintValueWithImplicitToString(ICorDebugValue *pValue, ICorDebugThread *pThread, FrameLevel frameLevel, + int evalFlags, const std::string &receiverExpression, + EvalStackMachine *pEvalStackMachine, std::string &output) +{ + HRESULT Status; + + IfFailRet(PrintValue(pValue, output, true)); + + if (!pThread || receiverExpression.empty() || !pEvalStackMachine || (evalFlags & EVAL_NOFUNCEVAL)) + return S_OK; + + std::string typeName; + if (FAILED(TypePrinter::GetTypeOfValue(pValue, typeName))) + return S_OK; + if (!IsTypeDisplayFallback(output, typeName) || !HasOverriddenToString(pValue)) + return S_OK; + + ToRelease pToStringValue; + std::string evalOutput; + if (SUCCEEDED(pEvalStackMachine->EvaluateExpression(pThread, frameLevel, evalFlags, + receiverExpression + ".ToString()", + &pToStringValue, evalOutput)) && + pToStringValue != nullptr) + { + std::string display; + if (SUCCEEDED(PrintValue(pToStringValue, display, false))) + output = display; + } + + return S_OK; +} + +static HRESULT FillValueAndType(VariableMember &member, Variable &var, ICorDebugThread *pThread, FrameLevel frameLevel, + EvalStackMachine *pEvalStackMachine) { if (member.value == nullptr) { @@ -87,7 +235,8 @@ static HRESULT FillValueAndType(VariableMember &member, Variable &var) } TypePrinter::GetTypeOfValue(member.value, var.type); - return PrintValue(member.value, var.value, true); + return PrintValueWithImplicitToString(member.value, pThread, frameLevel, var.evalFlags, var.evaluateName, + pEvalStackMachine, var.value); } static HRESULT FetchFieldsAndProperties(Evaluator *pEvaluator, ICorDebugValue *pInputValue, ICorDebugThread *pThread, @@ -258,7 +407,8 @@ HRESULT Variables::GetStackVariables( ToRelease iCorValue; IfFailRet(getValue(&iCorValue, var.evalFlags)); IfFailRet(TypePrinter::GetTypeOfValue(iCorValue, var.type)); - IfFailRet(PrintValue(iCorValue, var.value)); + IfFailRet(PrintValueWithImplicitToString(iCorValue, pThread, frameId.getLevel(), var.evalFlags, var.evaluateName, + m_sharedEvalStackMachine.get(), var.value)); IfFailRet(AddVariableReference(var, frameId, iCorValue, ValueIsVariable)); variables.push_back(var); @@ -354,7 +504,7 @@ HRESULT Variables::GetChildren( bool isIndex = !it.name.empty() && it.name.at(0) == '['; if (var.name.find('(') == std::string::npos) // expression evaluator does not support typecasts var.evaluateName = ref.evaluateName + (isIndex ? "" : ".") + var.name; - IfFailRet(FillValueAndType(it, var)); + IfFailRet(FillValueAndType(it, var, pThread, ref.frameId.getLevel(), m_sharedEvalStackMachine.get())); IfFailRet(AddVariableReference(var, ref.frameId, it.value, ValueIsVariable)); variables.push_back(var); } @@ -404,7 +554,8 @@ HRESULT Variables::Evaluate( variable.evaluateName = expression; IfFailRet(TypePrinter::GetTypeOfValue(pResultValue, variable.type)); - IfFailRet(PrintValue(pResultValue, variable.value)); + IfFailRet(PrintValueWithImplicitToString(pResultValue, pThread, frameLevel, variable.evalFlags, variable.evaluateName, + m_sharedEvalStackMachine.get(), variable.value)); return AddVariableReference(variable, frameId, pResultValue, ValueIsVariable); } diff --git a/test-suite/MITestVariables/Program.cs b/test-suite/MITestVariables/Program.cs index 19021a9f..26d72a71 100644 --- a/test-suite/MITestVariables/Program.cs +++ b/test-suite/MITestVariables/Program.cs @@ -341,6 +341,50 @@ public override string ToString() } } + public class ToStringDisplayClass + { + public int Value = 42; + + public override string ToString() + { + return "class:" + Value.ToString(); + } + } + + public struct ToStringDisplayStruct + { + public int Value; + + public ToStringDisplayStruct(int value) + { + Value = value; + } + + public override string ToString() + { + return "struct:" + Value.ToString(); + } + } + + public class ToStringDisplayBase + { + public override string ToString() + { + return "base:7"; + } + } + + public class ToStringDisplayInherited : ToStringDisplayBase + { + public int Value = 7; + } + + public class ToStringNoOverrideClass + { + public int Value = 123; + public Guid Id = new Guid("11111111-2222-3333-4444-555555555555"); + } + public struct TestSetVarStruct { public static int static_field_i; @@ -548,6 +592,7 @@ static void Main(string[] args) Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK5"); Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK6"); Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK7"); + Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK8"); Context.EnableBreakpoint(@"__FILE__:__LINE__", "BREAK_GETTER"); Context.EnableBreakpoint(@"__FILE__:__LINE__", "bp_func1"); Context.EnableBreakpoint(@"__FILE__:__LINE__", "bp_func2"); @@ -1250,7 +1295,7 @@ static void Main(string[] args) int dummy7 = 7; Label.Breakpoint("BREAK7"); - Label.Checkpoint("test_eval_with_exception", "finish", (Object context) => { + Label.Checkpoint("test_eval_with_exception", "test_to_string_display", (Object context) => { Context Context = (Context)context; Context.WasBreakpointHit(@"__FILE__:__LINE__", "BREAK7"); @@ -1262,6 +1307,37 @@ static void Main(string[] args) Context.Continue(@"__FILE__:__LINE__"); }); + Guid toStringGuid = new Guid("11111111-2222-3333-4444-555555555555"); + Version toStringVersion = new Version(1, 2, 3, 4); + TimeSpan toStringTimeSpan = new TimeSpan(1, 2, 3); + ToStringDisplayClass toStringClass = new ToStringDisplayClass(); + ToStringDisplayStruct toStringStruct = new ToStringDisplayStruct(99); + ToStringDisplayInherited toStringInherited = new ToStringDisplayInherited(); + ToStringNoOverrideClass toStringNoOverride = new ToStringNoOverrideClass(); + + int dummy8 = 8; Label.Breakpoint("BREAK8"); + + Label.Checkpoint("test_to_string_display", "finish", (Object context) => { + Context Context = (Context)context; + Context.WasBreakpointHit(@"__FILE__:__LINE__", "BREAK8"); + + Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringGuid", "11111111-2222-3333-4444-555555555555"); + Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringVersion", "1.2.3.4"); + Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringTimeSpan", "01:02:03"); + Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringClass", "class:42"); + Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringStruct", "struct:99"); + Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringInherited", "base:7"); + Context.CreateAndCompareVar(@"__FILE__:__LINE__", "toStringNoOverride", "{MITestVariables.ToStringNoOverrideClass}"); + Context.GetAndCheckChildValue(@"__FILE__:__LINE__", "123", "toStringNoOverride", 0, false, 0); + Context.GetAndCheckChildValue(@"__FILE__:__LINE__", "11111111-2222-3333-4444-555555555555", "toStringNoOverride", 1, false, 0); + + var res = Context.MIDebugger.Request(String.Format("-var-create - * \"toStringGuid\" --evalFlags {0}", (int)Context.enum_EVALFLAGS.EVAL_NOFUNCEVAL)); + Assert.Equal(MIResultClass.Done, res.Class, @"__FILE__:__LINE__"); + Assert.Equal("{System.Guid}", ((MIConst)res["value"]).CString, @"__FILE__:__LINE__"); + + Context.Continue(@"__FILE__:__LINE__"); + }); + Label.Checkpoint("finish", "", (Object context) => { Context Context = (Context)context; Context.WasExit(@"__FILE__:__LINE__"); diff --git a/test-suite/VSCodeTestVariables/Program.cs b/test-suite/VSCodeTestVariables/Program.cs index 081a84ce..d18bab6e 100644 --- a/test-suite/VSCodeTestVariables/Program.cs +++ b/test-suite/VSCodeTestVariables/Program.cs @@ -432,6 +432,50 @@ public override string ToString() } } + public class ToStringDisplayClass + { + public int Value = 42; + + public override string ToString() + { + return "class:" + Value.ToString(); + } + } + + public struct ToStringDisplayStruct + { + public int Value; + + public ToStringDisplayStruct(int value) + { + Value = value; + } + + public override string ToString() + { + return "struct:" + Value.ToString(); + } + } + + public class ToStringDisplayBase + { + public override string ToString() + { + return "base:7"; + } + } + + public class ToStringDisplayInherited : ToStringDisplayBase + { + public int Value = 7; + } + + public class ToStringNoOverrideClass + { + public int Value = 123; + public Guid Id = new Guid("11111111-2222-3333-4444-555555555555"); + } + public struct TestSetVarStruct { public static int static_field_i; @@ -638,6 +682,7 @@ static void Main(string[] args) Context.AddBreakpoint(@"__FILE__:__LINE__", "bp3"); Context.AddBreakpoint(@"__FILE__:__LINE__", "bp4"); Context.AddBreakpoint(@"__FILE__:__LINE__", "bp5"); + Context.AddBreakpoint(@"__FILE__:__LINE__", "bp_to_string"); Context.AddBreakpoint(@"__FILE__:__LINE__", "bp_func1"); Context.AddBreakpoint(@"__FILE__:__LINE__", "bp_func2"); Context.AddBreakpoint(@"__FILE__:__LINE__", "bp_getter"); @@ -1381,7 +1426,7 @@ static void Main(string[] args) i++; Label.Breakpoint("bp5"); - Label.Checkpoint("test_eval_exception", "finish", (Object context) => { + Label.Checkpoint("test_eval_exception", "test_to_string_display", (Object context) => { Context Context = (Context)context; Context.WasBreakpointHit(@"__FILE__:__LINE__", "bp5"); Int64 frameId = Context.DetectFrameId(@"__FILE__:__LINE__", "bp5"); @@ -1402,6 +1447,40 @@ static void Main(string[] args) Context.Continue(@"__FILE__:__LINE__"); }); + Guid toStringGuid = new Guid("11111111-2222-3333-4444-555555555555"); + Version toStringVersion = new Version(1, 2, 3, 4); + TimeSpan toStringTimeSpan = new TimeSpan(1, 2, 3); + ToStringDisplayClass toStringClass = new ToStringDisplayClass(); + ToStringDisplayStruct toStringStruct = new ToStringDisplayStruct(99); + ToStringDisplayInherited toStringInherited = new ToStringDisplayInherited(); + ToStringNoOverrideClass toStringNoOverride = new ToStringNoOverrideClass(); + + i++; Label.Breakpoint("bp_to_string"); + + Label.Checkpoint("test_to_string_display", "finish", (Object context) => { + Context Context = (Context)context; + Context.WasBreakpointHit(@"__FILE__:__LINE__", "bp_to_string"); + Int64 frameId = Context.DetectFrameId(@"__FILE__:__LINE__", "bp_to_string"); + + int variablesReference_Locals = Context.GetVariablesReference(@"__FILE__:__LINE__", frameId, "Locals"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_Locals, "System.Guid", "toStringGuid", "11111111-2222-3333-4444-555555555555"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_Locals, "System.Version", "toStringVersion", "1.2.3.4"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_Locals, "System.TimeSpan", "toStringTimeSpan", "01:02:03"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_Locals, "VSCodeTestVariables.ToStringDisplayClass", "toStringClass", "class:42"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_Locals, "VSCodeTestVariables.ToStringDisplayStruct", "toStringStruct", "struct:99"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_Locals, "VSCodeTestVariables.ToStringDisplayInherited", "toStringInherited", "base:7"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_Locals, "VSCodeTestVariables.ToStringNoOverrideClass", "toStringNoOverride", "{VSCodeTestVariables.ToStringNoOverrideClass}"); + + Context.GetAndCheckValue(@"__FILE__:__LINE__", frameId, "toStringGuid", "11111111-2222-3333-4444-555555555555"); + Context.GetAndCheckValue(@"__FILE__:__LINE__", frameId, "toStringClass", "class:42"); + + int variablesReference_toStringNoOverride = Context.GetChildVariablesReference(@"__FILE__:__LINE__", variablesReference_Locals, "toStringNoOverride"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_toStringNoOverride, "int", "Value", "123"); + Context.EvalVariable(@"__FILE__:__LINE__", variablesReference_toStringNoOverride, "System.Guid", "Id", "11111111-2222-3333-4444-555555555555"); + + Context.Continue(@"__FILE__:__LINE__"); + }); + Label.Checkpoint("finish", "", (Object context) => { Context Context = (Context)context; Context.WasExit(@"__FILE__:__LINE__");