Merge pull request #1036 from oraios/generic-symbol-output-grouping

Implement generic SymbolDictGrouper
This commit is contained in:
Dominik Jain authored and GitHub committed 2026-02-15 13:19:08 +01:00
commit 448b3693bc
11 files changed
+288 -155

No files matched your search

+2 -3
View File
@@ -39,6 +39,7 @@ from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsO
from serena.util.dataclass import get_dataclass_default
from serena.util.logging import MemoryLogHandler
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
from solidlsp.util.subprocess_util import subprocess_kwargs
log = logging.getLogger(__name__)
@@ -811,9 +812,7 @@ class ProjectCommands(AutoRegisteringGroup):
return
# Extract suitable symbol (prefer class or function over variables)
# LSP symbol kinds: 5=class, 12=function, 6=method, 9=constructor
preferred_kinds = [5, 12, 6, 9] # class, function, method, constructor
preferred_kinds = {SymbolKind.Class.name, SymbolKind.Function.name, SymbolKind.Method.name, SymbolKind.Constructor.name}
selected_symbol = None
for symbol in overview_data:
if symbol.get("kind") in preferred_kinds:
+1 -1
View File
@@ -6,6 +6,7 @@ from collections.abc import Iterable, Iterator, Reversible
from contextlib import contextmanager
from typing import TYPE_CHECKING, Generic, Optional, TypeVar, cast
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
from serena.symbol import JetBrainsSymbol, LanguageServerSymbol, LanguageServerSymbolRetriever, PositionInFile, Symbol
from solidlsp import SolidLanguageServer, ls_types
from solidlsp.ls import LSPFileBuffer
@@ -13,7 +14,6 @@ from solidlsp.ls_utils import PathUtils, TextUtils
from .constants import DEFAULT_SOURCE_FILE_ENCODING
from .project import Project
from .tools.jetbrains_plugin_client import JetBrainsPluginClient
if TYPE_CHECKING:
from .agent import SerenaAgent
@@ -12,10 +12,10 @@ import requests
from requests import Response
from sensai.util.string import ToStringMixin
import serena.tools.jetbrains_types as jb
import serena.jetbrains.jetbrains_types as jb
from serena.jetbrains.jetbrains_types import PluginStatusDTO
from serena.project import Project
from serena.text_utils import render_html
from serena.tools.jetbrains_types import PluginStatusDTO
from serena.util.version import Version
T = TypeVar("T")
@@ -1,4 +1,4 @@
from typing import NotRequired, TypedDict
from typing import Literal, NotRequired, TypedDict
class PluginStatusDTO(TypedDict):
@@ -30,6 +30,9 @@ class SymbolDTO(TypedDict):
num_usages: NotRequired[int]
SymbolDTOKey = Literal["name_path", "relative_path", "type", "body", "quick_info", "documentation", "text_range", "children", "num_usages"]
class SymbolCollectionResponse(TypedDict):
symbols: list[SymbolDTO]
+190 -51
View File
@@ -4,11 +4,11 @@ import os
from abc import ABC, abstractmethod
from collections.abc import Callable, Iterator, Sequence
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any, Self, Union
from typing import TYPE_CHECKING, Any, Generic, Literal, NotRequired, Self, TypedDict, TypeVar, Union
from sensai.util.string import ToStringMixin
import serena.tools.jetbrains_types as jb
import serena.jetbrains.jetbrains_types as jb
from solidlsp import SolidLanguageServer
from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol
from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation
@@ -212,14 +212,17 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
return []
def _tostring_additional_entries(self) -> dict[str, Any]:
return dict(name=self.name, kind=self.kind, num_children=len(self.symbol_root["children"]))
return dict(name=self.name, kind=self.symbol_kind_name, num_children=len(self.symbol_root["children"]))
@property
def name(self) -> str:
return self.symbol_root["name"]
@property
def kind(self) -> str:
def symbol_kind_name(self) -> str:
"""
:return: string representation of the symbol kind (name attribute of the `SymbolKind` enum item)
"""
return SymbolKind(self.symbol_kind).name
@property
@@ -395,67 +398,99 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
traverse(self)
return result
class OutputDict(TypedDict):
name_path: NotRequired[str]
name: NotRequired[str]
location: NotRequired[dict[str, Any]]
relative_path: NotRequired[str | None]
body_location: NotRequired[dict[str, Any]]
body: NotRequired[str | None]
kind: NotRequired[str]
"""
string representation of the symbol kind (name attribute of the `SymbolKind` enum item)
"""
children: NotRequired[list["LanguageServerSymbol.OutputDict"]]
OutputDictKey = Literal["name", "name_path", "relative_path", "location", "body_location", "body", "kind", "children"]
def to_dict(
self,
*,
name_path: bool = True,
name: bool = False,
kind: bool = False,
location: bool = False,
depth: int = 0,
include_body: bool = False,
include_children_body: bool = False,
include_relative_path: bool = True,
body: bool = False,
body_location: bool = False,
children_body: bool = False,
relative_path: bool = False,
child_inclusion_predicate: Callable[[Self], bool] | None = None,
) -> dict[str, Any]:
) -> OutputDict:
"""
Converts the symbol to a dictionary.
:param name_path: whether to include the name path of the symbol
:param name: whether to include the name of the symbol
:param kind: whether to include the kind of the symbol
:param location: whether to include the location of the symbol
:param depth: the depth up to which to include child symbols (0 = do not include children)
:param include_body: whether to include the body of the top-level symbol.
:param include_children_body: whether to also include the body of the children.
:param body: whether to include the body of the top-level symbol.
:param children_body: whether to also include the body of the children.
Note that the body of the children is part of the body of the parent symbol,
so there is usually no need to set this to True unless you want process the output
and pass the children without passing the parent body to the LM.
:param include_relative_path: whether to include the relative path of the symbol in the location
entry. Relative paths of the symbol's children are always excluded.
:param relative_path: whether to include the relative path of the symbol.
If `location` is True, this defines whether to include the path in the location entry.
If `location` is False, this defines whether to include the relative path as a top-level entry.
Relative paths of the symbol's children are always excluded.
:param child_inclusion_predicate: an optional predicate that decides whether a child symbol
should be included.
:return: a dictionary representation of the symbol
"""
result: dict[str, Any] = {"name": self.name, "name_path": self.get_name_path()}
result: LanguageServerSymbol.OutputDict = {}
if name_path:
result["name_path"] = self.get_name_path()
if name:
result["name"] = self.name
if kind:
result["kind"] = self.kind
result["kind"] = self.symbol_kind_name
if location:
result["location"] = self.location.to_dict(include_relative_path=include_relative_path)
result["location"] = self.location.to_dict(include_relative_path=relative_path)
elif relative_path:
result["relative_path"] = self.relative_path
if body_location:
body_start_line, body_end_line = self.get_body_line_numbers()
result["body_location"] = {"start_line": body_start_line, "end_line": body_end_line}
if include_body:
body = self.body
if body is None:
log.warning("Requested body for symbol, but it is not present. The symbol might have been loaded with include_body=False.")
result["body"] = body
if body:
result["body"] = self.body
if child_inclusion_predicate is None:
child_inclusion_predicate = lambda s: True
def included_children(s: Self) -> list[dict[str, Any]]:
def included_children(s: Self) -> list[LanguageServerSymbol.OutputDict]:
children = []
for c in s.iter_children():
if not child_inclusion_predicate(c):
continue
children.append(
c.to_dict(
name_path=name_path,
name=name,
kind=kind,
location=location,
body_location=body_location,
depth=depth - 1,
child_inclusion_predicate=child_inclusion_predicate,
include_body=include_children_body,
include_children_body=include_children_body,
body=children_body,
children_body=children_body,
# all children have the same relative path as the parent
include_relative_path=False,
relative_path=False,
)
)
return children
@@ -463,7 +498,7 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
if depth > 0:
children = included_children(self)
if len(children) > 0:
result["children"] = included_children(self)
result["children"] = children
return result
@@ -604,8 +639,7 @@ class LanguageServerSymbolRetriever:
include_rel_path = within_relative_path is not None
raise ValueError(
f"Found multiple {len(symbol_candidates)} symbols matching '{name_path_pattern}'. "
"They are: \n"
+ json.dumps([s.to_dict(kind=True, include_relative_path=include_rel_path) for s in symbol_candidates], indent=2)
"They are: \n" + json.dumps([s.to_dict(kind=True, relative_path=include_rel_path) for s in symbol_candidates], indent=2)
)
def find_by_location(self, location: LanguageServerSymbolLocation) -> LanguageServerSymbol | None:
@@ -689,36 +723,15 @@ class LanguageServerSymbolRetriever:
return [ReferenceInLanguageServerSymbol.from_lsp_reference(r) for r in references]
def get_symbol_overview(self, relative_path: str, depth: int = 0) -> dict[str, list[dict]]:
def get_symbol_overview(self, relative_path: str) -> dict[str, list[LanguageServerSymbol]]:
"""
:param relative_path: the path of the file or directory for which to get the symbol overview
:param depth: the depth up to which to include child symbols (0 = only top-level symbols)
:return: a mapping from file paths to lists of symbol dictionaries.
:return: a mapping from file paths to lists of symbols.
For the case where a file is passed, the mapping will contain a single entry.
"""
lang_server = self.get_language_server(relative_path)
path_to_unified_symbols = lang_server.request_overview(relative_path)
def child_inclusion_predicate(s: LanguageServerSymbol) -> bool:
return not s.is_low_level()
result = {}
for file_path, unified_symbols in path_to_unified_symbols.items():
symbols_in_file = []
for us in unified_symbols:
symbol = LanguageServerSymbol(us)
symbols_in_file.append(
symbol.to_dict(
depth=depth,
kind=True,
include_relative_path=False,
location=False,
child_inclusion_predicate=child_inclusion_predicate,
)
)
result[file_path] = symbols_in_file
return result
return {k: [LanguageServerSymbol(us) for us in v] for k, v in path_to_unified_symbols.items()}
class JetBrainsSymbol(Symbol):
@@ -775,3 +788,129 @@ class JetBrainsSymbol(Symbol):
def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
# NOTE: Symbol types cannot really be differentiated, because types are not handled in a language-agnostic way.
return False
TSymbolDict = TypeVar("TSymbolDict")
GroupedSymbolDict = dict[str, list[dict] | dict[str, dict]]
class SymbolDictGrouper(Generic[TSymbolDict], ABC):
"""
A utility class for grouping a list of symbol dictionaries by one or more specified keys.
If an instance is statically initialised (upon module import), then this establishes a guarantee
that the specified keys are defined in the symbol dictionary type, ensuring at least basic type safety.
The respective ValueError will immediately be apparent.
"""
def __init__(
self,
symbol_dict_type: type[TSymbolDict],
children_key: Any,
group_keys: list[Any],
group_children_keys: list[Any],
collapse_singleton: bool,
) -> None:
"""
:param symbol_dict_type: the TypedDict type that represents the type of the symbol dictionaries to be grouped
:param children_key: the key in the symbol dictionaries that contains the list of child symbols (for recursive grouping).
:param group_keys: keys by which to group the symbol dictionaries. Must be a subset of the keys of `symbol_dict_type`.
:param group_children_keys: keys by which to group the child symbol dictionaries. Must be a subset of the keys of `symbol_dict_type`.
:param collapse_singleton: whether to collapse dictionaries containing a single entry after regrouping to just the entry's value
"""
# check whether the type contains all the keys specified in `keys` and raise an error if not.
if not hasattr(symbol_dict_type, "__annotations__"):
raise ValueError(f"symbol_dict_type must be a TypedDict type, got {symbol_dict_type}")
symbol_dict_keys = set(symbol_dict_type.__annotations__.keys())
for key in group_keys + [children_key] + group_children_keys:
if key not in symbol_dict_keys:
raise ValueError(f"symbol_dict_type {symbol_dict_type} does not contain key '{key}'")
self._children_key = children_key
self._group_keys = group_keys
self._group_children_keys = group_children_keys
self._collapse_singleton = collapse_singleton
def _group_by(self, l: list[dict], keys: list[str], children_keys: list[str]) -> dict[str, Any]:
assert len(keys) > 0, "keys must not be empty"
# group by the first key
grouped: dict[str, Any] = {}
for item in l:
key_value = item.pop(keys[0], "unknown")
if key_value not in grouped:
grouped[key_value] = []
grouped[key_value].append(item)
if len(keys) > 1:
# continue grouping by the remaining keys
for k, group in grouped.items():
grouped[k] = self._group_by(group, keys[1:], children_keys)
else:
# grouping is complete; now group the children if necessary
if children_keys:
for k, group in grouped.items():
for item in group:
if self._children_key in item:
children = item[self._children_key]
item[self._children_key] = self._group_by(children, children_keys, children_keys)
# post-process final group items
grouped = {k: [self._transform_item(i) for i in v] for k, v in grouped.items()}
return grouped
def _transform_item(self, item: dict) -> dict:
"""
Post-processes a final group item (which has been regrouped, i.e. some keys may have been removed),
collapsing singleton items (and items containing only a single non-children key)
"""
if self._collapse_singleton:
if len(item) == 1:
# {"name": "foo"} -> "foo"
# if there is only a single entry, collapse the dictionary to just the value of that entry
return next(iter(item.values()))
elif len(item) == 2 and self._children_key in item:
# {"name": "foo", "children": {...}} -> {"foo": {...}}
# if there are exactly two entries and one of them is the children key,
# convert to {other_value: children}
other_key = next(k for k in item.keys() if k != self._children_key)
new_item = {item[other_key]: item[self._children_key]}
return new_item
return item
def group(self, symbols: list[TSymbolDict]) -> GroupedSymbolDict:
"""
:param symbols: the symbols to group
:return: dictionary with the symbols grouped as defined at construction
"""
return self._group_by(symbols, self._group_keys, self._group_children_keys) # type: ignore
class LanguageServerSymbolDictGrouper(SymbolDictGrouper[LanguageServerSymbol.OutputDict]):
def __init__(
self,
group_keys: list[LanguageServerSymbol.OutputDictKey],
group_children_keys: list[LanguageServerSymbol.OutputDictKey],
collapse_singleton: bool = False,
) -> None:
super().__init__(LanguageServerSymbol.OutputDict, "children", group_keys, group_children_keys, collapse_singleton)
class JetBrainsSymbolDictGrouper(SymbolDictGrouper[jb.SymbolDTO]):
def __init__(
self,
group_keys: list[jb.SymbolDTOKey],
group_children_keys: list[jb.SymbolDTOKey],
collapse_singleton: bool = False,
map_name_path_to_name: bool = False,
) -> None:
super().__init__(jb.SymbolDTO, "children", group_keys, group_children_keys, collapse_singleton)
self._map_name_path_to_name = map_name_path_to_name
def _transform_item(self, item: dict) -> dict:
if self._map_name_path_to_name:
# {"name_path: "Class/myMethod"} -> {"name: "myMethod"}
new_item = dict(item)
if "name_path" in item:
name_path = new_item.pop("name_path")
new_item["name"] = name_path.split("/")[-1]
return super()._transform_item(new_item)
else:
return super()._transform_item(item)
+11 -37
View File
@@ -1,10 +1,10 @@
import logging
from collections import defaultdict
from typing import Any, Literal
import serena.tools.jetbrains_types as jb
import serena.jetbrains.jetbrains_types as jb
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
from serena.symbol import JetBrainsSymbolDictGrouper
from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead
from serena.tools.jetbrains_plugin_client import JetBrainsPluginClient
log = logging.getLogger(__name__)
@@ -91,6 +91,8 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark
Finds symbols that reference the given symbol using the JetBrains backend
"""
symbol_dict_grouper = JetBrainsSymbolDictGrouper(["relative_path", "type"], ["type"], collapse_singleton=True)
# TODO: (maybe) - add content snippets showing the references like in LS based version?
def apply(
self,
@@ -118,8 +120,10 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark
relative_path=relative_path,
include_quick_info=False, # TODO: Hotfix for serena-jetbrains-plugin/issues/13; revert once fixed
)
result = self._to_json(response_dict)
return self._limit_length(result, max_answer_chars)
symbol_dicts = response_dict["symbols"]
result = self.symbol_dict_grouper.group(symbol_dicts)
result_json = self._to_json(result)
return self._limit_length(result_json, max_answer_chars)
class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional):
@@ -128,37 +132,7 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp
"""
USE_COMPACT_FORMAT = True
@staticmethod
def _transform_symbols_to_compact_format(symbols: list[jb.SymbolDTO]) -> dict[str, list]:
"""
Transform symbol overview from verbose format to compact grouped format.
Groups symbols by kind and uses names instead of full symbol objects.
For symbols with children, creates nested dictionaries.
The name_path can be inferred from the hierarchical structure:
- Top-level symbols: name_path = name
- Nested symbols: name_path = parent_name + "/" + name
For example, "convert" under class "ProjectType" has name_path "ProjectType/convert".
"""
result = defaultdict(list)
for symbol in symbols:
kind = symbol.get("type", "Unknown")
name_path = symbol["name_path"]
name = name_path.split("/")[-1]
children = symbol.get("children", [])
if children:
# Symbol has children: create nested dict {name: children_dict}
children_dict = JetBrainsGetSymbolsOverviewTool._transform_symbols_to_compact_format(children)
result[kind].append({name: children_dict})
else:
# Symbol has no children: just add the name
result[kind].append(name) # type: ignore
return result
symbol_dict_grouper = JetBrainsSymbolDictGrouper(["type"], ["type"], collapse_singleton=True, map_name_path_to_name=True)
def apply(
self,
@@ -187,7 +161,7 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp
)
if self.USE_COMPACT_FORMAT:
symbols = symbol_overview["symbols"]
result: dict[str, Any] = {"symbols": self._transform_symbols_to_compact_format(symbols)}
result: dict[str, Any] = {"symbols": self.symbol_dict_grouper.group(symbols)}
documentation = symbol_overview.pop("documentation", None)
if documentation:
result["docstring"] = documentation
+33 -54
View File
@@ -3,11 +3,9 @@ Language server-related tools
"""
import os
from collections import defaultdict
from collections.abc import Sequence
from copy import copy
from typing import Any
from serena.symbol import LanguageServerSymbol, LanguageServerSymbolDictGrouper
from serena.tools import (
SUCCESS_RESULT,
Tool,
@@ -18,22 +16,6 @@ from serena.tools.tools_base import ToolMarkerOptional
from solidlsp.ls_types import SymbolKind
def _sanitize_symbol_dict(symbol_dict: dict[str, Any]) -> dict[str, Any]:
"""
Sanitize a symbol dictionary inplace by removing unnecessary information.
"""
# We replace the location entry, which repeats line information already included in body_location
# and has unnecessary information on column, by just the relative path.
symbol_dict = copy(symbol_dict)
s_relative_path = symbol_dict.get("location", {}).get("relative_path")
if s_relative_path is not None:
symbol_dict["relative_path"] = s_relative_path
symbol_dict.pop("location", None)
# also remove name, name_path should be enough
symbol_dict.pop("name")
return symbol_dict
class RestartLanguageServerTool(Tool, ToolMarkerOptional):
"""Restarts the language server, may be necessary when edits not through Serena happen."""
@@ -50,6 +32,8 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
Gets an overview of the top-level symbols defined in a given file.
"""
symbol_dict_grouper = LanguageServerSymbolDictGrouper(["kind"], ["kind"], collapse_singleton=True)
def apply(self, relative_path: str, depth: int = 0, max_answer_chars: int = -1) -> str:
"""
Use this tool to get a high-level understanding of the code symbols in a file.
@@ -65,11 +49,11 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
:return: a JSON object containing symbols grouped by kind in a compact format.
"""
result = self.get_symbol_overview(relative_path, depth=depth)
compact_result = self._transform_symbols_to_compact_format(result)
compact_result = self.symbol_dict_grouper.group(result)
result_json_str = self._to_json(compact_result)
return self._limit_length(result_json_str, max_answer_chars)
def get_symbol_overview(self, relative_path: str, depth: int = 0) -> list[dict]:
def get_symbol_overview(self, relative_path: str, depth: int = 0) -> list[LanguageServerSymbol.OutputDict]:
"""
:param relative_path: relative path to a source file
:param depth: the depth up to which descendants shall be retrieved
@@ -85,37 +69,25 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
if os.path.isdir(file_path):
raise ValueError(f"Expected a file path, but got a directory path: {relative_path}. ")
return symbol_retriever.get_symbol_overview(relative_path, depth=depth)[relative_path]
symbols = symbol_retriever.get_symbol_overview(relative_path)[relative_path]
@staticmethod
def _transform_symbols_to_compact_format(symbols: list[dict[str, Any]]) -> dict[str, list]:
"""
Transform symbol overview from verbose format to compact grouped format.
Groups symbols by kind and uses names instead of full symbol objects.
For symbols with children, creates nested dictionaries.
The name_path can be inferred from the hierarchical structure:
- Top-level symbols: name_path = name
- Nested symbols: name_path = parent_name + "/" + name
For example, "convert" under class "ProjectType" has name_path "ProjectType/convert".
"""
result = defaultdict(list)
def child_inclusion_predicate(s: LanguageServerSymbol) -> bool:
return not s.is_low_level()
symbol_dicts = []
for symbol in symbols:
kind = symbol.get("kind", "Unknown")
name = symbol.get("name", "unknown")
children = symbol.get("children", [])
if children:
# Symbol has children: create nested dict {name: children_dict}
children_dict = GetSymbolsOverviewTool._transform_symbols_to_compact_format(children)
result[kind].append({name: children_dict})
else:
# Symbol has no children: just add the name
result[kind].append(name)
return result
symbol_dicts.append(
symbol.to_dict(
name_path=False,
name=True,
depth=depth,
kind=True,
relative_path=False,
location=False,
child_inclusion_predicate=child_inclusion_predicate,
)
)
return symbol_dicts
class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
@@ -185,7 +157,7 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
substring_matching=substring_matching,
within_relative_path=relative_path,
)
symbol_dicts = [_sanitize_symbol_dict(s.to_dict(kind=True, location=True, depth=depth, include_body=include_body)) for s in symbols]
symbol_dicts = [dict(s.to_dict(kind=True, relative_path=True, body_location=True, depth=depth, body=include_body)) for s in symbols]
if not include_body and include_info:
# we add an info field to the symbol dicts if requested
for s, s_dict in zip(symbols, symbol_dicts, strict=True):
@@ -201,6 +173,8 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
Finds symbols that reference the given symbol using the language server backend
"""
symbol_dict_grouper = LanguageServerSymbolDictGrouper(["relative_path", "kind"], ["kind"], collapse_singleton=True)
# noinspection PyDefaultArgument
def apply(
self,
@@ -229,6 +203,7 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
parsed_include_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in include_kinds] if include_kinds else None
parsed_exclude_kinds: Sequence[SymbolKind] | None = [SymbolKind(k) for k in exclude_kinds] if exclude_kinds else None
symbol_retriever = self.create_language_server_symbol_retriever()
references_in_symbols = symbol_retriever.find_referencing_symbols(
name_path,
relative_file_path=relative_path,
@@ -236,10 +211,11 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
include_kinds=parsed_include_kinds,
exclude_kinds=parsed_exclude_kinds,
)
reference_dicts = []
for ref in references_in_symbols:
ref_dict = ref.symbol.to_dict(kind=True, location=True, depth=0, include_body=include_body)
ref_dict = _sanitize_symbol_dict(ref_dict)
ref_dict_orig = ref.symbol.to_dict(kind=True, relative_path=True, depth=0, body=include_body, body_location=True)
ref_dict = dict(ref_dict_orig)
if not include_body:
ref_relative_path = ref.symbol.location.relative_path
assert ref_relative_path is not None, f"Referencing symbol {ref.symbol.name} has no relative path, this is likely a bug."
@@ -250,8 +226,11 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
)
ref_dict["content_around_reference"] = content_around_ref.to_display_string()
reference_dicts.append(ref_dict)
result = self._to_json(reference_dicts)
return self._limit_length(result, max_answer_chars)
result = self.symbol_dict_grouper.group(reference_dicts) # type: ignore
result_json = self._to_json(result)
return self._limit_length(result_json, max_answer_chars)
class ReplaceSymbolBodyTool(Tool, ToolMarkerSymbolicEdit):
+19 -5
View File
@@ -209,7 +209,7 @@ class TestSerenaAgent:
# Find the symbol location first
find_symbol_tool = agent.get_tool(FindSymbolTool)
result = find_symbol_tool.apply_ex(name_path_pattern=symbol_name, relative_path=def_file)
result = find_symbol_tool.apply(name_path_pattern=symbol_name, relative_path=def_file)
time.sleep(1)
symbols = json.loads(result)
@@ -218,12 +218,26 @@ class TestSerenaAgent:
# Now find references
find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
result = find_refs_tool.apply_ex(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"])
result = find_refs_tool.apply(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"])
def contains_ref_with_relative_path(refs, relative_path):
"""
Checks for reference to relative path, regardless of output format (grouped an ungrouped)
"""
if isinstance(refs, list):
for ref in refs:
if contains_ref_with_relative_path(ref, relative_path):
return True
elif isinstance(refs, dict):
if relative_path in refs:
return True
for value in refs.values():
if contains_ref_with_relative_path(value, relative_path):
return True
return False
refs = json.loads(result)
assert any(
ref["relative_path"] == ref_file for ref in refs
), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}"
assert contains_ref_with_relative_path(refs, ref_file), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}"
@pytest.mark.parametrize(
"serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file",
+23 -1
View File
@@ -1,6 +1,7 @@
import pytest
from serena.symbol import LanguageServerSymbolRetriever, NamePathComponent, NamePathMatcher
from serena.jetbrains.jetbrains_types import SymbolDTO, SymbolDTOKey
from serena.symbol import LanguageServerSymbol, LanguageServerSymbolRetriever, NamePathComponent, NamePathMatcher
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
@@ -238,3 +239,24 @@ class TestLanguageServerSymbolRetriever:
create_user_method_symbol = symbol_retriever.find("UserService/create_user", within_relative_path="test_repo/services.py")[0]
create_user_method_symbol_info = symbol_retriever.request_info_for_symbol(create_user_method_symbol)
assert "Create a new user and store it" in create_user_method_symbol_info
class TestSymbolDictTypes:
@staticmethod
def check_key_type(dict_type: type, key_type: type):
"""
:param dict_type: a TypedDict type
:param key_type: the corresponding key type (Literal[...]) that the dict should have for keys
"""
dict_type_keys = dict_type.__annotations__.keys()
assert len(dict_type_keys) == len(
key_type.__args__ # type: ignore
), f"Expected {len(key_type.__args__)} keys in {dict_type}, but got {len(dict_type_keys)}" # type: ignore
for expected_key in key_type.__args__: # type: ignore
assert expected_key in dict_type_keys, f"Expected key '{expected_key}' not found in {dict_type}"
def test_ls_symbol_dict_type(self):
self.check_key_type(LanguageServerSymbol.OutputDict, LanguageServerSymbol.OutputDictKey)
def test_jb_symbol_dict_type(self):
self.check_key_type(SymbolDTO, SymbolDTOKey)
@@ -125,6 +125,7 @@ class TestFSharpLanguageServer:
# We should get at least some definitions
assert len(definitions) >= 0, "Should get definitions (even if empty for complex cases)"
@pytest.mark.skipif(is_ci, reason="Test is flaky") # TODO: Re-enable if the LS can be made more reliable #1039
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
def test_hover_information(self, language_server: SolidLanguageServer) -> None:
"""Test hover information functionality."""
+2
View File
@@ -14,6 +14,7 @@ from serena.project import Project
from serena.text_utils import LineType
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import is_ci
# Skip Swift tests on Windows due to complex GitHub Actions configuration
WINDOWS_SKIP = platform.system() == "Windows"
@@ -99,6 +100,7 @@ class TestSwiftLanguageServerBasics:
utils_def = definitions[0]
assert utils_def.get("uri", "").endswith("utils.swift"), "Definition should be in utils.swift"
@pytest.mark.skipif(is_ci, reason="Test is flaky in CI")
@pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True)
def test_request_references_calculator_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_references on the Calculator class."""