================
@@ -265,6 +267,349 @@ StructuredData::DictionarySP 
ScriptInterpreterPython::GetInterpreterInfo() {
   return info_json.CreateStructuredDictionary();
 }
 
+llvm::Expected<std::string> ScriptInterpreterPython::ExtensionToImportPath(
+    lldb::ScriptedExtension extension) {
+  switch (extension) {
+  case eScriptedExtensionOperatingSystem:
+    return "lldb.plugins.operating_system";
+  case eScriptedExtensionScriptedPlatform:
+    return "lldb.plugins.scripted_platform";
+  case eScriptedExtensionScriptedProcess:
+    return "lldb.plugins.scripted_process";
+  case eScriptedExtensionScriptedHook:
+    return "lldb.plugins.scripted_hook";
+  case eScriptedExtensionScriptedBreakpointResolver:
+    return "lldb.plugins.scripted_breakpoint";
+  case eScriptedExtensionScriptedThreadPlan:
+    return "lldb.plugins.scripted_thread_plan";
+  case eScriptedExtensionScriptedFrameProvider:
+    return "lldb.plugins.scripted_frame_provider";
+  case eScriptedExtensionScriptedThread:
+  case eScriptedExtensionScriptedFrame:
+    return "lldb.plugins.scripted_process";
+  case eScriptedExtensionInvalid:
+    return llvm::createStringError("invalid extension name");
+  }
+  return llvm::createStringError("invalid extension name");
+}
+
+llvm::Expected<StructuredData::ObjectSP>
+ScriptInterpreterPython::GetExtensionSchema(
+    llvm::SmallVector<llvm::StringRef> &extension_path) {
+  lldb::ScriptedExtension extension =
+      ScriptInterpreter::StringToExtension(extension_path.back());
+  auto import_path_or_err = ExtensionToImportPath(extension);
+  if (!import_path_or_err)
+    return import_path_or_err.takeError();
+
+  StreamString command_stream;
+  // __import__(path, fromlist=['']) imports the submodule and returns it
+  // directly (rather than the top-level package), as a single expression --
+  // this keeps the whole call eval-able in one line while guaranteeing the
+  // module is imported first; referencing "<import_path>.<ClassName>"
+  // directly would only work if something else had already imported
+  // <import_path> as a side effect.
+  command_stream.Printf("lldb.embedded_interpreter.generate_extension_schema("
+                        "__import__('%s', fromlist=['']).%s)",
+                        import_path_or_err->c_str(),
+                        
ScriptInterpreter::ExtensionToString(extension).data());
+
+  // Use eScriptReturnTypeOpaqueObject: it transfers a real owned reference
+  // we can safely extract the string from. eScriptReturnTypeCharStrOrNone
+  // instead hands back a pointer to a temporary Python object's buffer
+  // that gets destroyed (and, for a freshly created string like this one,
+  // deallocated) as soon as ExecuteOneLineWithReturn returns -- reading
+  // it afterwards is a use-after-free.
+  void *result_obj = nullptr;
+  if (!ExecuteOneLineWithReturn(
+          command_stream.GetData(),
+          ScriptInterpreter::eScriptReturnTypeOpaqueObject, &result_obj,
+          ExecuteScriptOptions().SetEnableIO(false)))
+    return llvm::createStringError("invalid extension schema format");
+
+  // ExecuteOneLineWithReturn releases the GIL before returning, so touching
+  // the returned object (Str() below can execute arbitrary Python code) must
+  // re-acquire it first. py_result is scoped so its destructor (a DECREF)
+  // also runs before the GIL is released below, not after.
+  std::string schema_str;
+  {
+    PyGILState_STATE gil_state = PyGILState_Ensure();
+    {
+      PythonObject py_result(PyRefType::Owned,
+                             static_cast<PyObject *>(result_obj));
+      if (py_result.IsAllocated() && py_result.get() != Py_None)
+        schema_str = py_result.Str().GetString().str();
+    }
+    PyGILState_Release(gil_state);
+  }
+
+  if (schema_str.empty())
+    return llvm::createStringError("empty extension schema");
+  return StructuredData::ParseJSON(schema_str);
+}
+
+llvm::Error ScriptInterpreterPython::ParseExtensionSchema(
+    Stream &s, llvm::StringRef output_script_prefix,
+    llvm::SmallVector<llvm::StringRef> &extension_path,
+    bool generate_non_abstract_methods, std::set<std::string> &typing_imports) 
{
+  auto schema_or_err = GetExtensionSchema(extension_path);
+  if (!schema_or_err)
+    return schema_or_err.takeError();
+
+  StructuredData::ObjectSP schema = *schema_or_err;
+  StructuredData::Dictionary *dict = schema->GetAsDictionary();
+
+  // Merge each class' typing imports into the caller-owned set so the
+  // final `from typing import ...` line covers every class we emit.
+  StructuredData::Array *schema_typing;
+  if (dict->GetValueForKeyAsArray("typing_imports", schema_typing))
+    schema_typing->ForEach([&](StructuredData::Object *entry) {
+      if (auto *str = entry->GetAsString())
+        typing_imports.insert(str->GetValue().str());
+      return true;
+    });
+
+  llvm::StringRef base_class, import_path;
+  if (!dict->GetValueForKeyAsString("class", base_class))
+    return llvm::createStringError(
+        llvm::formatv("extension schema dictionary is missing 'class' key")
+            .str());
+  if (!dict->GetValueForKeyAsString("module", import_path))
+    return llvm::createStringError(
+        llvm::formatv("extension schema dictionary is missing 'module' key")
+            .str());
+
+  // imports
+  s.Printf("from %s import %s\n", import_path.data(), base_class.data());
+  s.EOL();
+
+  // class definition
+  s.Printf("class %s%s(%s):\n", output_script_prefix.data(), base_class.data(),
+           base_class.data());
+  s.IndentMore();
+
+  // Class docstring: list the non-callable members the base class exposes
+  // so the user sees what's available without having to hop back to the
+  // base class definition.
+  bool has_body = false;
+  StructuredData::Array *attributes;
+  if (dict->GetValueForKeyAsArray("attributes", attributes) &&
+      attributes->GetSize()) {
+    s.Indent();
+    s.PutCString("\"\"\"\n");
+    s.Indent();
+    s.Printf("Attributes inherited from %s:\n", base_class.data());
+    for (size_t i = 0; i < attributes->GetSize(); i++) {
+      auto maybe_dict = attributes->GetItemAtIndexAsDictionary(i);
+      if (!maybe_dict)
+        continue;
+      StructuredData::Dictionary *attr_dict = *maybe_dict;
+      llvm::StringRef attr_name;
+      if (!attr_dict->GetValueForKeyAsString("name", attr_name))
+        continue;
+      llvm::StringRef attr_type;
+      bool has_type = attr_dict->GetValueForKeyAsString("type", attr_type);
+      s.Indent();
+      s.Printf("- %s", attr_name.data());
+      if (has_type)
+        s.Printf(": %s", attr_type.data());
+      s.EOL();
+    }
+    s.Indent();
+    s.PutCString("\"\"\"\n\n");
+    has_body = true;
+  }
+
+  // members
+  StructuredData::Array *members;
+  if (!dict->GetValueForKeyAsArray("members", members))
+    return llvm::createStringError("missing 'members' key in extension 
schema");
+
+  // If the base class doesn't mark anything `@abstractmethod`, the filter
+  // "only stub abstract methods" would leave the derived class empty --
+  // which isn't a useful starting point. Fall back to emitting every
+  // method in that case so the user has actual code to edit.
+  bool any_abstract = false;
+  for (size_t i = 0; i < members->GetSize(); i++) {
+    auto maybe_dict = members->GetItemAtIndexAsDictionary(i);
+    if (!maybe_dict)
+      continue;
+    bool is_abstract = false;
+    if ((*maybe_dict)->GetValueForKeyAsBoolean("is_abstract", is_abstract) &&
+        is_abstract) {
+      any_abstract = true;
+      break;
+    }
+  }
+  bool emit_all_methods = generate_non_abstract_methods || !any_abstract;
+
+  for (size_t i = 0; i < members->GetSize(); i++) {
+    auto maybe_dict = members->GetItemAtIndexAsDictionary(i);
+    if (!maybe_dict)
+      return llvm::createStringError(
+          llvm::formatv(
+              "member at index {0} in extension schema isn't a dictionary")
+              .str());
+
+    StructuredData::Dictionary *member_dict = *maybe_dict;
+    llvm::StringRef symbol, args;
+    if (!member_dict->GetValueForKeyAsString("name", symbol))
+      return llvm::createStringError(
+          llvm::formatv(
+              "member at index {0} in extension schema is missing 'name' key")
+              .str());
+    if (!member_dict->GetValueForKeyAsString("signature", args))
+      return llvm::createStringError(
+          llvm::formatv("member at index {0} in extension schema is missing "
+                        "'signature' key")
+              .str());
+
+    bool is_abstract = false;
+    bool has_is_abstract =
+        member_dict->GetValueForKeyAsBoolean("is_abstract", is_abstract);
+    if (!emit_all_methods)
+      if (!has_is_abstract || !is_abstract)
+        continue;
+
+    s.Indent();
+    s.Printf("def %s%s:\n", symbol.data(), args.data());
+
+    s.IndentMore();
+    llvm::StringRef documentation;
+    if (member_dict->GetValueForKeyAsString("doc", documentation)) {
+      s.Indent();
+      s.PutCString("\"\"\"\n");
+
+      llvm::SmallVector<llvm::StringRef> lines;
+      documentation.split(lines, "\n");
+
+      for (llvm::StringRef line : lines) {
+        s.Indent();
+        s.PutCString(line);
+        s.EOL();
+      }
+
+      s.Indent();
+      s.PutCString("\"\"\"");
+      s.EOL();
+    }
+
+    if (symbol == "__init__") {
+      // The base class' constructor sets up attributes (e.g. self.target,
+      // self.process) that the inherited, non-overridden methods rely on.
+      // Forward the same arguments so that state is still initialized.
+      // Splitting the param list on `,` requires bracket-depth awareness
+      // because annotations like `Union[X, Y]` also contain commas.
+      llvm::StringRef params = args.trim("()");
+      std::vector<std::string> forwarded_args;
+      int depth = 0;
+      size_t start = 0;
+      auto flush = [&](size_t end) {
+        llvm::StringRef param = params.slice(start, end);
+        param = param.split(':').first.split('=').first.trim();
+        if (!param.empty() && param != "self")
+          forwarded_args.push_back(param.str());
+      };
+      for (size_t i = 0; i < params.size(); ++i) {
+        char c = params[i];
+        if (c == '[' || c == '(' || c == '{')
+          ++depth;
+        else if (c == ']' || c == ')' || c == '}')
+          --depth;
+        else if (c == ',' && depth == 0) {
+          flush(i);
+          start = i + 1;
+        }
+      }
+      flush(params.size());
+      s.Indent();
+      s.Printf("super().__init__(%s)\n",
+               llvm::join(forwarded_args, ", ").c_str());
+    }
+
+    s.Indent();
+    s.PutCString("# TODO: Implement\n");
+    s.Indent();
+    s.PutCString("pass\n\n");
+    s.IndentLess();
+    has_body = true;
+  }
+
+  // A class with no body is a Python syntax error, so emit `pass` when the
+  // base class has nothing to stub out (no methods and no attributes to
+  // document).
+  if (!has_body) {
+    s.Indent();
+    s.PutCString("pass\n");
+  }
+
+  return llvm::Error::success();
+}
+
+llvm::Expected<FileSpec> ScriptInterpreterPython::GenerateExtensionTemplate(
+    const std::string name,
+    std::vector<std::pair<llvm::StringRef, llvm::SmallVector<llvm::StringRef>>>
+        &extensions,
+    bool generate_non_abstract_methods, std::string output_file) {
+  // `ParseExtensionSchema` accumulates every `typing` generic it sees
+  // (`Optional`, `Union`, `List`, ...) into this set so we can emit a
+  // targeted `from typing import ...` line only for what's actually
+  // referenced. The Python schema does the detection so we don't have
+  // to re-scan strings here.
+  std::set<std::string> typing_imports;
+  StreamString bodies;
+  for (auto extension_pair : extensions) {
+    if (llvm::Error err =
+            ParseExtensionSchema(bodies, name, extension_pair.second,
+                                 generate_non_abstract_methods, 
typing_imports))
+      return std::move(err);
+    bodies.PutCString("\n\n");
+  }
+
+  StreamString generated_file_stream;
+  generated_file_stream.PutCString("import lldb\n");
+  if (!typing_imports.empty()) {
+    std::vector<std::string> sorted_imports(typing_imports.begin(),
+                                            typing_imports.end());
+    generated_file_stream.Format("from typing import {0}\n",
+                                 llvm::join(sorted_imports, ", "));
+  }
+  generated_file_stream.PutCString("\n");
+  generated_file_stream.PutCString(bodies.GetString());
+
+  FileSpec save_location;
+  if (output_file.empty()) {
+    const std::string file_name =
+        "lldb_" + llvm::StringRef(name).lower() + "_extension.py";
----------------
JDevlieghere wrote:

What happens if I pass `/` as the name? Should this be sanitized?

https://github.com/llvm/llvm-project/pull/209647
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to