mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 04:15:11 +00:00
Add overload_idx to name path matching, reifying NamePathMatcher for efficency
Implements #515
This commit is contained in:
1 parent
55b4a665fd
commit
d31cfab25d
2 files changed
+76
-34
No files matched your search
+57
-31
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
||||
from .agent import SerenaAgent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
NAME_PATH_SEP = "/"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -113,45 +114,65 @@ class Symbol(ToStringMixin, ABC):
|
||||
"""
|
||||
|
||||
|
||||
class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
_NAME_PATH_SEP = "/"
|
||||
|
||||
@staticmethod
|
||||
def match_name_path(
|
||||
name_path: str,
|
||||
symbol_name_path_parts: list[str],
|
||||
substring_matching: bool,
|
||||
) -> bool:
|
||||
class NamePathMatcher(ToStringMixin):
|
||||
def __init__(self, name_path_expr: str, substring_matching: bool) -> None:
|
||||
"""
|
||||
Checks if a given `name_path` matches a symbol's qualified name parts.
|
||||
See docstring of `Symbol.find` for more details.
|
||||
:param name_path_expr: the name path expression to match against
|
||||
:param substring_matching: whether to use substring matching for the last segment
|
||||
"""
|
||||
assert name_path, "name_path must not be empty"
|
||||
assert symbol_name_path_parts, "symbol_name_path_parts must not be empty"
|
||||
name_path_sep = LanguageServerSymbol._NAME_PATH_SEP
|
||||
assert name_path_expr, "name_path must not be empty"
|
||||
self._expr = name_path_expr
|
||||
self._substring_matching = substring_matching
|
||||
self._is_absolute_pattern = name_path_expr.startswith(NAME_PATH_SEP)
|
||||
self._pattern_parts = name_path_expr.lstrip(NAME_PATH_SEP).rstrip(NAME_PATH_SEP).split(NAME_PATH_SEP)
|
||||
|
||||
is_absolute_pattern = name_path.startswith(name_path_sep)
|
||||
pattern_parts = name_path.lstrip(name_path_sep).rstrip(name_path_sep).split(name_path_sep)
|
||||
# extract overload index "[idx]" if present at end of last part
|
||||
self._overload_idx: int | None = None
|
||||
last_part = self._pattern_parts[-1]
|
||||
if last_part.endswith("]") and "[" in last_part:
|
||||
bracket_idx = last_part.rfind("[")
|
||||
index_part = last_part[bracket_idx + 1 : -1]
|
||||
if index_part.isdigit():
|
||||
self._pattern_parts[-1] = last_part[:bracket_idx]
|
||||
self._overload_idx = int(index_part)
|
||||
|
||||
def _tostring_includes(self) -> list[str]:
|
||||
return ["_expr"]
|
||||
|
||||
def matches_ls_symbol(self, symbol: "LanguageServerSymbol") -> bool:
|
||||
return self.matches_components(symbol.get_name_path_parts(), symbol.overload_idx)
|
||||
|
||||
def matches_components(self, symbol_name_path_parts: list[str], overload_idx: int | None) -> bool:
|
||||
# filtering based on ancestors
|
||||
if len(pattern_parts) > len(symbol_name_path_parts):
|
||||
if len(self._pattern_parts) > len(symbol_name_path_parts):
|
||||
# can't possibly match if pattern has more parts than symbol
|
||||
return False
|
||||
if is_absolute_pattern and len(pattern_parts) != len(symbol_name_path_parts):
|
||||
if self._is_absolute_pattern and len(self._pattern_parts) != len(symbol_name_path_parts):
|
||||
# for absolute patterns, the number of parts must match exactly
|
||||
return False
|
||||
if symbol_name_path_parts[-len(pattern_parts) : -1] != pattern_parts[:-1]:
|
||||
if symbol_name_path_parts[-len(self._pattern_parts) : -1] != self._pattern_parts[:-1]:
|
||||
# ancestors must match
|
||||
return False
|
||||
|
||||
# matching the last part of the symbol name
|
||||
name_to_match = pattern_parts[-1]
|
||||
name_to_match = self._pattern_parts[-1]
|
||||
symbol_name = symbol_name_path_parts[-1]
|
||||
if substring_matching:
|
||||
return name_to_match in symbol_name
|
||||
if self._substring_matching:
|
||||
if name_to_match not in symbol_name:
|
||||
return False
|
||||
else:
|
||||
return name_to_match == symbol_name
|
||||
if name_to_match != symbol_name:
|
||||
return False
|
||||
|
||||
# check for matching overload index
|
||||
if self._overload_idx is not None:
|
||||
if overload_idx != self._overload_idx:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
def __init__(self, symbol_root_from_ls: UnifiedSymbolInformation) -> None:
|
||||
self.symbol_root = symbol_root_from_ls
|
||||
|
||||
@@ -173,6 +194,10 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
def symbol_kind(self) -> SymbolKind:
|
||||
return self.symbol_root["kind"]
|
||||
|
||||
@property
|
||||
def overload_idx(self) -> int | None:
|
||||
return self.symbol_root.get("overload_idx")
|
||||
|
||||
def is_neighbouring_definition_separated_by_empty_line(self) -> bool:
|
||||
return self.symbol_kind in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Class, SymbolKind.Interface, SymbolKind.Struct)
|
||||
|
||||
@@ -256,9 +281,13 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
|
||||
def get_name_path(self) -> str:
|
||||
"""
|
||||
Get the name path of the symbol (e.g. "class/method/inner_function").
|
||||
Get the name path of the symbol, e.g. "class/method/inner_function" or
|
||||
"class/method[1]" (overloaded method with identifying index).
|
||||
"""
|
||||
return self._NAME_PATH_SEP.join(self.get_name_path_parts())
|
||||
name_path = NAME_PATH_SEP.join(self.get_name_path_parts())
|
||||
if "overload_idx" in self.symbol_root:
|
||||
name_path += f"[{self.symbol_root['overload_idx']}]"
|
||||
return name_path
|
||||
|
||||
def get_name_path_parts(self) -> list[str]:
|
||||
"""
|
||||
@@ -321,7 +350,7 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
For example, passing `/class` will match only against top-level symbols like `class` but not against `nested_class/class`.
|
||||
Passing `/class/method` will match against `class/method` but not `nested_class/class/method` or `method`.
|
||||
|
||||
:param name_path: the name path to match against
|
||||
:param name_path: the name path expression to match against
|
||||
:param substring_matching: whether to use substring matching (as opposed to exact matching)
|
||||
of the last segment of `name_path` against the symbol name.
|
||||
:param include_kinds: an optional sequence of ints representing the LSP symbol kind.
|
||||
@@ -329,17 +358,14 @@ class LanguageServerSymbol(Symbol, ToStringMixin):
|
||||
:param exclude_kinds: If provided, symbols of the given kinds will be excluded from the result.
|
||||
"""
|
||||
result = []
|
||||
name_path_matcher = NamePathMatcher(name_path, substring_matching)
|
||||
|
||||
def should_include(s: "LanguageServerSymbol") -> bool:
|
||||
if include_kinds is not None and s.symbol_kind not in include_kinds:
|
||||
return False
|
||||
if exclude_kinds is not None and s.symbol_kind in exclude_kinds:
|
||||
return False
|
||||
return LanguageServerSymbol.match_name_path(
|
||||
name_path=name_path,
|
||||
symbol_name_path_parts=s.get_name_path_parts(),
|
||||
substring_matching=substring_matching,
|
||||
)
|
||||
return name_path_matcher.matches_ls_symbol(s)
|
||||
|
||||
def traverse(s: "LanguageServerSymbol") -> None:
|
||||
if should_include(s):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from src.serena.symbol import LanguageServerSymbol
|
||||
from serena.symbol import NamePathMatcher
|
||||
|
||||
|
||||
class TestSymbolNameMatching:
|
||||
@@ -77,7 +77,7 @@ class TestSymbolNameMatching:
|
||||
)
|
||||
def test_match_simple_name(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected):
|
||||
"""Tests matching for simple names (no '/' in pattern)."""
|
||||
result = LanguageServerSymbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match)
|
||||
result = NamePathMatcher(name_path_pattern, is_substring_match).matches_components(symbol_name_path_parts, None)
|
||||
error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result)
|
||||
assert result == expected, error_msg
|
||||
|
||||
@@ -157,6 +157,22 @@ class TestSymbolNameMatching:
|
||||
)
|
||||
def test_match_name_path_pattern_path_len_2(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected):
|
||||
"""Tests matching for qualified names (e.g. 'module/class/func')."""
|
||||
result = LanguageServerSymbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match)
|
||||
result = NamePathMatcher(name_path_pattern, is_substring_match).matches_components(symbol_name_path_parts, None)
|
||||
error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result)
|
||||
assert result == expected, error_msg
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name_path_pattern, symbol_name_path_parts, symbol_overload_idx, expected",
|
||||
[
|
||||
pytest.param("bar/foo", ["bar", "foo"], 0, True, id="R: 'bar/foo' matches ['bar', 'foo'] with overload_index=0"),
|
||||
pytest.param("bar/foo", ["bar", "foo"], 1, True, id="R: 'bar/foo' matches ['bar', 'foo'] with overload_index=1"),
|
||||
pytest.param("bar/foo[0]", ["bar", "foo"], 0, True, id="R: 'bar/foo[0]' matches ['bar', 'foo'] with overload_index=0"),
|
||||
pytest.param("bar/foo[1]", ["bar", "foo"], 0, False, id="R: 'bar/foo[1]' does not match ['bar', 'foo'] with overload_index=0"),
|
||||
],
|
||||
)
|
||||
def test_match_name_path_pattern_with_overload_idx(self, name_path_pattern, symbol_name_path_parts, symbol_overload_idx, expected):
|
||||
"""Tests matching for qualified names (e.g. 'module/class/func')."""
|
||||
matcher = NamePathMatcher(name_path_pattern, False)
|
||||
result = matcher.matches_components(symbol_name_path_parts, symbol_overload_idx)
|
||||
error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, False, expected, result)
|
||||
assert result == expected, error_msg
|
||||
Reference in new issue
Block a user