https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/209400
>From 68ff8c0eb55aca6fe722fc73971e37ede2106c4a Mon Sep 17 00:00:00 2001 From: Med Ismail Bennani <[email protected]> Date: Tue, 14 Jul 2026 10:47:59 -0700 Subject: [PATCH] [lldb/script] Improve `scripting extension list` output and filtering This patch improves `scripting extension list` in three ways. First, it groups the output by `ScriptedExtension`: instead of one row per registered plugin instance, one entry per extension is printed with a combined `Language` field. Second, it colorizes and visually separates the output. Each entry is preceded by a dimmed dashed separator; field labels are printed in bold green, the extension name value in bold cyan as a mini-heading, and `None` usage values are dimmed, all via the same `ansi::FormatAnsiTerminalCodes(..., use_color)` idiom `Breakpoint::GetDescription` uses elsewhere, gracefully no-op when color is disabled or unsupported. `ScriptedInterfaceUsages::Dump` takes an optional `use_color` parameter so its own `API Usages:` / `Command Interpreter Usages:` labels can match. Third, it adds `-j`/`--json` to emit a JSON array of `{name, description, languages, api_usages, command_interpreter_usages}` per extension, mirroring `plugin list`'s existing `-j`. `DoExecute` now computes the extension grouping once and branches into `OutputJsonFormat` or `OutputTextFormat`, sharing a `GetLanguagesForExtension` helper. It also accepts optional positional `<extension-name>` arguments to filter the listing to specific extensions (matched via the same case-insensitive `ScriptInterpreter::StringToExtension` used by `scripting extension generate`), with completion wired up via `eScriptedExtensionCompletion`. Groups whose plugin instances don't match the requested `-l` language are dropped from the output entirely, so `scripting extension list -l lua` prints `Available scripted extension templates: None` (and the JSON path returns `[]`) rather than every group with an empty `Language:` field. Also fix a latent lifetime bug in the shared language-collection helper so it owns its `std::string` values instead of holding `StringRef`s to temporaries; the JSON path made it observable. Add `TestScriptingExtensionListJSON.py`, which drives `scripting extension list -j` (bare, filtered, and language-restricted) through `SBCommandInterpreter` and parses the output back through `SBStructuredData::SetFromJSON` to verify the array/object shape and the presence of a known extension. Signed-off-by: Med Ismail Bennani <[email protected]> --- lldb/include/lldb/Core/PluginManager.h | 3 + .../lldb/Interpreter/CommandCompletions.h | 4 + .../Interpreter/CommandOptionArgumentTable.h | 1 + .../Interfaces/ScriptedInterfaceUsages.h | 2 +- .../lldb/Interpreter/ScriptInterpreter.h | 5 + lldb/include/lldb/lldb-enumerations.h | 4 +- lldb/source/Commands/CommandCompletions.cpp | 9 + .../Commands/CommandObjectScripting.cpp | 192 +++++++++++++++--- lldb/source/Commands/Options.td | 3 + lldb/source/Core/PluginManager.cpp | 13 ++ .../Interfaces/ScriptedInterfaceUsages.cpp | 16 +- lldb/source/Interpreter/ScriptInterpreter.cpp | 45 ++++ .../TestScriptingExtensionListJSON.py | 103 ++++++++++ .../command-scripting-extension-list.test | 101 ++++----- 14 files changed, 420 insertions(+), 81 deletions(-) create mode 100644 lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h index 9d36a9361dbc4..230a025c2f9e1 100644 --- a/lldb/include/lldb/Core/PluginManager.h +++ b/lldb/include/lldb/Core/PluginManager.h @@ -687,6 +687,9 @@ class PluginManager { static ScriptedInterfaceUsages GetScriptedInterfaceUsagesAtIndex(uint32_t idx); + static void AutoCompleteScriptedExtension(llvm::StringRef partial_name, + CompletionRequest &request); + // REPL static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description, REPLCreateInstance create_callback, diff --git a/lldb/include/lldb/Interpreter/CommandCompletions.h b/lldb/include/lldb/Interpreter/CommandCompletions.h index 0c0424cbac6eb..55de91e294e5d 100644 --- a/lldb/include/lldb/Interpreter/CommandCompletions.h +++ b/lldb/include/lldb/Interpreter/CommandCompletions.h @@ -127,6 +127,10 @@ class CommandCompletions { CompletionRequest &request, SearchFilter *searcher); + static void ScriptedExtensions(CommandInterpreter &interpreter, + CompletionRequest &request, + SearchFilter *searcher); + /// This completer works for commands whose only arguments are a command path. /// It isn't tied to an argument type because it completes not on a single /// argument but on the sequence of arguments, so you have to invoke it by diff --git a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h index abb056d9d51df..7075a7acb9733 100644 --- a/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h +++ b/lldb/include/lldb/Interpreter/CommandOptionArgumentTable.h @@ -364,6 +364,7 @@ static constexpr CommandObject::ArgumentTableEntry g_argument_table[] = { { lldb::eArgTypeNameMatchStyle, "match-style", lldb::CompletionType::eNoCompletion, g_name_match_style, { nullptr, false }, "Specify the kind of match to use when looking up names." }, { lldb::eArgTypePluginDomain, "plugin-domain", lldb::CompletionType::eNoCompletion, g_plugin_domain_values, { nullptr, false }, "The domain to apply the plugin operation to." }, { lldb::eArgTypeBreakpointResolverMask, "resolver-mask", lldb::CompletionType::eNoCompletion, g_resolver_mask_values, { nullptr, false }, "Specify the breakpoint resolver type your override will handle. Can be specified more than once to specify a mask of resolver types." }, + { lldb::eArgTypeScriptedExtension, "scripting-extension", lldb::CompletionType::eScriptedExtensionCompletion, {}, { nullptr, false }, "The name of a scripting extension." }, // clang-format on }; diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h index d958e782d86bd..48f38eb70bdb3 100644 --- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h +++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h @@ -32,7 +32,7 @@ class ScriptedInterfaceUsages { enum class UsageKind { CommandInterpreter, API }; - void Dump(Stream &s, UsageKind kind) const; + void Dump(Stream &s, UsageKind kind, bool use_color = false) const; private: std::vector<llvm::StringRef> m_command_interpreter_usages; diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h index 7ab098f177a80..2b9cb8f7bb866 100644 --- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h +++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h @@ -526,6 +526,11 @@ class ScriptInterpreter : public PluginInterface { static lldb::ScriptLanguage StringToLanguage(const llvm::StringRef &string); + static llvm::StringLiteral + ExtensionToString(lldb::ScriptedExtension extension); + + static lldb::ScriptedExtension StringToExtension(llvm::StringRef string); + lldb::ScriptLanguage GetLanguage() { return m_script_lang; } virtual lldb::ScriptedProcessInterfaceUP CreateScriptedProcessInterface() { diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h index d21081ff754e5..b36ad09f2e892 100644 --- a/lldb/include/lldb/lldb-enumerations.h +++ b/lldb/include/lldb/lldb-enumerations.h @@ -797,6 +797,7 @@ enum CommandArgumentType { eArgTypeNameMatchStyle, eArgTypePluginDomain, eArgTypeBreakpointResolverMask, + eArgTypeScriptedExtension, eArgTypeLastArg // Always keep this entry as the last entry in this // enumeration!! }; @@ -1477,10 +1478,11 @@ enum CompletionType { eCustomCompletion = (1ul << 25), eThreadIDCompletion = (1ul << 26), eManagedPluginCompletion = (1ul << 27), + eScriptedExtensionCompletion = (1ul << 28), // This last enum element is just for input validation. // Add new completions before this element, // and then increment eTerminatorCompletion's shift value - eTerminatorCompletion = (1ul << 28) + eTerminatorCompletion = (1ul << 29) }; /// Specifies if children need to be re-computed after a call to \ref diff --git a/lldb/source/Commands/CommandCompletions.cpp b/lldb/source/Commands/CommandCompletions.cpp index 9804e579a252d..18ee8f1a12fc0 100644 --- a/lldb/source/Commands/CommandCompletions.cpp +++ b/lldb/source/Commands/CommandCompletions.cpp @@ -89,6 +89,8 @@ bool CommandCompletions::InvokeCommonCompletionCallbacks( CommandCompletions::TypeCategoryNames}, {lldb::eThreadIDCompletion, CommandCompletions::ThreadIDs}, {lldb::eManagedPluginCompletion, CommandCompletions::ManagedPlugins}, + {lldb::eScriptedExtensionCompletion, + CommandCompletions::ScriptedExtensions}, {lldb::eTerminatorCompletion, nullptr} // This one has to be last in the list. }; @@ -869,6 +871,13 @@ void CommandCompletions::ManagedPlugins(CommandInterpreter &interpreter, request); } +void CommandCompletions::ScriptedExtensions(CommandInterpreter &interpreter, + CompletionRequest &request, + SearchFilter *searcher) { + PluginManager::AutoCompleteScriptedExtension( + request.GetCursorArgumentPrefix(), request); +} + void CommandCompletions::CompleteModifiableCmdPathArgs( CommandInterpreter &interpreter, CompletionRequest &request, OptionElementVector &opt_element_vector) { diff --git a/lldb/source/Commands/CommandObjectScripting.cpp b/lldb/source/Commands/CommandObjectScripting.cpp index 21400a62d697f..ff02169df1172 100644 --- a/lldb/source/Commands/CommandObjectScripting.cpp +++ b/lldb/source/Commands/CommandObjectScripting.cpp @@ -18,8 +18,11 @@ #include "lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h" #include "lldb/Interpreter/OptionArgParser.h" #include "lldb/Interpreter/ScriptInterpreter.h" +#include "lldb/Utility/AnsiTerminal.h" #include "lldb/Utility/Args.h" +#include "llvm/ADT/StringMap.h" + using namespace lldb; using namespace lldb_private; @@ -138,12 +141,23 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed { : CommandObjectParsed( interpreter, "scripting extension list", "List all the available scripting extension templates. ", - "scripting template list [--language <scripting-language> --]") {} + "scripting extension list [--language <scripting-language> --] " + "[--json --] [<extension-name> ...]") { + AddSimpleArgumentList(eArgTypeScriptedExtension, eArgRepeatStar); + } ~CommandObjectScriptingExtensionList() override = default; Options *GetOptions() override { return &m_options; } + void + HandleArgumentCompletion(CompletionRequest &request, + OptionElementVector &opt_element_vector) override { + lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks( + GetCommandInterpreter(), lldb::eScriptedExtensionCompletion, request, + nullptr); + } + class CommandOptions : public Options { public: CommandOptions() = default; @@ -162,6 +176,9 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed { error = Status::FromErrorStringWithFormatv( "unrecognized value for language '{0}'", option_arg); break; + case 'j': + m_json_format = true; + break; default: llvm_unreachable("Unimplemented option"); } @@ -171,6 +188,7 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed { void OptionParsingStarting(ExecutionContext *execution_context) override { m_language = lldb::eScriptLanguageDefault; + m_json_format = false; } llvm::ArrayRef<OptionDefinition> GetDefinitions() override { @@ -178,53 +196,169 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed { } lldb::ScriptLanguage m_language = lldb::eScriptLanguageDefault; + bool m_json_format = false; }; protected: void DoExecute(Args &command, CommandReturnObject &result) override { - Stream &s = result.GetOutputStream(); - s.Printf("Available scripted extension templates:"); + llvm::StringMap<std::vector<size_t>> grouped_by_extension; + for (size_t i = 0; i < PluginManager::GetNumScriptedInterfaces(); i++) { + lldb::ScriptedExtension extension = + PluginManager::GetScriptedInterfaceExtensionAtIndex(i); + if (extension == eScriptedExtensionInvalid) + continue; + + llvm::StringLiteral extension_name = + ScriptInterpreter::ExtensionToString(extension); + if (grouped_by_extension.contains(extension_name)) + grouped_by_extension[extension_name].push_back(i); + else + grouped_by_extension[extension_name] = {i}; + } - auto print_field = [&s](llvm::StringRef key, llvm::StringRef value) { - if (!value.empty()) { - s.IndentMore(); - s.Indent(); - s << key << ": " << value << '\n'; - s.IndentLess(); + if (command.GetArgumentCount() > 0) { + llvm::StringMap<std::vector<size_t>> filtered; + for (const Args::ArgEntry &arg : command.entries()) { + lldb::ScriptedExtension extension = + ScriptInterpreter::StringToExtension(arg.ref()); + if (extension == eScriptedExtensionInvalid) { + result.AppendErrorWithFormat("no scripted extension named '%s'", + arg.c_str()); + return; + } + llvm::StringLiteral extension_name = + ScriptInterpreter::ExtensionToString(extension); + auto it = grouped_by_extension.find(extension_name); + if (it != grouped_by_extension.end()) + filtered[extension_name] = it->second; } - }; + grouped_by_extension = std::move(filtered); + } - size_t num_listed_interface = 0; - size_t num_extensions = PluginManager::GetNumScriptedInterfaces(); - for (size_t i = 0; i < num_extensions; i++) { - llvm::StringRef plugin_name = - PluginManager::GetScriptedInterfaceNameAtIndex(i); - if (plugin_name.empty()) - break; + if (m_options.m_json_format) + OutputJsonFormat(grouped_by_extension, result); + else + OutputTextFormat(grouped_by_extension, result); + } +private: + std::vector<std::string> + GetLanguagesForExtension(const std::vector<size_t> &indices) { + std::vector<std::string> languages; + for (const size_t idx : indices) { lldb::ScriptLanguage lang = - PluginManager::GetScriptedInterfaceLanguageAtIndex(i); + PluginManager::GetScriptedInterfaceLanguageAtIndex(idx); if (lang != m_options.m_language) continue; + languages.push_back(ScriptInterpreter::LanguageToString(lang)); + } + return languages; + } - if (!num_listed_interface) - s.EOL(); + void OutputJsonFormat( + const llvm::StringMap<std::vector<size_t>> &grouped_by_extension, + CommandReturnObject &result) { + llvm::json::Array extensions; + for (const auto &extension_pair : grouped_by_extension) { + // llvm::json::Value's StringRef constructor does not copy the + // underlying characters, so every string handed to the JSON structure + // below must be an owned std::string -- otherwise it dangles by the + // time the object tree is serialized at the end of this function. + llvm::json::Array languages; + for (const std::string &lang : + GetLanguagesForExtension(extension_pair.second)) + languages.push_back(lang); + if (languages.empty()) + continue; + llvm::StringRef desc = + PluginManager::GetScriptedInterfaceDescriptionAtIndex( + extension_pair.second[0]); + ScriptedInterfaceUsages usages = + PluginManager::GetScriptedInterfaceUsagesAtIndex( + extension_pair.second[0]); + + llvm::json::Array api_usages; + for (llvm::StringRef usage : usages.GetSBAPIUsages()) + api_usages.push_back(usage.str()); + + llvm::json::Array cmd_usages; + for (llvm::StringRef usage : usages.GetCommandInterpreterUsages()) + cmd_usages.push_back(usage.str()); + + extensions.push_back(llvm::json::Object{ + {"name", extension_pair.first().str()}, + {"description", desc.str()}, + {"languages", std::move(languages)}, + {"api_usages", std::move(api_usages)}, + {"command_interpreter_usages", std::move(cmd_usages)}, + }); + } + + std::string str; + llvm::raw_string_ostream os(str); + os << llvm::formatv("{0:2}", llvm::json::Value(std::move(extensions))); + result.AppendMessage(str); + result.SetStatus(eReturnStatusSuccessFinishResult); + } + + void OutputTextFormat( + const llvm::StringMap<std::vector<size_t>> &grouped_by_extension, + CommandReturnObject &result) { + Stream &s = result.GetOutputStream(); + const bool use_color = s.AsRawOstream().colors_enabled(); + auto ansi_code = [use_color](llvm::StringRef code) { + return ansi::FormatAnsiTerminalCodes(code, use_color); + }; + const std::string label_color = ansi_code("${ansi.fg.green}${ansi.bold}"); + const std::string name_color = ansi_code("${ansi.fg.cyan}${ansi.bold}"); + const std::string sep_color = ansi_code("${ansi.faint}"); + const std::string reset = ansi_code("${ansi.normal}"); + const std::string separator( + std::min<uint64_t>(GetDebugger().GetTerminalWidth(), 80), '-'); + + s.Printf("Available scripted extension templates:"); + + auto print_field = [&](llvm::StringRef key, llvm::StringRef value, + const std::string &value_color = std::string()) { + if (value.empty()) + return; + s.IndentMore(); + s.Indent(); + s << label_color << key << ": " << reset; + if (!value_color.empty()) + s << value_color << value << reset; + else + s << value; + s << '\n'; + s.IndentLess(); + }; + + size_t num_listed_interface = 0; + for (const auto &extension_pair : grouped_by_extension) { + std::vector<std::string> languages = + GetLanguagesForExtension(extension_pair.second); + if (languages.empty()) + continue; num_listed_interface++; + s.EOL(); + s << sep_color << separator << reset; + s.EOL(); + llvm::StringRef desc = - PluginManager::GetScriptedInterfaceDescriptionAtIndex(i); + PluginManager::GetScriptedInterfaceDescriptionAtIndex( + extension_pair.second[0]); ScriptedInterfaceUsages usages = - PluginManager::GetScriptedInterfaceUsagesAtIndex(i); + PluginManager::GetScriptedInterfaceUsagesAtIndex( + extension_pair.second[0]); - print_field("Name", plugin_name); - print_field("Language", ScriptInterpreter::LanguageToString(lang)); + print_field("Name", extension_pair.first(), name_color); print_field("Description", desc); - usages.Dump(s, ScriptedInterfaceUsages::UsageKind::API); - usages.Dump(s, ScriptedInterfaceUsages::UsageKind::CommandInterpreter); - - if (i != num_extensions - 1) - s.EOL(); + print_field("Language", llvm::join(languages, "")); + usages.Dump(s, ScriptedInterfaceUsages::UsageKind::API, use_color); + usages.Dump(s, ScriptedInterfaceUsages::UsageKind::CommandInterpreter, + use_color); } if (!num_listed_interface) diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td index 7e6e2c173d67f..bd6fa8efad0a5 100644 --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -1582,6 +1582,9 @@ let Command = "scripting extension list" in { Desc<"Specify the scripting " " language. If none is specified the default scripting language " "is used.">; + def scripting_extension_list_json + : Option<"json", "j">, + Desc<"Output the scripted extension list in json format.">; } let Command = "source info" in { diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp index 34b7f87b09af8..893993d2b24a1 100644 --- a/lldb/source/Core/PluginManager.cpp +++ b/lldb/source/Core/PluginManager.cpp @@ -2126,6 +2126,19 @@ PluginManager::GetScriptedInterfaceUsagesAtIndex(uint32_t idx) { return {}; } +void PluginManager::AutoCompleteScriptedExtension(llvm::StringRef name, + CompletionRequest &request) { + for (size_t idx = 0; idx < GetNumScriptedInterfaces(); idx++) { + if (auto instance = + GetScriptedInterfaceInstances().GetInstanceAtIndex(idx)) { + llvm::StringLiteral extension_name = + ScriptInterpreter::ExtensionToString(instance->extension); + if (extension_name.starts_with(name)) + request.AddCompletion(extension_name); + } + } +} + #pragma mark REPL struct REPLInstance : public PluginInstance<REPLCreateInstance> { diff --git a/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp b/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp index 05d7a5d852f8c..0f1cc83a70a38 100644 --- a/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp +++ b/lldb/source/Interpreter/Interfaces/ScriptedInterfaceUsages.cpp @@ -8,20 +8,30 @@ #include "lldb/Interpreter/Interfaces/ScriptedInterfaceUsages.h" +#include "lldb/Utility/AnsiTerminal.h" + using namespace lldb; using namespace lldb_private; -void ScriptedInterfaceUsages::Dump(Stream &s, UsageKind kind) const { +void ScriptedInterfaceUsages::Dump(Stream &s, UsageKind kind, + bool use_color) const { + const std::string label = + ansi::FormatAnsiTerminalCodes("${ansi.fg.green}${ansi.bold}", use_color); + const std::string dim = + ansi::FormatAnsiTerminalCodes("${ansi.faint}", use_color); + const std::string reset = + ansi::FormatAnsiTerminalCodes("${ansi.normal}", use_color); + s.IndentMore(); s.Indent(); llvm::StringRef usage_kind = (kind == UsageKind::CommandInterpreter) ? "Command Interpreter" : "API"; - s << usage_kind << " Usages:"; + s << label << usage_kind << " Usages:" << reset; const std::vector<llvm::StringRef> &usages = (kind == UsageKind::CommandInterpreter) ? GetCommandInterpreterUsages() : GetSBAPIUsages(); if (usages.empty()) - s << " None\n"; + s << ' ' << dim << "None" << reset << '\n'; else if (usages.size() == 1) s << " " << usages.front() << '\n'; else { diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp index 8dafb7f823e70..8420782b02814 100644 --- a/lldb/source/Interpreter/ScriptInterpreter.cpp +++ b/lldb/source/Interpreter/ScriptInterpreter.cpp @@ -16,6 +16,7 @@ #include "lldb/Utility/Stream.h" #include "lldb/Utility/StringList.h" #include "lldb/ValueObject/ValueObject.h" +#include "llvm/ADT/StringSwitch.h" #if defined(_WIN32) #include "lldb/Host/windows/ConnectionGenericFileWindows.h" #endif @@ -188,6 +189,50 @@ ScriptInterpreter::StringToLanguage(const llvm::StringRef &language) { return eScriptLanguageUnknown; } +llvm::StringLiteral +ScriptInterpreter::ExtensionToString(lldb::ScriptedExtension extension) { + switch (extension) { + case eScriptedExtensionInvalid: + return "Invalid"; + case eScriptedExtensionOperatingSystem: + return "OperatingSystem"; + case eScriptedExtensionScriptedPlatform: + return "ScriptedPlatform"; + case eScriptedExtensionScriptedProcess: + return "ScriptedProcess"; + case eScriptedExtensionScriptedBreakpointResolver: + return "ScriptedBreakpointResolver"; + case eScriptedExtensionScriptedThreadPlan: + return "ScriptedThreadPlan"; + case eScriptedExtensionScriptedFrameProvider: + return "ScriptedFrameProvider"; + case eScriptedExtensionScriptedHook: + return "ScriptedHook"; + case eScriptedExtensionScriptedThread: + return "ScriptedThread"; + case eScriptedExtensionScriptedFrame: + return "ScriptedFrame"; + } + llvm_unreachable("unhandled ScriptedExtension"); +} + +lldb::ScriptedExtension +ScriptInterpreter::StringToExtension(llvm::StringRef string) { + return llvm::StringSwitch<lldb::ScriptedExtension>(string) + .CaseLower("OperatingSystem", eScriptedExtensionOperatingSystem) + .CaseLower("ScriptedPlatform", eScriptedExtensionScriptedPlatform) + .CaseLower("ScriptedProcess", eScriptedExtensionScriptedProcess) + .CaseLower("ScriptedBreakpointResolver", + eScriptedExtensionScriptedBreakpointResolver) + .CaseLower("ScriptedThreadPlan", eScriptedExtensionScriptedThreadPlan) + .CaseLower("ScriptedFrameProvider", + eScriptedExtensionScriptedFrameProvider) + .CaseLower("ScriptedHook", eScriptedExtensionScriptedHook) + .CaseLower("ScriptedThread", eScriptedExtensionScriptedThread) + .CaseLower("ScriptedFrame", eScriptedExtensionScriptedFrame) + .Default(eScriptedExtensionInvalid); +} + Status ScriptInterpreter::SetBreakpointCommandCallback( std::vector<std::reference_wrapper<BreakpointOptions>> &bp_options_vec, const char *callback_text) { diff --git a/lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py b/lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py new file mode 100644 index 0000000000000..5d92717babc4b --- /dev/null +++ b/lldb/test/API/commands/scripting/extension/TestScriptingExtensionListJSON.py @@ -0,0 +1,103 @@ +""" +Verify that `scripting extension list -j` emits a well-formed JSON array of +scripted-extension records that can be round-tripped through SBStructuredData. +""" + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * + + +class TestScriptingExtensionListJSON(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + def run_command(self, command): + result = lldb.SBCommandReturnObject() + self.dbg.GetCommandInterpreter().HandleCommand(command, result) + self.assertTrue(result.Succeeded(), result.GetError()) + return result.GetOutput() + + def run_and_parse(self, command): + data = lldb.SBStructuredData() + self.assertSuccess(data.SetFromJSON(self.run_command(command))) + return data + + @skipIfNoSBHeaders + def test_json_output_shape(self): + """Every entry has the expected top-level keys and array-valued + `languages`, `api_usages`, `command_interpreter_usages`.""" + data = self.run_and_parse("scripting extension list -j") + self.assertEqual(data.GetType(), lldb.eStructuredDataTypeArray) + self.assertGreater(data.GetSize(), 0) + + expected_keys = { + "name", + "description", + "languages", + "api_usages", + "command_interpreter_usages", + } + for i in range(data.GetSize()): + entry = data.GetItemAtIndex(i) + self.assertEqual(entry.GetType(), lldb.eStructuredDataTypeDictionary) + keys = lldb.SBStringList() + entry.GetKeys(keys) + got = {keys.GetStringAtIndex(k) for k in range(keys.GetSize())} + self.assertEqual(got, expected_keys) + self.assertEqual( + entry.GetValueForKey("name").GetType(), + lldb.eStructuredDataTypeString, + ) + self.assertEqual( + entry.GetValueForKey("description").GetType(), + lldb.eStructuredDataTypeString, + ) + for array_key in ("languages", "api_usages", "command_interpreter_usages"): + self.assertEqual( + entry.GetValueForKey(array_key).GetType(), + lldb.eStructuredDataTypeArray, + array_key, + ) + + def test_json_includes_known_extension(self): + """`ScriptedProcess` is registered unconditionally, so it must appear + with `SBTarget.Launch` in its `api_usages`.""" + data = self.run_and_parse("scripting extension list -j") + + names = set() + scripted_process_entry = None + for i in range(data.GetSize()): + entry = data.GetItemAtIndex(i) + name = entry.GetValueForKey("name").GetStringValue(256) + names.add(name) + if name == "ScriptedProcess": + scripted_process_entry = entry + self.assertIn("ScriptedProcess", names) + + api_usages = scripted_process_entry.GetValueForKey("api_usages") + found_launch = False + for i in range(api_usages.GetSize()): + usage = api_usages.GetItemAtIndex(i).GetStringValue(256) + if usage == "SBTarget.Launch": + found_launch = True + break + self.assertTrue( + found_launch, + f"expected SBTarget.Launch in ScriptedProcess.api_usages", + ) + + def test_json_name_filter(self): + """`scripting extension list -j <name>` restricts output to that + one extension.""" + data = self.run_and_parse("scripting extension list -j ScriptedProcess") + self.assertEqual(data.GetSize(), 1) + entry = data.GetItemAtIndex(0) + self.assertEqual( + entry.GetValueForKey("name").GetStringValue(256), "ScriptedProcess" + ) + + def test_json_lua_yields_empty_array(self): + """No lua-registered extensions -> empty JSON array.""" + data = self.run_and_parse("scripting extension list -j -l lua --") + self.assertEqual(data.GetType(), lldb.eStructuredDataTypeArray) + self.assertEqual(data.GetSize(), 0) diff --git a/lldb/test/Shell/Commands/command-scripting-extension-list.test b/lldb/test/Shell/Commands/command-scripting-extension-list.test index 8ca183a609671..880d4035516de 100644 --- a/lldb/test/Shell/Commands/command-scripting-extension-list.test +++ b/lldb/test/Shell/Commands/command-scripting-extension-list.test @@ -2,53 +2,60 @@ # RUN: %lldb -s %s -o exit | FileCheck %s scripting extension list -# CHECK:Available scripted extension templates: - -# CHECK: Name: OperatingSystemPythonInterface -# CHECK-NEXT: Language: Python -# CHECK-NEXT: Description: Mock thread state -# CHECK-NEXT: API Usages: None -# CHECK-NEXT: Command Interpreter Usages: -# CHECK-NEXT: settings set target.process.python-os-plugin-path <script-path> -# CHECK-NEXT: settings set process.experimental.os-plugin-reports-all-threads [0/1] - -# CHECK: Name: ScriptedPlatformPythonInterface -# CHECK-NEXT: Language: Python -# CHECK-NEXT: Description: Mock platform and interact with its processes. -# CHECK-NEXT: API Usages: None -# CHECK-NEXT: Command Interpreter Usages: None - -# CHECK: Name: ScriptedProcessPythonInterface -# CHECK-NEXT: Language: Python -# CHECK-NEXT: Description: Mock process state -# CHECK-NEXT: API Usages: -# CHECK-NEXT: SBAttachInfo.SetScriptedProcessClassName -# CHECK-NEXT: SBAttachInfo.SetScriptedProcessDictionary -# CHECK-NEXT: SBTarget.Attach -# CHECK-NEXT: SBLaunchInfo.SetScriptedProcessClassName -# CHECK-NEXT: SBLaunchInfo.SetScriptedProcessDictionary -# CHECK-NEXT: SBTarget.Launch -# CHECK-NEXT: Command Interpreter Usages: -# CHECK-NEXT: process attach -C <script-name> [-k key -v value ...] -# CHECK-NEXT: process launch -C <script-name> [-k key -v value ...] - -# CHECK: Name: ScriptedThreadPlanPythonInterface -# CHECK-NEXT: Language: Python -# CHECK-NEXT: Description: Alter thread stepping logic and stop reason -# CHECK-NEXT: API Usages: SBThread.StepUsingScriptedThreadPlan -# CHECK-NEXT: Command Interpreter Usages: thread step-scripted -C <script-name> [-k key -v value ...] - -# CHECK: Name: ScriptedThreadPythonInterface -# CHECK-NEXT: Language: Python -# CHECK-NEXT: Description: Provide thread state for a scripted process. -# CHECK-NEXT: API Usages: None -# CHECK-NEXT: Command Interpreter Usages: None - -# CHECK: Name: ScriptedFramePythonInterface -# CHECK-NEXT: Language: Python -# CHECK-NEXT: Description: Provide frame state for scripted threads and frame providers. -# CHECK-NEXT: API Usages: None -# CHECK-NEXT: Command Interpreter Usages: None +# CHECK: Available scripted extension templates: + +# The extensions are grouped by lldb::ScriptedExtension in a hash map, so +# groups are not printed in a fixed order across builds. Match each group's +# fields with CHECK-DAG instead of CHECK-NEXT. +# CHECK-DAG: Name: OperatingSystem +# CHECK-DAG: Language: Python +# CHECK-DAG: Description: Mock thread state +# CHECK-DAG: settings set target.process.python-os-plugin-path <script-path> +# CHECK-DAG: settings set process.experimental.os-plugin-reports-all-threads [0/1] + +# CHECK-DAG: Name: ScriptedPlatform +# CHECK-DAG: Description: Mock platform and interact with its processes. + +# CHECK-DAG: Name: ScriptedProcess +# CHECK-DAG: Description: Mock process state +# CHECK-DAG: SBAttachInfo.SetScriptedProcessClassName +# CHECK-DAG: SBAttachInfo.SetScriptedProcessDictionary +# CHECK-DAG: SBTarget.Attach +# CHECK-DAG: SBLaunchInfo.SetScriptedProcessClassName +# CHECK-DAG: SBLaunchInfo.SetScriptedProcessDictionary +# CHECK-DAG: SBTarget.Launch +# CHECK-DAG: process attach -C <script-name> [-k key -v value ...] +# CHECK-DAG: process launch -C <script-name> [-k key -v value ...] + +# CHECK-DAG: Name: ScriptedThreadPlan +# CHECK-DAG: Description: Alter thread stepping logic and stop reason +# CHECK-DAG: SBThread.StepUsingScriptedThreadPlan +# CHECK-DAG: thread step-scripted -C <script-name> [-k key -v value ...] + +# CHECK-DAG: Name: ScriptedBreakpointResolver +# CHECK-DAG: Description: Create a breakpoint that chooses locations based on user-created callbacks +# CHECK-DAG: SBTarget.BreakpointCreateFromScript +# CHECK-DAG: breakpoint set -P classname [-k key -v value ...] + +# CHECK-DAG: Name: ScriptedFrameProvider +# CHECK-DAG: Description: Provide scripted stack frames for threads +# CHECK-DAG: SBTarget.RegisterScriptedFrameProvider +# CHECK-DAG: SBTarget.RemoveScriptedFrameProvider +# CHECK-DAG: SBTarget.ClearScriptedFrameProvider +# CHECK-DAG: target frame-provider register -C <script-name> [-k key -v value ...] +# CHECK-DAG: target frame-provider list +# CHECK-DAG: target frame-provider remove <provider-name> +# CHECK-DAG: target frame-provider clear + +# CHECK-DAG: Name: ScriptedHook +# CHECK-DAG: Description: Perform actions on target lifecycle events (module load/unload, process stop). +# CHECK-DAG: target hook add -P <script-name> [-k key -v value ...] + +# CHECK-DAG: Name: ScriptedThread +# CHECK-DAG: Description: Provide thread state for a scripted process. + +# CHECK-DAG: Name: ScriptedFrame +# CHECK-DAG: Description: Provide frame state for scripted threads and frame providers. scripting extension list -l lua # CHECK: Available scripted extension templates: None _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
