mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 16:15:29 +00:00
Apply formatter
This commit is contained in:
1 parent
ec1277167b
commit
f2d81812c7
4 files changed
+107
-100
No files matched your search
@@ -9,8 +9,7 @@ import pathlib
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
@@ -20,8 +19,6 @@ from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
from solidlsp import ls_types
|
||||
|
||||
class BashLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
|
||||
@@ -47,8 +44,6 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
self.server_ready = threading.Event()
|
||||
self.initialize_searcher_command_available = threading.Event()
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def _setup_runtime_dependencies(
|
||||
cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
|
||||
@@ -180,7 +175,7 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
# Enhanced capability checks for bash-language-server 5.6.0
|
||||
assert init_response["capabilities"]["textDocumentSync"] in [1, 2] # Full or Incremental
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
|
||||
|
||||
# Verify document symbol support is available
|
||||
if "documentSymbolProvider" in init_response["capabilities"]:
|
||||
self.logger.log("Bash server supports document symbols", logging.INFO)
|
||||
@@ -204,139 +199,138 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
Enhanced document symbol request with hybrid LSP + regex-based function detection for bash files.
|
||||
|
||||
|
||||
This method combines both LSP-based detection and regex-based detection to provide comprehensive
|
||||
function discovery. This dual approach is necessary because:
|
||||
|
||||
|
||||
1. bash-language-server (v5.6.0) has inconsistent function detection capabilities
|
||||
2. Some bash function syntaxes are not reliably detected by the LSP server
|
||||
3. Files may contain mixed function notation styles within the same file
|
||||
4. Different formatting or indentation can affect LSP detection
|
||||
|
||||
|
||||
The hybrid approach ensures maximum compatibility and comprehensive function discovery
|
||||
for reliable symbolic editing operations in Serena.
|
||||
"""
|
||||
# First get symbols from the standard LSP approach
|
||||
lsp_all_symbols, lsp_root_symbols = super().request_document_symbols(relative_file_path, include_body)
|
||||
|
||||
|
||||
# Always run regex-based function detection for comprehensive coverage
|
||||
# This addresses bash-language-server limitations and ensures we catch all function patterns
|
||||
self.logger.log(f"Running hybrid function detection (LSP + regex) for {relative_file_path}", logging.DEBUG)
|
||||
|
||||
|
||||
regex_detected_functions = self._detect_bash_functions(relative_file_path, include_body)
|
||||
|
||||
|
||||
# Merge LSP and regex results, avoiding duplicates
|
||||
merged_all_symbols, merged_root_symbols = self._merge_function_detections(
|
||||
lsp_all_symbols, lsp_root_symbols, regex_detected_functions
|
||||
)
|
||||
|
||||
|
||||
# Log detection results for debugging
|
||||
lsp_functions = [s for s in lsp_all_symbols if s.get("kind") == 12]
|
||||
total_functions = [s for s in merged_all_symbols if s.get("kind") == 12]
|
||||
|
||||
|
||||
self.logger.log(
|
||||
f"Function detection for {relative_file_path}: LSP={len(lsp_functions)}, "
|
||||
f"Regex={len(regex_detected_functions)}, Total={len(total_functions)}",
|
||||
logging.INFO
|
||||
f"Regex={len(regex_detected_functions)}, Total={len(total_functions)}",
|
||||
logging.INFO,
|
||||
)
|
||||
|
||||
|
||||
return merged_all_symbols, merged_root_symbols
|
||||
|
||||
def _merge_function_detections(
|
||||
self,
|
||||
lsp_all_symbols: list[ls_types.UnifiedSymbolInformation],
|
||||
self,
|
||||
lsp_all_symbols: list[ls_types.UnifiedSymbolInformation],
|
||||
lsp_root_symbols: list[ls_types.UnifiedSymbolInformation],
|
||||
regex_detected_functions: list[ls_types.UnifiedSymbolInformation]
|
||||
regex_detected_functions: list[ls_types.UnifiedSymbolInformation],
|
||||
) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
|
||||
"""
|
||||
Merge LSP-detected symbols with regex-detected functions, avoiding duplicates.
|
||||
|
||||
|
||||
This method:
|
||||
1. Keeps all non-function symbols from LSP detection
|
||||
2. Keeps all LSP-detected functions (they have more accurate positioning)
|
||||
3. Adds regex-detected functions that weren't found by LSP
|
||||
4. Uses function names to detect duplicates
|
||||
|
||||
|
||||
Args:
|
||||
lsp_all_symbols: All symbols detected by LSP
|
||||
lsp_root_symbols: Root-level symbols detected by LSP
|
||||
regex_detected_functions: Functions detected by regex (all are root-level)
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (merged_all_symbols, merged_root_symbols)
|
||||
|
||||
"""
|
||||
# Extract function names that LSP already detected
|
||||
lsp_function_names = set()
|
||||
for symbol in lsp_all_symbols:
|
||||
if symbol.get("kind") == 12: # LSP Symbol Kind 12 = Function
|
||||
lsp_function_names.add(symbol["name"])
|
||||
|
||||
|
||||
# Start with all LSP symbols (both functions and non-functions)
|
||||
merged_all_symbols = lsp_all_symbols.copy()
|
||||
merged_root_symbols = lsp_root_symbols.copy()
|
||||
|
||||
|
||||
# Add regex-detected functions that weren't found by LSP
|
||||
added_functions = 0
|
||||
for regex_function in regex_detected_functions:
|
||||
function_name = regex_function["name"]
|
||||
|
||||
|
||||
# Only add if this function wasn't detected by LSP
|
||||
if function_name not in lsp_function_names:
|
||||
merged_all_symbols.append(regex_function)
|
||||
merged_root_symbols.append(regex_function) # All regex functions are root-level
|
||||
added_functions += 1
|
||||
self.logger.log(f"Added regex-detected function '{function_name}' not found by LSP", logging.DEBUG)
|
||||
|
||||
|
||||
if added_functions > 0:
|
||||
self.logger.log(f"Merged {added_functions} additional functions from regex detection", logging.INFO)
|
||||
|
||||
|
||||
return merged_all_symbols, merged_root_symbols
|
||||
|
||||
|
||||
def _detect_bash_functions(self, relative_file_path: str, include_body: bool = False) -> list[ls_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Regex-based detection of bash functions as fallback when LSP doesn't provide them.
|
||||
"""
|
||||
import re
|
||||
|
||||
from solidlsp import ls_types
|
||||
|
||||
|
||||
try:
|
||||
# Read the file content directly from filesystem
|
||||
abs_path = os.path.join(self.repository_root_path, relative_file_path)
|
||||
with open(abs_path, 'r', encoding='utf-8') as f:
|
||||
with open(abs_path, encoding="utf-8") as f:
|
||||
file_content = f.read()
|
||||
lines = file_content.split("\n")
|
||||
|
||||
|
||||
detected_functions = []
|
||||
|
||||
|
||||
# Regex patterns for bash function definitions
|
||||
# Pattern 1: function name() { ... }
|
||||
# Pattern 2: name() { ... }
|
||||
function_patterns = [
|
||||
re.compile(r'^\s*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)\s*\{'),
|
||||
re.compile(r'^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)\s*\{')
|
||||
re.compile(r"^\s*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)\s*\{"),
|
||||
re.compile(r"^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*\)\s*\{"),
|
||||
]
|
||||
|
||||
|
||||
for line_num, line in enumerate(lines):
|
||||
for pattern in function_patterns:
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
func_name = match.group(1)
|
||||
|
||||
|
||||
# Find the end of the function by matching braces
|
||||
start_line = line_num
|
||||
end_line = self._find_function_end(lines, start_line)
|
||||
|
||||
|
||||
# Create location information
|
||||
location = ls_types.Location(
|
||||
uri=pathlib.Path(abs_path).as_uri(),
|
||||
range={
|
||||
"start": {"line": start_line, "character": match.start(1)},
|
||||
"end": {"line": end_line, "character": 0}
|
||||
},
|
||||
range={"start": {"line": start_line, "character": match.start(1)}, "end": {"line": end_line, "character": 0}},
|
||||
absolutePath=abs_path,
|
||||
relativePath=relative_file_path
|
||||
relativePath=relative_file_path,
|
||||
)
|
||||
|
||||
|
||||
# Create the function symbol
|
||||
func_symbol = ls_types.UnifiedSymbolInformation(
|
||||
name=func_name,
|
||||
@@ -345,44 +339,44 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
range=location["range"],
|
||||
selectionRange={
|
||||
"start": {"line": start_line, "character": match.start(1)},
|
||||
"end": {"line": start_line, "character": match.end(1)}
|
||||
"end": {"line": start_line, "character": match.end(1)},
|
||||
},
|
||||
children=[],
|
||||
parent=None
|
||||
parent=None,
|
||||
)
|
||||
|
||||
|
||||
# Add function body if requested
|
||||
if include_body:
|
||||
func_symbol["body"] = "\n".join(lines[start_line:end_line + 1])
|
||||
|
||||
func_symbol["body"] = "\n".join(lines[start_line : end_line + 1])
|
||||
|
||||
detected_functions.append(func_symbol)
|
||||
break
|
||||
|
||||
|
||||
return detected_functions
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error in regex-based function detection for {relative_file_path}: {e}", logging.WARNING)
|
||||
return []
|
||||
|
||||
|
||||
def _find_function_end(self, lines: list[str], start_line: int) -> int:
|
||||
"""
|
||||
Find the end line of a bash function by matching opening and closing braces.
|
||||
"""
|
||||
brace_count = 0
|
||||
in_function = False
|
||||
|
||||
|
||||
for i in range(start_line, len(lines)):
|
||||
line = lines[i]
|
||||
|
||||
|
||||
# Count braces, handling basic cases
|
||||
for char in line:
|
||||
if char == '{':
|
||||
if char == "{":
|
||||
brace_count += 1
|
||||
in_function = True
|
||||
elif char == '}' and in_function:
|
||||
elif char == "}" and in_function:
|
||||
brace_count -= 1
|
||||
if brace_count == 0:
|
||||
return i
|
||||
|
||||
|
||||
# Fallback: if we can't match braces properly, assume single line or small function
|
||||
return min(start_line + 10, len(lines) - 1)
|
||||
@@ -144,6 +144,3 @@ def project(request: LanguageParamRequest):
|
||||
|
||||
language = request.param
|
||||
yield create_default_project(language)
|
||||
|
||||
|
||||
|
||||
@@ -26,14 +26,14 @@ class TestBashLanguageServerBasics:
|
||||
"""Test request_document_symbols for bash files."""
|
||||
# Test getting symbols from main.sh
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("main.sh", include_body=False)
|
||||
|
||||
|
||||
# Extract function symbols (LSP Symbol Kind 12)
|
||||
function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
|
||||
function_names = [symbol["name"] for symbol in function_symbols]
|
||||
|
||||
|
||||
# Should detect all 3 functions from main.sh
|
||||
assert "greet_user" in function_names, "Should find greet_user function"
|
||||
assert "process_items" in function_names, "Should find process_items function"
|
||||
assert "process_items" in function_names, "Should find process_items function"
|
||||
assert "main" in function_names, "Should find main function"
|
||||
assert len(function_symbols) >= 3, f"Should find at least 3 functions, found {len(function_symbols)}"
|
||||
|
||||
@@ -42,13 +42,13 @@ class TestBashLanguageServerBasics:
|
||||
"""Test request_document_symbols with body extraction."""
|
||||
# Test with include_body=True
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("main.sh", include_body=True)
|
||||
|
||||
|
||||
function_symbols = [symbol for symbol in all_symbols if symbol.get("kind") == 12]
|
||||
|
||||
|
||||
# Find greet_user function and check it has body
|
||||
greet_user_symbol = next((sym for sym in function_symbols if sym["name"] == "greet_user"), None)
|
||||
assert greet_user_symbol is not None, "Should find greet_user function"
|
||||
|
||||
|
||||
if "body" in greet_user_symbol:
|
||||
body = greet_user_symbol["body"]
|
||||
assert "function greet_user()" in body, "Function body should contain function definition"
|
||||
@@ -58,47 +58,53 @@ class TestBashLanguageServerBasics:
|
||||
def test_bash_utils_functions(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test function detection in utils.sh file."""
|
||||
# Test with utils.sh as well
|
||||
utils_all_symbols, utils_root_symbols = language_server.request_document_symbols(
|
||||
"utils.sh", include_body=False
|
||||
)
|
||||
|
||||
utils_all_symbols, utils_root_symbols = language_server.request_document_symbols("utils.sh", include_body=False)
|
||||
|
||||
utils_function_symbols = [symbol for symbol in utils_all_symbols if symbol.get("kind") == 12]
|
||||
utils_function_names = [symbol["name"] for symbol in utils_function_symbols]
|
||||
|
||||
|
||||
# Should detect functions from utils.sh
|
||||
expected_utils_functions = ["to_uppercase", "to_lowercase", "trim_whitespace", "backup_file",
|
||||
"contains_element", "log_message", "is_valid_email", "is_number"]
|
||||
|
||||
expected_utils_functions = [
|
||||
"to_uppercase",
|
||||
"to_lowercase",
|
||||
"trim_whitespace",
|
||||
"backup_file",
|
||||
"contains_element",
|
||||
"log_message",
|
||||
"is_valid_email",
|
||||
"is_number",
|
||||
]
|
||||
|
||||
for func_name in expected_utils_functions:
|
||||
assert func_name in utils_function_names, f"Should find {func_name} function in utils.sh"
|
||||
|
||||
|
||||
assert len(utils_function_symbols) >= 8, f"Should find at least 8 functions in utils.sh, found {len(utils_function_symbols)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
|
||||
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
|
||||
def test_bash_regex_fallback_detection(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test the regex-based fallback function detection directly."""
|
||||
# Check if the method exists (it should if we have our enhanced bash server)
|
||||
if not hasattr(language_server, '_detect_bash_functions'):
|
||||
if not hasattr(language_server, "_detect_bash_functions"):
|
||||
pytest.fail("Enhanced bash function detection not available - this may be due to test environment setup")
|
||||
|
||||
# Test the _detect_bash_functions method directly
|
||||
|
||||
# Test the _detect_bash_functions method directly
|
||||
detected_functions = language_server._detect_bash_functions("main.sh", include_body=True)
|
||||
|
||||
|
||||
# Should detect functions even if LSP doesn't provide them
|
||||
assert len(detected_functions) == 3, f"Should detect exactly 3 functions, found {len(detected_functions)}"
|
||||
|
||||
|
||||
function_names = [func["name"] for func in detected_functions]
|
||||
assert "greet_user" in function_names, "Regex should detect greet_user function"
|
||||
assert "process_items" in function_names, "Regex should detect process_items function"
|
||||
assert "main" in function_names, "Regex should detect main function"
|
||||
|
||||
|
||||
# Check that symbols have proper structure
|
||||
for func in detected_functions:
|
||||
assert func["kind"] == 12, f"Function {func['name']} should have LSP Symbol Kind 12"
|
||||
assert "location" in func, f"Function {func['name']} should have location info"
|
||||
assert "range" in func, f"Function {func['name']} should have range info"
|
||||
assert "selectionRange" in func, f"Function {func['name']} should have selectionRange info"
|
||||
|
||||
|
||||
# Check location structure
|
||||
location = func["location"]
|
||||
assert "uri" in location, f"Function {func['name']} location should have URI"
|
||||
@@ -109,29 +115,37 @@ class TestBashLanguageServerBasics:
|
||||
def test_bash_function_syntax_patterns(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test detection of different bash function syntax patterns."""
|
||||
# Check if the method exists (it should if we have our enhanced bash server)
|
||||
if not hasattr(language_server, '_detect_bash_functions'):
|
||||
if not hasattr(language_server, "_detect_bash_functions"):
|
||||
pytest.fail("Enhanced bash function detection not available - this may be due to test environment setup")
|
||||
|
||||
|
||||
# Test main.sh (has 'function' keyword functions and regular function)
|
||||
main_functions = language_server._detect_bash_functions("main.sh", include_body=False)
|
||||
|
||||
|
||||
# Test utils.sh (all use 'function' keyword)
|
||||
utils_functions = language_server._detect_bash_functions("utils.sh", include_body=False)
|
||||
|
||||
|
||||
# Verify we detect both syntax patterns
|
||||
main_function_names = [func["name"] for func in main_functions]
|
||||
utils_function_names = [func["name"] for func in utils_functions]
|
||||
|
||||
|
||||
# main() uses regular syntax: main() {
|
||||
assert "main" in main_function_names, "Should detect regular function syntax"
|
||||
|
||||
|
||||
# Functions with 'function' keyword: function name() {
|
||||
assert "greet_user" in main_function_names, "Should detect function keyword syntax"
|
||||
assert "to_uppercase" in utils_function_names, "Should detect function keyword syntax in utils"
|
||||
|
||||
|
||||
# Verify all expected utils functions are detected
|
||||
expected_utils = ["to_uppercase", "to_lowercase", "trim_whitespace", "backup_file",
|
||||
"contains_element", "log_message", "is_valid_email", "is_number"]
|
||||
|
||||
expected_utils = [
|
||||
"to_uppercase",
|
||||
"to_lowercase",
|
||||
"trim_whitespace",
|
||||
"backup_file",
|
||||
"contains_element",
|
||||
"log_message",
|
||||
"is_valid_email",
|
||||
"is_number",
|
||||
]
|
||||
|
||||
for expected_func in expected_utils:
|
||||
assert expected_func in utils_function_names, f"Should detect {expected_func} function"
|
||||
assert expected_func in utils_function_names, f"Should detect {expected_func} function"
|
||||
@@ -27,14 +27,16 @@ class TestRubyLanguageServer:
|
||||
helper_symbol = sym
|
||||
break
|
||||
print(helper_symbol)
|
||||
assert helper_symbol is not None, "Could not find 'helper_function' symbol in main.rb"
|
||||
assert helper_symbol is not None, "Could not find 'helper_function' symbol in main.rb"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.RUBY], indirect=True)
|
||||
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
@pytest.mark.parametrize("repo_path", [Language.RUBY], indirect=True)
|
||||
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
# Test finding Calculator.add method definition from line 17: Calculator.new.add(demo.value, 10)
|
||||
definition_location_list = language_server.request_definition(str(repo_path / "main.rb"), 16,17) # add method at line 17 (0-indexed 16), position 17
|
||||
|
||||
definition_location_list = language_server.request_definition(
|
||||
str(repo_path / "main.rb"), 16, 17
|
||||
) # add method at line 17 (0-indexed 16), position 17
|
||||
|
||||
assert len(definition_location_list) == 1
|
||||
definition_location = definition_location_list[0]
|
||||
print(f"Found definition: {definition_location}")
|
||||
|
||||
Reference in new issue
Block a user