From 2aa6603dbba0fbfeb6e1bf236cbea8971bd3d443 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 22 Mar 2026 12:04:34 +0100 Subject: [PATCH] Added safe_delete and inline_symbol tools (JB backend) --- scripts/demo_run_tools.py | 15 ++++- .../jetbrains/jetbrains_plugin_client.py | 36 +++++++++++ .../config/internal_modes/jetbrains.yml | 3 + src/serena/tools/jetbrains_tools.py | 59 +++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index bad7ed60..5d2b3358 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -4,6 +4,7 @@ for testing or development. Here the tools will be operation the serena repo its """ import json +from pathlib import Path from pprint import pprint from serena.agent import SerenaAgent @@ -14,13 +15,17 @@ from serena.tools import ( FindReferencingSymbolsTool, JetBrainsFindSymbolTool, JetBrainsGetSymbolsOverviewTool, + JetBrainsInlineSymbol, + JetBrainsSafeDeleteTool, SearchForPatternTool, ) if __name__ == "__main__": serena_config = SerenaConfig.from_config_file() serena_config.web_dashboard = False - agent = SerenaAgent(project=REPO_ROOT, serena_config=serena_config) + # project = Path(REPO_ROOT).parent / "serena-jetbrains-plugin-copy" + project = Path(REPO_ROOT) / "test/resources/repos/python/test_repo" + agent = SerenaAgent(project=str(project), serena_config=serena_config) # apply a tool find_symbol_tool = agent.get_tool(JetBrainsFindSymbolTool) @@ -28,9 +33,15 @@ if __name__ == "__main__": find_file_tool = agent.get_tool(FindFileTool) search_pattern_tool = agent.get_tool(SearchForPatternTool) overview_tool = agent.get_tool(JetBrainsGetSymbolsOverviewTool) + safe_delete_tool = agent.get_tool(JetBrainsSafeDeleteTool) + inline_symbol = agent.get_tool(JetBrainsInlineSymbol) result = agent.execute_task( - lambda: find_symbol_tool.apply("SerenaAgent/get_tool_description_override"), + lambda: inline_symbol.apply( + relative_path="test_repo/nested.py", + name_path="OuterClass/NestedClass", + keep_definition=True, + ) ) pprint(json.loads(result)) # input("Press Enter to continue...") diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index 56b08758..b79b226a 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -519,6 +519,42 @@ class JetBrainsPluginClient(ToStringMixin): } return cast(jb.TypeHierarchyResponse, self._make_request("POST", "/getSubtypes", request_data)) + def safe_delete(self, name_path: str | None, relative_path: str, delete_even_if_used: bool, propagate: bool) -> dict[str, Any]: + """ + Safely deletes a symbol, checking for usages first. + + :param name_path: the name path of the symbol to delete + :param relative_path: the relative path to the file containing the symbol + :param delete_even_if_used: if True, delete the symbol even if it has usages + """ + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "deleteEvenIfUsed": delete_even_if_used, + "propagate": propagate, + } + return self._make_request("POST", "/safeDelete", request_data) + + def inline_symbol( + self, + name_path: str, + relative_path: str, + keep_definition: bool, + ) -> dict[str, Any]: + """ + Inlines a method, replacing all call sites with the method body. + + :param name_path: the name path of the method to inline + :param relative_path: the relative path to the file containing the method + :param keep_definition: if True, keep the original method definition after inlining + """ + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "keepDefinition": keep_definition, + } + return self._make_request("POST", "/inlineSymbol", request_data) + def rename_symbol( self, name_path: str, relative_path: str, new_name: str, rename_in_comments: bool, rename_in_text_occurrences: bool ) -> None: diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index 863f3beb..090c6b8f 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -13,4 +13,7 @@ included_optional_tools: - jet_brains_find_symbol - jet_brains_find_referencing_symbols - jet_brains_get_symbols_overview + - jet_brains_move_symbol - jet_brains_type_hierarchy + - jet_brains_safe_delete + - jet_brains_inline_symbol diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 68323872..f96bcd67 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -141,6 +141,65 @@ class JetBrainsMoveSymbolTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): return self._to_json(response_dict) +class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + def apply( + self, + relative_path: str, + name_path: str | None = None, + delete_even_if_used: bool = False, + propagate: bool = False, + ) -> str: + """ + Safely deletes a symbol, checking for usages first. It is also + possible to request deleting of usages and cleaning up of unused code. + + :param relative_path: the relative path to the file containing the symbol to delete. + :param name_path: the name path of the symbol to delete. + A name path identifies a symbol within a source file, e.g. "MyClass/my_method". + :param delete_even_if_used: whether to force deletion even if the symbol still has usages. + Default is False (safe mode: will report usages instead of deleting). + :param propagate: whether to propagate the deletion to usages of the symbol and also + remove symbols that become unused after the deletion. Default is False. + :return: JSON string with the result of the operation. + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.safe_delete( + name_path=name_path, + relative_path=relative_path, + delete_even_if_used=delete_even_if_used, + propagate=propagate, + ) + return self._to_json(response_dict) + + +class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): + def apply( + self, + name_path: str, + relative_path: str, + keep_definition: bool = False, + ) -> str: + """ + Inlines a symbol (usually method, but also classes may be amenable to inlining, + which turns invocation into anonymous class creation), + replacing all call sites with the method's body. + An error is raised when requested on a symbol that cannot be inlined. + + :param name_path: the name path of the method to inline. + :param relative_path: the relative path to the file containing the method to inline. + :param keep_definition: whether to keep the original method definition after inlining all call sites. + May be ignored in some cases (e.g., when inlining a class).. + :return: JSON string with the result of the operation. + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.inline_symbol( + name_path=name_path, + relative_path=relative_path, + keep_definition=keep_definition, + ) + return self._to_json(response_dict) + + class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds symbols that reference the given symbol using the JetBrains backend