diff --git a/kdevplatform/debugger/variable/variablecollection.cpp b/kdevplatform/debugger/variable/variablecollection.cpp index e938913c051..958b991f4af 100644 --- a/kdevplatform/debugger/variable/variablecollection.cpp +++ b/kdevplatform/debugger/variable/variablecollection.cpp @@ -121,7 +121,7 @@ void Variable::setShowError (bool v) reportChange(); } -bool Variable::showError() +bool Variable::showError() const { return m_showError; } diff --git a/kdevplatform/debugger/variable/variablecollection.h b/kdevplatform/debugger/variable/variablecollection.h index a1c1af2d0a2..4a84b7f64f8 100644 --- a/kdevplatform/debugger/variable/variablecollection.h +++ b/kdevplatform/debugger/variable/variablecollection.h @@ -54,7 +54,7 @@ class KDEVPLATFORMDEBUGGER_EXPORT Variable : public TreeItem QString type() const; void setTopLevel(bool v); void setShowError(bool v); - bool showError(); + [[nodiscard]] bool showError() const; using TreeItem::setHasMore; using TreeItem::setHasMoreInitial; diff --git a/plugins/debuggercommon/mi/micommand.cpp b/plugins/debuggercommon/mi/micommand.cpp index bba0ade060b..3370298c5a9 100644 --- a/plugins/debuggercommon/mi/micommand.cpp +++ b/plugins/debuggercommon/mi/micommand.cpp @@ -323,6 +323,12 @@ CommandType MICommand::type() const return type_; } +bool MICommand::needsContext() const +{ + return (type_ >= StackInfoDepth && type_ <= StackListLocals) + || (type_ >= VarAssign && type_ <= VarUpdate && type_ != VarDelete && type_ != VarSetFormat); +} + int MICommand::thread() const { return m_thread; diff --git a/plugins/debuggercommon/mi/micommand.h b/plugins/debuggercommon/mi/micommand.h index c04b3707a63..bc7ece69080 100644 --- a/plugins/debuggercommon/mi/micommand.h +++ b/plugins/debuggercommon/mi/micommand.h @@ -96,6 +96,11 @@ class MICommand virtual ~MICommand(); CommandType type() const; + /** + * @return whether this command should be executed in the context of a specific thread and frame + */ + [[nodiscard]] bool needsContext() const; + virtual QString miCommand() const; CommandFlags flags() const {return flags_;} diff --git a/plugins/debuggercommon/midebugsession.cpp b/plugins/debuggercommon/midebugsession.cpp index 955b5dcfae1..b8de59f4fd5 100644 --- a/plugins/debuggercommon/midebugsession.cpp +++ b/plugins/debuggercommon/midebugsession.cpp @@ -854,14 +854,7 @@ void MIDebugSession::queueCmd(std::unique_ptr cmd) << (m_stateReloadInProgress ? "(state reloading)" : "") << m_commandQueue->count() << "pending"; - bool varCommandWithContext= (cmd->type() >= MI::VarAssign - && cmd->type() <= MI::VarUpdate - && cmd->type() != MI::VarDelete); - - bool stackCommandWithContext = (cmd->type() >= MI::StackInfoDepth - && cmd->type() <= MI::StackListLocals); - - if (varCommandWithContext || stackCommandWithContext) { + if (cmd->needsContext()) { if (cmd->thread() == -1) qCDebug(DEBUGGERCOMMON) << "\t--thread will be added on execution"; @@ -908,16 +901,7 @@ void MIDebugSession::executeCmd() setDebuggerStateOn(s_dbgNotListening); } - bool varCommandWithContext= (currentCmd->type() >= MI::VarAssign - && currentCmd->type() <= MI::VarUpdate - && currentCmd->type() != MI::VarDelete); - - bool stackCommandWithContext = (currentCmd->type() >= MI::StackInfoDepth - && currentCmd->type() <= MI::StackListLocals); - - if (varCommandWithContext || stackCommandWithContext) { - // Most var commands should be executed in the context - // of the selected thread and frame. + if (currentCmd->needsContext()) { if (currentCmd->thread() == -1) currentCmd->setThread(frameStackModel()->currentThread()); diff --git a/plugins/debuggercommon/mivariable.cpp b/plugins/debuggercommon/mivariable.cpp index 939fdcd4a02..b9846a0fa2b 100644 --- a/plugins/debuggercommon/mivariable.cpp +++ b/plugins/debuggercommon/mivariable.cpp @@ -18,6 +18,14 @@ using namespace KDevelop; using namespace KDevMI; using namespace KDevMI::MI; +namespace { +[[nodiscard]] QString valueField() +{ + return QStringLiteral("value"); +} + +} // unnamed namespace + bool MIVariable::sessionIsAlive() const { if (!m_debugSession) @@ -49,7 +57,7 @@ MIVariable *MIVariable::createChild(const Value& child) appendChild(var); var->setType(child[QStringLiteral("type")].literal()); - var->setValue(formatValue(child[QStringLiteral("value")].literal())); + var->setValueToValueFieldOf(child); var->setChanged(true); return var; } @@ -102,9 +110,10 @@ class CreateVarobjHandler : public MICommandHandler MIVariable* variable = m_variable.data(); variable->deleteChildren(); variable->setInScope(true); - if (r.isReasonError()) { - variable->setShowError(true); - } else { + + const auto isError = r.isReasonError(); + variable->setShowError(isError); + if (!isError) { variable->setVarobj(r[QStringLiteral("name")].literal()); bool hasMore = false; @@ -122,8 +131,8 @@ class CreateVarobjHandler : public MICommandHandler variable->setHasMore(hasMore); variable->setType(r[QStringLiteral("type")].literal()); - variable->setValue(variable->formatValue(r[QStringLiteral("value")].literal())); - hasValue = !r[QStringLiteral("value")].literal().isEmpty(); + const auto rawValue = variable->setValueToValueFieldOf(r); + hasValue = !rawValue.isEmpty(); if (variable->isExpanded() && r[QStringLiteral("numchild")].toInt()) { variable->fetchMoreChildren(); } @@ -293,7 +302,7 @@ void MIVariable::handleUpdate(const Value& var) if (var.hasField(QStringLiteral("type_changed")) && var[QStringLiteral("type_changed")].literal() == QLatin1String("true")) { setType(var[QStringLiteral("new_type")].literal()); } - setValue(formatValue(var[QStringLiteral("value")].literal())); + setValueToValueFieldOf(var); setChanged(true); setHasMore(var.hasField(QStringLiteral("has_more")) && var[QStringLiteral("has_more")].toInt()); } @@ -319,8 +328,9 @@ class SetFormatHandler : public MICommandHandler void handle(const ResultRecord &r) override { - if(m_variable && r.hasField(QStringLiteral("value"))) - m_variable->setValue(m_variable->formatValue(r[QStringLiteral("value")].literal())); + if (auto* const variable = m_variable.get()) { + variable->setValueToOptionalValueFieldOf(r); + } } private: QPointer m_variable; @@ -336,14 +346,19 @@ void MIVariable::formatChanged() var->setFormat(format()); } } + return; } - else - { - if (sessionIsAlive()) { - m_debugSession->addCommand(VarSetFormat, - QStringLiteral(" %1 %2 ").arg(m_varobj, format2str(format())), - new SetFormatHandler(this)); - } + + if (m_varobj.isEmpty() || !sessionIsAlive()) { + return; + } + + constexpr auto commandType = VarSetFormat; + QString arguments = m_varobj + QLatin1Char{' '} + format2str(format()); + if (auto customHandler = handlerOfSetFormatCommand()) + m_debugSession->addCommand(commandType, std::move(arguments), std::move(customHandler)); + else { + m_debugSession->addCommand(commandType, std::move(arguments), new SetFormatHandler(this)); } } @@ -352,4 +367,23 @@ QString MIVariable::formatValue(const QString &rawValue) const return rawValue; } +QString MIVariable::setValueToValueFieldOf(const MI::Value& tupleValue) +{ + auto rawValue = tupleValue[valueField()].literal(); + setValue(formatValue(rawValue)); + return rawValue; +} + +void MIVariable::setValueToOptionalValueFieldOf(const Value& tupleValue) +{ + if (tupleValue.hasField(valueField())) { + setValueToValueFieldOf(tupleValue); + } +} + +std::function MIVariable::handlerOfSetFormatCommand() +{ + return {}; +} + #include "moc_mivariable.cpp" diff --git a/plugins/debuggercommon/mivariable.h b/plugins/debuggercommon/mivariable.h index 7a12893cf22..3f7490ececb 100644 --- a/plugins/debuggercommon/mivariable.h +++ b/plugins/debuggercommon/mivariable.h @@ -13,6 +13,7 @@ #include +#include class CreateVarobjHandler; class FetchMoreChildrenHandler; @@ -59,6 +60,20 @@ class MIVariable : public KDevelop::Variable QString enquotedExpression() const; virtual QString formatValue(const QString &rawValue) const; + /** + * Format the value of a field "value" of a given tuple value and assign the result to @c this->value(). + * + * @pre @p tupleValue contains a field named "value" + * @return unformatted, raw value of the "value" field of @p tupleValue + */ + QString setValueToValueFieldOf(const MI::Value& tupleValue); + + /** + * If a given tuple value has a field "value", format the value + * of the field and assign the result to @c this->value(). + */ + void setValueToOptionalValueFieldOf(const MI::Value& tupleValue); + bool sessionIsAlive() const; void setVarobj(const QString& v); @@ -67,6 +82,14 @@ class MIVariable : public KDevelop::Variable QPointer m_debugSession; private: + /** + * @return a callback to handle the result of an MI command @c -var-set-format + * for @c this->varobj() in place of the default handler + * + * The default implementation returns an empty function in order to use the default handler. + */ + [[nodiscard]] virtual std::function handlerOfSetFormatCommand(); + QString m_varobj; // How many children should be fetched in one diff --git a/plugins/debuggercommon/tests/debuggertestbase.cpp b/plugins/debuggercommon/tests/debuggertestbase.cpp index 276b7b4fbc9..0d132743c9a 100644 --- a/plugins/debuggercommon/tests/debuggertestbase.cpp +++ b/plugins/debuggercommon/tests/debuggertestbase.cpp @@ -1885,6 +1885,96 @@ void DebuggerTestBase::testVariablesQuicklySwitchFrame() WAIT_FOR_STATE(session, IDebugSession::EndedState); } +void DebuggerTestBase::testVariablesAttributes() +{ + auto* const session = createTestDebugSession(); + TestLaunchConfiguration cfg; + + variableCollection()->variableWidgetShown(); + + addDebugeeBreakpoint(24); + addDebugeeBreakpoint(29); + ActiveStateSessionSpy sessionSpy(session); + START_DEBUGGING_AND_WAIT_FOR_PAUSED_STATE_E(session, cfg, sessionSpy); + + auto* const jVariable = variableCollection()->watches()->add("j"); + QVERIFY(jVariable); + QCOMPARE(jVariable->canSetFormat(), true); + // Verify initialization of attributes in the constructor of a variable. + QCOMPARE(jVariable->expression(), "j"); + QCOMPARE(jVariable->showError(), false); + QCOMPARE(jVariable->format(), Variable::Natural); + + // Wait for the results of an MI command -var-create. + WAIT_FOR_STATE_AND_IDLE(session, IDebugSession::PausedState); + QCOMPARE(currentMiLine(session), 29); + // There is no variable named `j` in the scope of main(). + QCOMPARE(jVariable->showError(), true); + + // Setting format of a not yet attached variable succeeds but does not modify the value of the variable. + jVariable->setFormat(Variable::Octal); + QCOMPARE(jVariable->format(), Variable::Octal); + + CONTINUE_AND_WAIT_FOR_PAUSED_STATE(session, sessionSpy); + QCOMPARE(currentMiLine(session), 24); + // A variable named `j` is present in the scope of foo(). + QCOMPARE(jVariable->showError(), false); + QCOMPARE(jVariable->inScope(), true); + QCOMPARE(jVariable->type(), "int"); + QCOMPARE(jVariable->format(), Variable::Octal); + QCOMPARE(jVariable->value(), "01"); + + CONTINUE_AND_WAIT_FOR_PAUSED_STATE(session, sessionSpy); + QCOMPARE(currentMiLine(session), 24); + QCOMPARE(jVariable->showError(), false); + QCOMPARE(jVariable->inScope(), true); + QCOMPARE(jVariable->type(), "int"); + QCOMPARE(jVariable->format(), Variable::Octal); + QCOMPARE(jVariable->value(), "02"); + + jVariable->setFormat(Variable::Binary); + // Wait for the results of an MI command -var-set-format. + WAIT_FOR_STATE_AND_IDLE(session, IDebugSession::PausedState); + QCOMPARE(jVariable->showError(), false); + QCOMPARE(jVariable->inScope(), true); + QCOMPARE(jVariable->type(), "int"); + QCOMPARE(jVariable->format(), Variable::Binary); + QCOMPARE(jVariable->value(), adjustedVariableValueInBinaryFormat("10")); + + STEP_OUT_AND_WAIT_FOR_PAUSED_STATE(session, sessionSpy); + QCOMPARE(currentMiLine(session), 32); + // Back to main() where `j` is out of scope. The type and value of an out-of-scope variable remain unchanged. + QCOMPARE(jVariable->showError(), false); + if (isLldb()) { + // A workaround in LLDB::DebugSession::updateAllVariables() updates all variables manually. + // Consequently MIVariable::handleUpdate(), which updates Variable::inScope(), is not invoked regularly. + QEXPECT_FAIL("", "MIVariable::handleUpdate() is invoked only when the format of a variable changes", Continue); + } + QCOMPARE(jVariable->inScope(), false); + QCOMPARE(jVariable->type(), "int"); + QCOMPARE(jVariable->format(), Variable::Binary); + QCOMPARE(jVariable->value(), adjustedVariableValueInBinaryFormat("10")); + + jVariable->setFormat(Variable::Hexadecimal); + // Wait for the results of an MI command -var-set-format. + WAIT_FOR_STATE_AND_IDLE(session, IDebugSession::PausedState); + QCOMPARE(jVariable->showError(), false); + QCOMPARE(jVariable->inScope(), false); + QCOMPARE(jVariable->type(), "int"); + QCOMPARE(jVariable->format(), Variable::Hexadecimal); + if (isLldb()) { + QCOMPARE(jVariable->value(), "0x2"); + } else { + // GDB/MI sends value="", and SetFormatHandler assigns the empty string to Variable::value(). + QEXPECT_FAIL("", "Changing format of an out-of-scope variable clears its value", Continue); + QCOMPARE(jVariable->value(), "0x2"); + QCOMPARE(jVariable->value(), ""); + } + + session->run(); + WAIT_FOR_STATE(session, IDebugSession::EndedState); +} + void DebuggerTestBase::testVariablesChanged() { // kdevlldb xfails the test because of a workaround in LLDB::DebugSession::updateAllVariables() @@ -2186,6 +2276,14 @@ QString DebuggerTestBase::adjustedStackModelFrameName(QString frameName) const return frameName; } +QString DebuggerTestBase::adjustedVariableValueInBinaryFormat(QString binaryValue) const +{ + if (isLldb()) { + binaryValue.prepend("0b"); + } + return binaryValue; +} + bool DebuggerTestBase::isAcceptableExecutableFileKindForCore(FileKind executableFileKind) { return executableFileKind == FileKind::Valid || executableFileKind == FileKind::EmptyName; diff --git a/plugins/debuggercommon/tests/debuggertestbase.h b/plugins/debuggercommon/tests/debuggertestbase.h index d5204e0916a..18e94eaecc4 100644 --- a/plugins/debuggercommon/tests/debuggertestbase.h +++ b/plugins/debuggercommon/tests/debuggertestbase.h @@ -168,6 +168,7 @@ private Q_SLOTS: void testVariablesSameWatchInSecondSession(); void testVariablesSwitchFrame(); void testVariablesQuicklySwitchFrame(); + void testVariablesAttributes(); void testVariablesChanged(); void testSwitchFrameDebuggerConsole(); @@ -197,6 +198,7 @@ private Q_SLOTS: } [[nodiscard]] QString adjustedStackModelFrameName(QString frameName) const; + [[nodiscard]] QString adjustedVariableValueInBinaryFormat(QString binaryValue) const; enum class FileKind { Valid, diff --git a/plugins/lldb/controllers/variable.cpp b/plugins/lldb/controllers/variable.cpp index 5447f7bcfc8..94ce72db03b 100644 --- a/plugins/lldb/controllers/variable.cpp +++ b/plugins/lldb/controllers/variable.cpp @@ -17,6 +17,17 @@ using namespace KDevelop; using namespace KDevMI::LLDB; using namespace KDevMI::MI; +namespace { +/** + * @return the name of a field that must be present in the record argument of LldbVariable::handleRawUpdate() + */ +[[nodiscard]] QString rawUpdateField() +{ + return QStringLiteral("changelist"); +} + +} // unnamed namespace + LldbVariable::LldbVariable(DebugSession *session, TreeModel *model, TreeItem *parent, const QString& expression, const QString& display) : MIVariable(session, model, parent, expression, display) @@ -25,6 +36,7 @@ LldbVariable::LldbVariable(DebugSession *session, TreeModel *model, TreeItem *pa void LldbVariable::refetch() { + // NOTE: a comment in the function that calls this one describes bugs that are worked around here. if (!topLevel() || varobj().isEmpty()) { return; } @@ -34,10 +46,22 @@ void LldbVariable::refetch() } // update the value itself + // LLDB-MI does not support floating variable objects (https://github.com/lldb-tools/lldb-mi/issues/105). + // Therefore it always binds each variable object to the debuggee object, for which it was originally + // created. Consequently, if another debuggee object with a name that matches a variable object's + // expression is in scope at a subsequent debugger stop, the value of the variable object returned by + // -var-evaluate-expression remains equal to the value of the original, now out-of-scope, debuggee object. Also + // note that even if LLDB-MI starts returning the value of a matching debuggee object that is in scope, KDevelop + // would display an obsolete type of the variable, seeing as -var-evaluate-expression does not return the type. + // TODO: -var-create a new variable object to display the types and values of variables that are currently in scope. + // If the -var-create command succeeds, i.e. a matching debuggee object is in scope, compare the old and + // new values of attributes of the reattached variable, update its attribute isChanged() accordingly + // so as to match the behavior of kdevgdb, and -var-delete the replaced variable object to clean up. QPointer guarded_this(this); m_debugSession->addCommand(VarEvaluateExpression, varobj(), [guarded_this](const ResultRecord &r){ - if (guarded_this && r.reason == QLatin1String("done") && r.hasField(QStringLiteral("value"))) { - guarded_this->setValue(guarded_this->formatValue(r[QStringLiteral("value")].literal())); + auto* const variable = guarded_this.get(); + if (variable && r.reason == QLatin1String("done")) { + variable->setValueToOptionalValueFieldOf(r); } }); @@ -50,43 +74,34 @@ void LldbVariable::refetch() } } -void LldbVariable::handleRawUpdate(const ResultRecord& r) +const Value* LldbVariable::handleRawUpdate(const ResultRecord& r) { qCDebug(DEBUGGERLLDB) << "handleRawUpdate for variable" << varobj(); - const Value& changelist = r[QStringLiteral("changelist")]; + const auto& changelist = r[rawUpdateField()]; Q_ASSERT_X(changelist.size() <= 1, "LldbVariable::handleRawUpdate", "should only be used with one variable VarUpdate"); - if (changelist.size() == 1) - handleUpdate(changelist[0]); + if (changelist.empty()) { + return nullptr; + } + + const auto& changes = changelist[0]; + handleUpdate(changes); + return &changes; } -void LldbVariable::formatChanged() +std::function LldbVariable::handlerOfSetFormatCommand() { - if(childCount()) - { - for (TreeItem* item : std::as_const(childItems)) { - Q_ASSERT(qobject_cast(item)); - if (auto* var = qobject_cast(item)) { - var->setFormat(format()); + return [guarded_this = QPointer{this}](const ResultRecord& r) { + auto* const variable = guarded_this.get(); + if (variable && r.hasField(rawUpdateField())) { + const auto* const changes = variable->handleRawUpdate(r); + if (changes && !variable->inScope()) { + // MIVariable::handleUpdate() updates value() only if the variable is in scope. But + // LLDB-MI always sends the value in the current format. Thus update value() manually. + variable->setValueToOptionalValueFieldOf(*changes); } } - } - else - { - if (sessionIsAlive()) { - QPointer guarded_this(this); - m_debugSession->addCommand( - VarSetFormat, - QStringLiteral(" %1 %2 ").arg(varobj(), format2str(format())), - [guarded_this](const ResultRecord &r){ - if(guarded_this && r.hasField(QStringLiteral("changelist"))) { - if (r[QStringLiteral("changelist")].size() > 0) { - guarded_this->handleRawUpdate(r); - } - } - }); - } - } + }; } QString LldbVariable::formatValue(const QString& value) const diff --git a/plugins/lldb/controllers/variable.h b/plugins/lldb/controllers/variable.h index 6147b4e4aa9..988cb6b1481 100644 --- a/plugins/lldb/controllers/variable.h +++ b/plugins/lldb/controllers/variable.h @@ -24,8 +24,6 @@ class LldbVariable : public MIVariable LldbVariable(DebugSession *session, KDevelop::TreeModel* model, KDevelop::TreeItem* parent, const QString& expression, const QString& display = QString()); - void handleRawUpdate(const MI::ResultRecord &r); - void refetch(); using KDevelop::Variable::topLevel; @@ -35,8 +33,18 @@ class LldbVariable : public MIVariable using KDevelop::Variable::child; protected: - void formatChanged() override; QString formatValue(const QString &value) const override; + +private: + /** + * Update this variable based on a given update record. + * + * @param r a record that contains a list field "changelist" of size 0 or 1 + * @return a pointer to a single element of the changelist or @c nullptr if the changelist is empty + */ + const MI::Value* handleRawUpdate(const MI::ResultRecord& r); + + [[nodiscard]] std::function handlerOfSetFormatCommand() override; }; } // end of namespace LLDB diff --git a/plugins/lldb/debugsession.cpp b/plugins/lldb/debugsession.cpp index 085ca35f5aa..24198902b37 100644 --- a/plugins/lldb/debugsession.cpp +++ b/plugins/lldb/debugsession.cpp @@ -361,9 +361,19 @@ void DebugSession::handleVersion(const QStringList& s) void DebugSession::updateAllVariables() { - // FIXME: this is only a workaround for lldb-mi doesn't provide -var-update changelist - // for variables that have a python synthetic provider. Remove this after this is fixed - // in the upstream. + // This function together with its caller and callees works around the following bugs: + // 1. LLDB-MI does not support `*` as the variable object names argument, for example: + // (lldb) 54-var-update --thread 1 --frame 0 --all-values * + // 54^error,msg="Command 'var-update'. Variable '*' does not exist" + // 2. LLDB-MI sends an empty -var-update changelist for variables that have a Python synthetic provider. + // 3. LLDB-MI does not support floating variable objects (https://github.com/lldb-tools/lldb-mi/issues/105). + // Therefore it always binds each variable object to the debuggee object, for which it was originally created. + // Consequently, if another debuggee object with a name that matches a variable object's expression is in scope + // at a subsequent debugger stop, the changelist reported for the variable object is empty. For example: + // (lldb) 107-var-update --thread 3 --frame 0 --all-values var9 + // 107^done,changelist=[] + // TODO: remove this workaround after the bugs are fixed upstream. If only the first + // bug remains unfixed, revert 91d7176c7a10cdb8eb65e8aa20ea0cd3816ed8e7. // re-fetch all toplevel variables, as -var-update doesn't work with data formatter // we have to pick out top level variables first, as refetching will delete child