Apply formatter

This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2025-08-06 20:58:14 +02:00
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)
-3
View File
@@ -144,6 +144,3 @@ def project(request: LanguageParamRequest):
language = request.param
yield create_default_project(language)
+48 -34
View File
@@ -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"
+7 -5
View File
@@ -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}")