[lldb] Expose structured command diagnostics via the SBAPI. (#112109)
This allows IDEs to render LLDB expression diagnostics to their liking without relying on characterprecise ASCII art from LLDB. It is exposed as a versioned SBStructuredData object, since it is expected that this may need to be tweaked based on actual usage.
This commit is contained in:
@@ -45,6 +45,7 @@ public:
|
||||
const char *GetOutput();
|
||||
|
||||
const char *GetError();
|
||||
SBStructuredData GetErrorData();
|
||||
|
||||
#ifndef SWIG
|
||||
LLDB_DEPRECATED_FIXME("Use PutOutput(SBFile) or PutOutput(FileSP)",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef LLDB_API_SBSTRUCTUREDDATA_H
|
||||
#define LLDB_API_SBSTRUCTUREDDATA_H
|
||||
|
||||
#include "lldb/API/SBCommandReturnObject.h"
|
||||
#include "lldb/API/SBDefines.h"
|
||||
#include "lldb/API/SBModule.h"
|
||||
#include "lldb/API/SBScriptObject.h"
|
||||
@@ -110,6 +111,7 @@ public:
|
||||
|
||||
protected:
|
||||
friend class SBAttachInfo;
|
||||
friend class SBCommandReturnObject;
|
||||
friend class SBLaunchInfo;
|
||||
friend class SBDebugger;
|
||||
friend class SBTarget;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "lldb/Utility/DiagnosticsRendering.h"
|
||||
#include "lldb/Utility/StreamString.h"
|
||||
#include "lldb/Utility/StreamTee.h"
|
||||
#include "lldb/Utility/StructuredData.h"
|
||||
#include "lldb/lldb-private.h"
|
||||
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
@@ -31,7 +32,7 @@ public:
|
||||
~CommandReturnObject() = default;
|
||||
|
||||
/// Format any inline diagnostics with an indentation of \c indent.
|
||||
llvm::StringRef GetInlineDiagnosticString(unsigned indent);
|
||||
std::string GetInlineDiagnosticString(unsigned indent);
|
||||
|
||||
llvm::StringRef GetOutputString() {
|
||||
lldb::StreamSP stream_sp(m_out_stream.GetStreamAtIndex(eStreamStringIndex));
|
||||
@@ -40,7 +41,13 @@ public:
|
||||
return llvm::StringRef();
|
||||
}
|
||||
|
||||
llvm::StringRef GetErrorString();
|
||||
/// Return the errors as a string.
|
||||
///
|
||||
/// If \c with_diagnostics is true, all diagnostics are also
|
||||
/// rendered into the string. Otherwise the expectation is that they
|
||||
/// are fetched with \ref GetInlineDiagnosticString().
|
||||
std::string GetErrorString(bool with_diagnostics = true);
|
||||
StructuredData::ObjectSP GetErrorData();
|
||||
|
||||
Stream &GetOutputStream() {
|
||||
// Make sure we at least have our normal string stream output stream
|
||||
@@ -168,7 +175,6 @@ private:
|
||||
StreamTee m_out_stream;
|
||||
StreamTee m_err_stream;
|
||||
std::vector<DiagnosticDetail> m_diagnostics;
|
||||
StreamString m_diag_stream;
|
||||
std::optional<uint16_t> m_diagnostic_indent;
|
||||
|
||||
lldb::ReturnStatus m_status = lldb::eReturnStatusStarted;
|
||||
@@ -178,6 +184,7 @@ private:
|
||||
|
||||
/// If true, then the input handle from the debugger will be hooked up.
|
||||
bool m_interactive = true;
|
||||
bool m_colors;
|
||||
};
|
||||
|
||||
} // namespace lldb_private
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "lldb/API/SBError.h"
|
||||
#include "lldb/API/SBFile.h"
|
||||
#include "lldb/API/SBStream.h"
|
||||
#include "lldb/API/SBStructuredData.h"
|
||||
#include "lldb/Core/StructuredDataImpl.h"
|
||||
#include "lldb/Interpreter/CommandReturnObject.h"
|
||||
#include "lldb/Utility/ConstString.h"
|
||||
#include "lldb/Utility/Instrumentation.h"
|
||||
@@ -96,6 +98,15 @@ const char *SBCommandReturnObject::GetError() {
|
||||
return output.AsCString(/*value_if_empty*/ "");
|
||||
}
|
||||
|
||||
SBStructuredData SBCommandReturnObject::GetErrorData() {
|
||||
LLDB_INSTRUMENT_VA(this);
|
||||
|
||||
StructuredData::ObjectSP data(ref().GetErrorData());
|
||||
SBStructuredData sb_data;
|
||||
sb_data.m_impl_up->SetObjectSP(data);
|
||||
return sb_data;
|
||||
}
|
||||
|
||||
size_t SBCommandReturnObject::GetOutputSize() {
|
||||
LLDB_INSTRUMENT_VA(this);
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ void CommandObjectDWIMPrint::DoExecute(StringRef command,
|
||||
// Record the position of the expression in the command.
|
||||
std::optional<uint16_t> indent;
|
||||
if (fixed_expression.empty()) {
|
||||
size_t pos = m_original_command.find(expr);
|
||||
size_t pos = m_original_command.rfind(expr);
|
||||
if (pos != llvm::StringRef::npos)
|
||||
indent = pos;
|
||||
}
|
||||
|
||||
@@ -485,35 +485,8 @@ bool CommandObjectExpression::EvaluateExpression(llvm::StringRef expr,
|
||||
|
||||
result.SetStatus(eReturnStatusSuccessFinishResult);
|
||||
} else {
|
||||
// Retrieve the diagnostics.
|
||||
std::vector<DiagnosticDetail> details;
|
||||
llvm::consumeError(llvm::handleErrors(
|
||||
result_valobj_sp->GetError().ToError(),
|
||||
[&](DiagnosticError &error) { details = error.GetDetails(); }));
|
||||
// Find the position of the expression in the command.
|
||||
std::optional<uint16_t> expr_pos;
|
||||
size_t nchar = m_original_command.find(expr);
|
||||
if (nchar != std::string::npos)
|
||||
expr_pos = nchar + GetDebugger().GetPrompt().size();
|
||||
|
||||
if (!details.empty()) {
|
||||
bool show_inline =
|
||||
GetDebugger().GetShowInlineDiagnostics() && !expr.contains('\n');
|
||||
RenderDiagnosticDetails(error_stream, expr_pos, show_inline, details);
|
||||
} else {
|
||||
const char *error_cstr = result_valobj_sp->GetError().AsCString();
|
||||
llvm::StringRef error(error_cstr);
|
||||
if (!error.empty()) {
|
||||
if (!error.starts_with("error:"))
|
||||
error_stream << "error: ";
|
||||
error_stream << error;
|
||||
if (!error.ends_with('\n'))
|
||||
error_stream.EOL();
|
||||
} else {
|
||||
error_stream << "error: unknown error\n";
|
||||
}
|
||||
}
|
||||
result.SetStatus(eReturnStatusFailed);
|
||||
result.SetError(result_valobj_sp->GetError().ToError());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -533,10 +506,13 @@ void CommandObjectExpression::IOHandlerInputComplete(IOHandler &io_handler,
|
||||
CommandReturnObject return_obj(
|
||||
GetCommandInterpreter().GetDebugger().GetUseColor());
|
||||
EvaluateExpression(line.c_str(), *output_sp, *error_sp, return_obj);
|
||||
|
||||
if (output_sp)
|
||||
output_sp->Flush();
|
||||
if (error_sp)
|
||||
if (error_sp) {
|
||||
*error_sp << return_obj.GetErrorString();
|
||||
error_sp->Flush();
|
||||
}
|
||||
}
|
||||
|
||||
bool CommandObjectExpression::IOHandlerIsInputComplete(IOHandler &io_handler,
|
||||
@@ -679,6 +655,14 @@ void CommandObjectExpression::DoExecute(llvm::StringRef command,
|
||||
}
|
||||
}
|
||||
|
||||
// Previously the indent was set up for diagnosing command line
|
||||
// parsing errors. Now point it to the expression.
|
||||
std::optional<uint16_t> indent;
|
||||
size_t pos = m_original_command.rfind(expr);
|
||||
if (pos != llvm::StringRef::npos)
|
||||
indent = pos;
|
||||
result.SetDiagnosticIndent(indent);
|
||||
|
||||
Target &target = GetTarget();
|
||||
if (EvaluateExpression(expr, result.GetOutputStream(),
|
||||
result.GetErrorStream(), result)) {
|
||||
|
||||
@@ -2636,20 +2636,18 @@ void CommandInterpreter::HandleCommands(const StringList &commands,
|
||||
}
|
||||
|
||||
if (!success || !tmp_result.Succeeded()) {
|
||||
llvm::StringRef error_msg = tmp_result.GetErrorString();
|
||||
std::string error_msg = tmp_result.GetErrorString();
|
||||
if (error_msg.empty())
|
||||
error_msg = "<unknown error>.\n";
|
||||
if (options.GetStopOnError()) {
|
||||
result.AppendErrorWithFormat(
|
||||
"Aborting reading of commands after command #%" PRIu64
|
||||
": '%s' failed with %s",
|
||||
(uint64_t)idx, cmd, error_msg.str().c_str());
|
||||
result.AppendErrorWithFormatv("Aborting reading of commands after "
|
||||
"command #{0}: '{1}' failed with {2}",
|
||||
(uint64_t)idx, cmd, error_msg);
|
||||
m_debugger.SetAsyncExecution(old_async_execution);
|
||||
return;
|
||||
} else if (options.GetPrintResults()) {
|
||||
result.AppendMessageWithFormat(
|
||||
"Command #%" PRIu64 " '%s' failed with %s", (uint64_t)idx + 1, cmd,
|
||||
error_msg.str().c_str());
|
||||
result.AppendMessageWithFormatv("Command #{0} '{1}' failed with {2}",
|
||||
(uint64_t)idx + 1, cmd, error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3187,11 +3185,12 @@ void CommandInterpreter::IOHandlerInputComplete(IOHandler &io_handler,
|
||||
io_handler.GetFlags().Test(eHandleCommandFlagPrintResult)) ||
|
||||
io_handler.GetFlags().Test(eHandleCommandFlagPrintErrors)) {
|
||||
// Display any inline diagnostics first.
|
||||
if (!result.GetImmediateErrorStream() &&
|
||||
GetDebugger().GetShowInlineDiagnostics()) {
|
||||
const bool inline_diagnostics = !result.GetImmediateErrorStream() &&
|
||||
GetDebugger().GetShowInlineDiagnostics();
|
||||
if (inline_diagnostics) {
|
||||
unsigned prompt_len = m_debugger.GetPrompt().size();
|
||||
if (auto indent = result.GetDiagnosticIndent()) {
|
||||
llvm::StringRef diags =
|
||||
std::string diags =
|
||||
result.GetInlineDiagnosticString(prompt_len + *indent);
|
||||
PrintCommandOutput(io_handler, diags, true);
|
||||
}
|
||||
@@ -3207,7 +3206,7 @@ void CommandInterpreter::IOHandlerInputComplete(IOHandler &io_handler,
|
||||
|
||||
// Now emit the command error text from the command we just executed.
|
||||
if (!result.GetImmediateErrorStream()) {
|
||||
llvm::StringRef error = result.GetErrorString();
|
||||
std::string error = result.GetErrorString(!inline_diagnostics);
|
||||
PrintCommandOutput(io_handler, error, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ static void DumpStringToStreamWithNewline(Stream &strm, const std::string &s) {
|
||||
}
|
||||
|
||||
CommandReturnObject::CommandReturnObject(bool colors)
|
||||
: m_out_stream(colors), m_err_stream(colors), m_diag_stream(colors) {}
|
||||
: m_out_stream(colors), m_err_stream(colors), m_colors(colors) {}
|
||||
|
||||
void CommandReturnObject::AppendErrorWithFormat(const char *format, ...) {
|
||||
SetStatus(eReturnStatusFailed);
|
||||
@@ -123,30 +123,79 @@ void CommandReturnObject::SetError(llvm::Error error) {
|
||||
}
|
||||
}
|
||||
|
||||
llvm::StringRef
|
||||
CommandReturnObject::GetInlineDiagnosticString(unsigned indent) {
|
||||
RenderDiagnosticDetails(m_diag_stream, indent, true, m_diagnostics);
|
||||
std::string CommandReturnObject::GetInlineDiagnosticString(unsigned indent) {
|
||||
StreamString diag_stream(m_colors);
|
||||
RenderDiagnosticDetails(diag_stream, indent, true, m_diagnostics);
|
||||
// Duplex the diagnostics to the secondary stream (but not inlined).
|
||||
if (auto stream_sp = m_err_stream.GetStreamAtIndex(eStreamStringIndex))
|
||||
if (auto stream_sp = m_err_stream.GetStreamAtIndex(eImmediateStreamIndex))
|
||||
RenderDiagnosticDetails(*stream_sp, std::nullopt, false, m_diagnostics);
|
||||
|
||||
// Clear them so GetErrorData() doesn't render them again.
|
||||
m_diagnostics.clear();
|
||||
return m_diag_stream.GetString();
|
||||
return diag_stream.GetString().str();
|
||||
}
|
||||
|
||||
llvm::StringRef CommandReturnObject::GetErrorString() {
|
||||
// Diagnostics haven't been fetched; render them now (not inlined).
|
||||
if (!m_diagnostics.empty()) {
|
||||
RenderDiagnosticDetails(GetErrorStream(), std::nullopt, false,
|
||||
m_diagnostics);
|
||||
m_diagnostics.clear();
|
||||
}
|
||||
std::string CommandReturnObject::GetErrorString(bool with_diagnostics) {
|
||||
StreamString stream(m_colors);
|
||||
if (with_diagnostics)
|
||||
RenderDiagnosticDetails(stream, std::nullopt, false, m_diagnostics);
|
||||
|
||||
lldb::StreamSP stream_sp(m_err_stream.GetStreamAtIndex(eStreamStringIndex));
|
||||
if (stream_sp)
|
||||
return std::static_pointer_cast<StreamString>(stream_sp)->GetString();
|
||||
return llvm::StringRef();
|
||||
stream << std::static_pointer_cast<StreamString>(stream_sp)->GetString();
|
||||
return stream.GetString().str();
|
||||
}
|
||||
|
||||
StructuredData::ObjectSP CommandReturnObject::GetErrorData() {
|
||||
auto make_array = []() { return std::make_unique<StructuredData::Array>(); };
|
||||
auto make_bool = [](bool b) {
|
||||
return std::make_unique<StructuredData::Boolean>(b);
|
||||
};
|
||||
auto make_dict = []() {
|
||||
return std::make_unique<StructuredData::Dictionary>();
|
||||
};
|
||||
auto make_int = [](unsigned i) {
|
||||
return std::make_unique<StructuredData::UnsignedInteger>(i);
|
||||
};
|
||||
auto make_string = [](llvm::StringRef s) {
|
||||
return std::make_unique<StructuredData::String>(s);
|
||||
};
|
||||
auto dict_up = make_dict();
|
||||
dict_up->AddItem("version", make_int(1));
|
||||
auto array_up = make_array();
|
||||
for (const DiagnosticDetail &diag : m_diagnostics) {
|
||||
auto detail_up = make_dict();
|
||||
if (auto &sloc = diag.source_location) {
|
||||
auto sloc_up = make_dict();
|
||||
sloc_up->AddItem("file", make_string(sloc->file.GetPath()));
|
||||
sloc_up->AddItem("line", make_int(sloc->line));
|
||||
sloc_up->AddItem("length", make_int(sloc->length));
|
||||
sloc_up->AddItem("hidden", make_bool(sloc->hidden));
|
||||
sloc_up->AddItem("in_user_input", make_bool(sloc->in_user_input));
|
||||
detail_up->AddItem("source_location", std::move(sloc_up));
|
||||
}
|
||||
llvm::StringRef severity = "unknown";
|
||||
switch (diag.severity) {
|
||||
case lldb::eSeverityError:
|
||||
severity = "error";
|
||||
break;
|
||||
case lldb::eSeverityWarning:
|
||||
severity = "warning";
|
||||
break;
|
||||
case lldb::eSeverityInfo:
|
||||
severity = "note";
|
||||
break;
|
||||
}
|
||||
detail_up->AddItem("severity", make_string(severity));
|
||||
detail_up->AddItem("message", make_string(diag.message));
|
||||
detail_up->AddItem("rendered", make_string(diag.rendered));
|
||||
array_up->AddItem(std::move(detail_up));
|
||||
}
|
||||
dict_up->AddItem("details", std::move(array_up));
|
||||
if (auto stream_sp = m_err_stream.GetStreamAtIndex(eStreamStringIndex)) {
|
||||
auto text = std::static_pointer_cast<StreamString>(stream_sp)->GetString();
|
||||
if (!text.empty())
|
||||
dict_up->AddItem("text", make_string(text));
|
||||
}
|
||||
return dict_up;
|
||||
}
|
||||
|
||||
// Similar to AppendError, but do not prepend 'Status: ' to message, and don't
|
||||
@@ -179,6 +228,7 @@ void CommandReturnObject::Clear() {
|
||||
stream_sp = m_err_stream.GetStreamAtIndex(eStreamStringIndex);
|
||||
if (stream_sp)
|
||||
static_cast<StreamString *>(stream_sp.get())->Clear();
|
||||
m_diagnostics.clear();
|
||||
m_status = eReturnStatusStarted;
|
||||
m_did_change_process_state = false;
|
||||
m_suppress_immediate_output = false;
|
||||
|
||||
@@ -184,53 +184,44 @@ note: candidate function not viable: requires single argument 'x', but 2 argumen
|
||||
# the first argument are probably stable enough that this test can check for them.
|
||||
self.assertIn("void NSLog(NSString *format", value.GetError().GetCString())
|
||||
|
||||
def test_command_expr_formatting(self):
|
||||
"""Test that the source and caret positions LLDB prints are correct"""
|
||||
def test_command_expr_sbdata(self):
|
||||
"""Test the structured diagnostics data"""
|
||||
self.build()
|
||||
|
||||
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
|
||||
self, "// Break here", self.main_source_spec
|
||||
)
|
||||
frame = thread.GetFrameAtIndex(0)
|
||||
self.expect("settings set show-inline-diagnostics true")
|
||||
interp = self.dbg.GetCommandInterpreter()
|
||||
cro = lldb.SBCommandReturnObject()
|
||||
interp.HandleCommand("expression -- a+b", cro)
|
||||
|
||||
def check(input_ref):
|
||||
self.expect(input_ref[0], error=True, substrs=input_ref[1:])
|
||||
diags = cro.GetErrorData()
|
||||
# Version.
|
||||
version = diags.GetValueForKey("version")
|
||||
self.assertEqual(version.GetIntegerValue(), 1)
|
||||
|
||||
check(
|
||||
[
|
||||
"expression -- a+b",
|
||||
" ^ ^",
|
||||
" | error: use of undeclared identifier 'b'",
|
||||
" error: use of undeclared identifier 'a'",
|
||||
]
|
||||
)
|
||||
details = diags.GetValueForKey("details")
|
||||
|
||||
check(
|
||||
[
|
||||
"expr -- a",
|
||||
" ^",
|
||||
" error: use of undeclared identifier 'a'",
|
||||
]
|
||||
)
|
||||
check(
|
||||
[
|
||||
"expr -i 0 -o 0 -- a",
|
||||
" ^",
|
||||
" error: use of undeclared identifier 'a'",
|
||||
]
|
||||
)
|
||||
# Detail 1/2: undeclared 'a'
|
||||
diag = details.GetItemAtIndex(0)
|
||||
|
||||
self.expect(
|
||||
"expression --top-level -- template<typename T> T FOO(T x) { return x/2;}"
|
||||
)
|
||||
check(
|
||||
[
|
||||
'expression -- FOO("")',
|
||||
" ^",
|
||||
" note: in instantiation of function template specialization 'FOO<const char *>' requested here",
|
||||
"error: <user expression",
|
||||
"invalid operands to binary expression",
|
||||
]
|
||||
)
|
||||
check(["expression --\na\n+\nb", "error: <user", "a", "error: <user", "b"])
|
||||
severity = diag.GetValueForKey("severity")
|
||||
message = diag.GetValueForKey("message")
|
||||
rendered = diag.GetValueForKey("rendered")
|
||||
sloc = diag.GetValueForKey("source_location")
|
||||
filename = sloc.GetValueForKey("file")
|
||||
hidden = sloc.GetValueForKey("hidden")
|
||||
in_user_input = sloc.GetValueForKey("in_user_input")
|
||||
|
||||
self.assertEqual(str(severity), "error")
|
||||
self.assertIn("undeclared identifier 'a'", str(message))
|
||||
# The rendered string should contain the source file.
|
||||
self.assertIn("user expression", str(rendered))
|
||||
self.assertIn("user expression", str(filename))
|
||||
self.assertFalse(hidden.GetBooleanValue())
|
||||
self.assertTrue(in_user_input.GetBooleanValue())
|
||||
|
||||
# Detail 1/2: undeclared 'b'
|
||||
diag = details.GetItemAtIndex(1)
|
||||
message = diag.GetValueForKey("message")
|
||||
self.assertIn("undeclared identifier 'b'", str(message))
|
||||
|
||||
6
lldb/test/Shell/Commands/Inputs/multiline-expr.txt
Normal file
6
lldb/test/Shell/Commands/Inputs/multiline-expr.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
expression --
|
||||
a
|
||||
+
|
||||
b
|
||||
|
||||
quit
|
||||
32
lldb/test/Shell/Commands/command-expr-diagnostics.test
Normal file
32
lldb/test/Shell/Commands/command-expr-diagnostics.test
Normal file
@@ -0,0 +1,32 @@
|
||||
# RUN: echo quit | %lldb -o "expression a+b" \
|
||||
# RUN: | FileCheck %s --strict-whitespace --check-prefix=CHECK1
|
||||
# (lldb) expression a+b
|
||||
# CHECK1:{{^ \^ \^}}
|
||||
# CHECK1: {{^ | error: use of undeclared identifier 'b'}}
|
||||
# CHECK1: {{^ error: use of undeclared identifier 'a'}}
|
||||
|
||||
# RUN: echo quit | %lldb -o "expr a" \
|
||||
# RUN: | FileCheck %s --strict-whitespace --check-prefix=CHECK2
|
||||
# (lldb) expr a
|
||||
# CHECK2:{{^ \^}}
|
||||
|
||||
# RUN: echo quit | %lldb -o "expr -i 0 -o 0 -- a" \
|
||||
# RUN: | FileCheck %s --strict-whitespace --check-prefix=CHECK3
|
||||
# (lldb) expr -i 0 -o 0 -- a
|
||||
# CHECK3:{{^ \^}}
|
||||
# CHECK3: {{^ error: use of undeclared identifier 'a'}}
|
||||
|
||||
# RUN: echo "int main(){return 0;}">%t.c
|
||||
# RUN: %clang_host %t.c -o %t.exe
|
||||
# RUN: echo quit | %lldb %t.exe -o "b main" -o r -o \
|
||||
# RUN: "expr --top-level -- template<typename T> T FOO(T x) { return x/2;}" -o \
|
||||
# RUN: "expression -- FOO(\"\")" 2>&1 | FileCheck %s --check-prefix=CHECK4
|
||||
# (lldb) expression -- FOO("")
|
||||
# CHECK4:{{^ \^}}
|
||||
# CHECK4: {{^ note: in instantiation of function template}}
|
||||
# CHECK4: error: <user expression
|
||||
|
||||
# RUN: echo expression --\na\n+\nb
|
||||
# RUN: cat %S/Inputs/multiline-expr.txt | %lldb 2>&1 | FileCheck %s --strict-whitespace --check-prefix=CHECK5
|
||||
# CHECK5: error: <user{{.*}}a
|
||||
# CHECK5: error: <user{{.*}}b
|
||||
Reference in New Issue
Block a user