From f94f891112a5a91c9f3dedc57765aa96a509f5a5 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 1 Jun 2026 08:22:51 +0200 Subject: [PATCH] JetBrains: add jet_brains_find_unused_code tool Add the jet_brains_find_unused_code tool (JetBrainsFindUnusedCodeTool) backed by a new find_unused_code plugin-client method that calls the plugin's /findUnusedCode endpoint, and enable it in the jetbrains internal mode. It reports code symbols (classes, methods, fields, ...) declared in a file that have no references in the project - a usage-based heuristic for finding likely-dead code. Requires Serena JetBrains plugin version 2023.2.17+. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 5 +++ .../jetbrains/jetbrains_plugin_client.py | 14 +++++++ .../config/internal_modes/jetbrains.yml | 3 ++ src/serena/tools/jetbrains_tools.py | 42 +++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2872553c78..292e878c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: Host validation required a local host regardless of the listen address (regression introduced in v1.5.2), preventing remote connections +* JetBrains: + - Add new tool `jet_brains_find_unused_code`: reports code symbols (classes, methods, fields, ...) declared in a + file that have no references in the project — likely-dead code (usage-based heuristic via the IDE's reference + search). Requires Serena JetBrains plugin version 2023.2.17+. + # v1.5.3 (2026-05-26) Add meta-data for the GitHub MCP registry diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index c7b440cefc..46eea4d9be 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -633,6 +633,20 @@ def find_implementations(self, relative_path: str, name_path: str, include_quick self._postprocess_symbol_collection_response(symbol_collection) return symbol_collection + def find_unused_code(self, relative_path: str, include_quick_info: bool = False) -> jb.SymbolCollectionResponse: + """ + Finds code symbols (classes, methods, fields, ...) declared in the given file that have no references + anywhere in the project, i.e. code that is likely unused. + + :param relative_path: the relative path to the file to analyze for unused code + :param include_quick_info: whether to include quick info (typically the signature) for each unused symbol + """ + self._require_version_at_least(2023, 2, 17) + request_data = {"relativePath": relative_path, "includeQuickInfo": include_quick_info} + symbol_collection = cast(jb.SymbolCollectionResponse, self._make_request("POST", "/findUnusedCode", request_data)) + self._postprocess_symbol_collection_response(symbol_collection) + return symbol_collection + def debug_eval(self, repl_key: str, expression: str) -> dict[str, Any]: """ Evaluates a Groovy expression in the persistent debug REPL. diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index e3776dd916..ebfde29dd3 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -6,6 +6,8 @@ prompt: | * `jet_brains_find_referencing_symbols` replaces `find_referencing_symbols` * `jet_brains_get_symbols_overview` replaces `get_symbols_overview` * `jet_brains_rename` replaces `rename_symbol` + In addition, `jet_brains_find_unused_code` reports code symbols in a file that have no references in the + project (a usage-based heuristic for finding likely-dead code). excluded_tools: - find_symbol - find_referencing_symbols @@ -32,3 +34,4 @@ included_optional_tools: - serena_info - jet_brains_run_inspections - jet_brains_list_inspections + - jet_brains_find_unused_code diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 74a0535905..34e9e6f8f1 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -668,3 +668,45 @@ def apply( ) result = self._to_json(response_dict) return self._limit_length(result, max_answer_chars) + + +class JetBrainsFindUnusedCodeTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional, ToolMarkerBeta): + """ + Finds likely-unused code symbols (classes, methods, fields, ...) in a file using the JetBrains backend + """ + + symbol_dict_grouper = JetBrainsSymbolDictGrouper(["relative_path", "type"], ["type"], collapse_singleton=True) + + def apply( + self, + relative_path: str, + include_quick_info: bool = False, + max_answer_chars: int = -1, + ) -> str: + """ + Finds code symbols (classes, methods, fields, etc.) declared in the given file that have no references + anywhere in the project, i.e. code that is likely unused and may be safe to remove. + This is a usage-based heuristic computed via the IDE's reference search: entry points (e.g. main methods, + public API that is consumed outside the project) and reflective/framework usages are NOT accounted for, and + transitively dead code (a private member used only by other dead code) is not reported. Review results + before deleting anything. + + :param relative_path: the relative path to the file to analyze for unused code. + :param include_quick_info: whether to include quick info (typically the signature) for each unused symbol. + :param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened + result is returned. + :return: the unused symbols grouped by file and type; a message stating that none were found if the file has + no unused symbols. + """ + relative_path = self._sanitize_input_param(relative_path) + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.find_unused_code( + relative_path=relative_path, + include_quick_info=include_quick_info, + ) + symbols = response_dict["symbols"] + if not symbols: + return f"No unused code symbols found in {relative_path}." + grouped = self.symbol_dict_grouper.group(symbols) + result = self._to_json(grouped) + return self._limit_length(result, max_answer_chars)