Add comprehensive bash language support (#375)

* feat: Add comprehensive bash language support

Implement BashLanguageServer using bash-language-server (v5.4.0)
- Add automatic Node.js/npm dependency management
- Support .sh and .bash file extensions
- Include cross-platform compatibility (Windows .cmd handling)
- Add comprehensive test infrastructure with realistic bash scripts
- Integrate bash into Language enum and language server factory
- Add bash pytest marker for selective testing
- Include bash_language_server fixture for tests

Features:
* LSP capabilities: text sync, completion, hover, document symbols
* Runtime dependency installation via npm
* Symbol recognition for functions, variables, conditionals
* Proper LSP initialization and error handling
* Test repository with main.sh, utils.sh, and config.sh examples

Files:
+ src/solidlsp/language_servers/bash_language_server.py (new implementation)
+ test/solidlsp/bash/test_bash_basic.py (comprehensive test suite)
+ test/resources/repos/bash/test_repo/ (test scripts)
* src/solidlsp/ls_config.py (add BASH enum and file patterns)
* src/solidlsp/ls.py (integrate bash language server)
* pyproject.toml (add bash pytest marker)
* test/conftest.py (add bash_language_server fixture)

Tests: 2 passed, 7 appropriately skipped
All existing tests continue to pass (288 passed total)

* trigger flow

* feat: enhance bash function detection with hybrid LSP + regex approach

- Upgrade bash-language-server from 5.4.0 to 5.6.0 for better capabilities
- Add hybrid function detection: primary LSP + regex fallback when needed
- Implement _detect_bash_functions() with regex patterns for both syntax types:
  * function name() { } (function keyword syntax)
  * name() { } (regular function syntax)
- Add _find_function_end() with brace matching for accurate function boundaries
- Enhance request_document_symbols() with intelligent fallback logic
- Add comprehensive test suite with 4 new test functions:
  * test_bash_function_detection_hybrid_approach
  * test_bash_function_detection_with_body
  * test_bash_regex_fallback_detection
  * test_bash_function_syntax_patterns
- All tests passing: 6 passed, 7 skipped, 0 failed
- Maintains 100% backward compatibility
- Enables full symbolic operations for bash functions: find_symbol, replace_symbol_body, etc.

* remove trash

* clean: remove debug changes from Python services.py

- Remove 'MODIFIED VIA SYMBOLIC OPERATIONS!' debug comments
- Remove debug print statements added during testing
- Restore original clean docstring and code
- No functional changes to bash language support implementation

* fix: Remove non-bash specific directory ignoring from BashLanguageServer

- Removes is_ignored_dirname override that included generic directories like node_modules, .cache, tmp, temp
- These directories aren't bash-specific and gitignore already handles file exclusion
- Addresses PR feedback: 'that doesn't seem bash related. Note that gitignored files are ignored anyway'

* fix: Restructure bash tests to follow standard patterns and remove pytest.skip

- Replace custom bash_language_server fixture with standard parametrized language_server fixture
- Remove all pytest.skip statements and replace with proper assertions or failures
- Remove tests for non-existent methods (get_symbols_for_document, get_workspace_symbols, etc.)
- Keep only tests that use actual SolidLanguageServer methods like request_document_symbols
- All bash tests now pass successfully
- Addresses PR feedback about test structure and pytest.skip removal

* docs: Add bash language support to README

- Add Bash to the list of directly supported languages
- Note that it requires Node.js and npm for bash-language-server installation
- Addresses PR feedback about missing README extension for bash support

* docs: Add bash language support to CHANGELOG

- Document comprehensive bash language support with bash-language-server integration
- Highlight key features: .sh/.bash extensions, hybrid function detection, automatic dependency management
- Note cross-platform compatibility including Windows support
- Addresses PR feedback about missing CHANGELOG extension for bash support

* refactor: Change regex fallback to always-on hybrid detection for bash functions

- Remove conditional logic that only ran regex when LSP found no functions
- Implement always-on hybrid approach that runs both LSP and regex detection
- Add comprehensive documentation explaining bash-language-server limitations
- Add new _merge_function_detections method to combine results without duplicates
- Addresses reviewer feedback about files with mixed function notation styles

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
YenandClaude authored and GitHub committed 2025-08-06 16:41:04 +02:00
1 parent 7158aa4870
commit ec1277167b
12 files changed
+787

No files matched your search

+5
View File
@@ -6,6 +6,11 @@ Status of the `main` branch. Changes prior to the next official version change w
* Reliably detect language server termination and propagate the respective error all the way
back to the tool application, where an unexpected termination is handled by restarting the language server
and subsequently retrying the tool application.
* Add comprehensive bash language support with bash-language-server integration
* Support for .sh and .bash file extensions
* Hybrid function detection (LSP + regex fallback) for reliable symbolic editing
* Automatic Node.js/npm dependency management
* Cross-platform compatibility including Windows
Fixes:
* Ignore `.git` in check for ignored paths and improve performance of `find_all_non_ignored_files`
+1
View File
@@ -75,6 +75,7 @@ With Serena, we provide
* Java (_Note_: startup is slow, initial startup especially so. There may be issues with java on macos and linux, we are working on it.)
* Elixir (Requires NextLS and Elixir install; **Windows not supported** - Next LS does not provide Windows binaries)
* Clojure
* Bash (Requires Node.js and npm for bash-language-server installation)
* C/C++ (You may experience issues with finding references, we are working on it)
* indirect support (may require some code changes/manual installation) for:
* Ruby (untested)
+1
View File
@@ -259,6 +259,7 @@ markers = [
"csharp: language server running for C#",
"elixir: language server running for Elixir",
"terraform: language server running for Terraform",
"bash: language server running for Bash",
"snapshot: snapshot tests for symbolic editing operations",
"ruby: language server running for Ruby",
]
@@ -0,0 +1,388 @@
"""
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
Contains various configurations and settings specific to Bash scripting.
"""
import logging
import os
import pathlib
import shutil
import threading
from overrides import override
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_logger import LanguageServerLogger
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
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.
Contains various configurations and settings specific to Bash scripting.
"""
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a BashLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
bash_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=bash_lsp_executable_path, cwd=repository_root_path),
"bash",
solidlsp_settings,
)
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
) -> str:
"""
Setup runtime dependencies for Bash Language Server and return the command to start the server.
"""
# Verify both node and npm are installed
is_node_installed = shutil.which("node") is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
is_npm_installed = shutil.which("npm") is not None
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="bash-language-server",
description="bash-language-server package",
command="npm install --prefix ./ bash-language-server@5.6.0",
platform_id="any",
),
]
)
# Install bash-language-server if not already installed
bash_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "bash-lsp")
bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server")
# Handle Windows executable extension
if os.name == "nt":
bash_executable_path += ".cmd"
if not os.path.exists(bash_executable_path):
logger.log(f"Bash Language Server executable not found at {bash_executable_path}. Installing...", logging.INFO)
deps.install(logger, bash_ls_dir)
logger.log("Bash language server dependencies installed successfully", logging.INFO)
if not os.path.exists(bash_executable_path):
raise FileNotFoundError(
f"bash-language-server executable not found at {bash_executable_path}, something went wrong with the installation."
)
return f"{bash_executable_path} start"
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Bash Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {"dynamicRegistration": True},
"codeAction": {"dynamicRegistration": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params
def _start_server(self):
"""
Starts the Bash Language Server, waits for the server to be ready and yields the LanguageServer instance.
"""
def register_capability_handler(params):
assert "registrations" in params
for registration in params["registrations"]:
if registration["method"] == "workspace/executeCommand":
self.initialize_searcher_command_available.set()
return
def execute_client_command_handler(params):
return []
def do_nothing(params):
return
def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
# Check for bash-language-server ready signals
message_text = msg.get("message", "")
if "Analyzing" in message_text or "analysis complete" in message_text.lower():
self.logger.log("Bash language server analysis signals detected", logging.INFO)
self.server_ready.set()
self.completions_available.set()
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.logger.log("Starting Bash server process", logging.INFO)
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
init_response = self.server.send.initialize(initialize_params)
self.logger.log(f"Received initialize response from bash server: {init_response}", logging.DEBUG)
# 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)
else:
self.logger.log("Warning: Bash server does not report document symbol support", logging.WARNING)
self.server.notify.initialized({})
# Wait for server readiness with timeout
self.logger.log("Waiting for Bash language server to be ready...", logging.INFO)
if not self.server_ready.wait(timeout=3.0):
# Fallback: assume server is ready after timeout
self.logger.log("Timeout waiting for bash server ready signal, proceeding anyway", logging.WARNING)
self.server_ready.set()
self.completions_available.set()
else:
self.logger.log("Bash server initialization complete", logging.INFO)
def request_document_symbols(
self, relative_file_path: str, include_body: bool = False
) -> 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
)
return merged_all_symbols, merged_root_symbols
def _merge_function_detections(
self,
lsp_all_symbols: list[ls_types.UnifiedSymbolInformation],
lsp_root_symbols: 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:
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*\{')
]
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}
},
absolutePath=abs_path,
relativePath=relative_file_path
)
# Create the function symbol
func_symbol = ls_types.UnifiedSymbolInformation(
name=func_name,
kind=12, # LSP Symbol Kind for Function
location=location,
range=location["range"],
selectionRange={
"start": {"line": start_line, "character": match.start(1)},
"end": {"line": start_line, "character": match.end(1)}
},
children=[],
parent=None
)
# Add function body if requested
if include_body:
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 == '{':
brace_count += 1
in_function = True
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)
+5
View File
@@ -226,6 +226,11 @@ class SolidLanguageServer(ABC):
ls = TerraformLS(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
elif config.code_language == Language.BASH:
from solidlsp.language_servers.bash_language_server import BashLanguageServer
ls = BashLanguageServer(config, logger, repository_root_path, solidlsp_settings=solidlsp_settings)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
raise SolidLSPException(f"Language {config.code_language} is not supported")
+3
View File
@@ -42,6 +42,7 @@ class Language(str, Enum):
CLOJURE = "clojure"
ELIXIR = "elixir"
TERRAFORM = "terraform"
BASH = "bash"
# Experimental or deprecated Language Servers
TYPESCRIPT_VTS = "typescript_vts"
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
@@ -103,6 +104,8 @@ class Language(str, Enum):
return FilenameMatcher("*.ex", "*.exs")
case self.TERRAFORM:
return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate")
case self.BASH:
return FilenameMatcher("*.sh", "*.bash")
case _:
raise ValueError(f"Unhandled language: {self}")
+3
View File
@@ -144,3 +144,6 @@ def project(request: LanguageParamRequest):
language = request.param
yield create_default_project(language)
@@ -0,0 +1,91 @@
#!/bin/bash
# Configuration script for project setup
# Environment variables
export PROJECT_NAME="bash-test-project"
export PROJECT_VERSION="1.0.0"
export LOG_LEVEL="INFO"
export CONFIG_DIR="./config"
# Default settings
DEFAULT_TIMEOUT=30
DEFAULT_RETRIES=3
DEFAULT_PORT=8080
# Configuration arrays
declare -A ENVIRONMENTS=(
["dev"]="development"
["prod"]="production"
["test"]="testing"
)
declare -A DATABASE_CONFIGS=(
["host"]="localhost"
["port"]="5432"
["name"]="myapp_db"
["user"]="dbuser"
)
# Function to load configuration
load_config() {
local env="${1:-dev}"
local config_file="${CONFIG_DIR}/${env}.conf"
if [[ -f "$config_file" ]]; then
echo "Loading configuration from $config_file"
source "$config_file"
else
echo "Warning: Configuration file $config_file not found, using defaults"
fi
}
# Function to validate configuration
validate_config() {
local errors=0
if [[ -z "$PROJECT_NAME" ]]; then
echo "Error: PROJECT_NAME is not set" >&2
((errors++))
fi
if [[ -z "$PROJECT_VERSION" ]]; then
echo "Error: PROJECT_VERSION is not set" >&2
((errors++))
fi
if [[ $DEFAULT_PORT -lt 1024 || $DEFAULT_PORT -gt 65535 ]]; then
echo "Error: Invalid port number $DEFAULT_PORT" >&2
((errors++))
fi
return $errors
}
# Function to print configuration
print_config() {
echo "=== Current Configuration ==="
echo "Project Name: $PROJECT_NAME"
echo "Version: $PROJECT_VERSION"
echo "Log Level: $LOG_LEVEL"
echo "Default Port: $DEFAULT_PORT"
echo "Default Timeout: $DEFAULT_TIMEOUT"
echo "Default Retries: $DEFAULT_RETRIES"
echo "\n=== Environments ==="
for env in "${!ENVIRONMENTS[@]}"; do
echo " $env: ${ENVIRONMENTS[$env]}"
done
echo "\n=== Database Configuration ==="
for key in "${!DATABASE_CONFIGS[@]}"; do
echo " $key: ${DATABASE_CONFIGS[$key]}"
done
}
# Initialize configuration if this script is run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
load_config "$1"
validate_config
print_config
fi
@@ -0,0 +1,70 @@
#!/bin/bash
# Main script demonstrating various bash features
# Global variables
readonly SCRIPT_NAME="Main Script"
COUNTER=0
declare -a ITEMS=("item1" "item2" "item3")
# Function definitions
function greet_user() {
local username="$1"
local greeting_type="${2:-default}"
case "$greeting_type" in
"formal")
echo "Good day, ${username}!"
;;
"casual")
echo "Hey ${username}!"
;;
*)
echo "Hello, ${username}!"
;;
esac
}
function process_items() {
local -n items_ref=$1
local operation="$2"
for item in "${items_ref[@]}"; do
case "$operation" in
"count")
((COUNTER++))
echo "Processing item $COUNTER: $item"
;;
"uppercase")
echo "${item^^}"
;;
*)
echo "Unknown operation: $operation"
return 1
;;
esac
done
}
# Main execution
main() {
echo "Starting $SCRIPT_NAME"
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <username> [greeting_type]"
exit 1
fi
local user="$1"
local greeting="${2:-default}"
greet_user "$user" "$greeting"
echo "Processing items..."
process_items ITEMS "count"
echo "Script completed successfully"
}
# Run main function with all arguments
main "$@"
@@ -0,0 +1,83 @@
#!/bin/bash
# Utility functions for bash scripting
# String manipulation functions
function to_uppercase() {
echo "${1^^}"
}
function to_lowercase() {
echo "${1,,}"
}
function trim_whitespace() {
local var="$1"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
echo "$var"
}
# File operations
function backup_file() {
local file="$1"
local backup_dir="${2:-./backups}"
if [[ ! -f "$file" ]]; then
echo "Error: File '$file' does not exist" >&2
return 1
fi
mkdir -p "$backup_dir"
cp "$file" "${backup_dir}/$(basename "$file").$(date +%Y%m%d_%H%M%S).bak"
echo "Backup created for $file"
}
# Array operations
function contains_element() {
local element="$1"
shift
local array=("$@")
for item in "${array[@]}"; do
if [[ "$item" == "$element" ]]; then
return 0
fi
done
return 1
}
# Logging functions
function log_message() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
"ERROR")
echo "[$timestamp] ERROR: $message" >&2
;;
"WARN")
echo "[$timestamp] WARN: $message" >&2
;;
"INFO")
echo "[$timestamp] INFO: $message"
;;
"DEBUG")
[[ "${DEBUG:-false}" == "true" ]] && echo "[$timestamp] DEBUG: $message"
;;
*)
echo "[$timestamp] $message"
;;
esac
}
# Validation functions
function is_valid_email() {
local email="$1"
[[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
}
function is_number() {
[[ $1 =~ ^[0-9]+$ ]]
}
View File
Whitespace-only changes.
+137
View File
@@ -0,0 +1,137 @@
"""
Basic integration tests for the bash language server functionality.
These tests validate the functionality of the language server APIs
like request_document_symbols using the bash test repository.
"""
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
@pytest.mark.bash
class TestBashLanguageServerBasics:
"""Test basic functionality of the bash language server."""
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
def test_bash_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
"""Test that bash language server can be initialized successfully."""
assert language_server is not None
assert language_server.language == Language.BASH
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
def test_bash_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""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 "main" in function_names, "Should find main function"
assert len(function_symbols) >= 3, f"Should find at least 3 functions, found {len(function_symbols)}"
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
def test_bash_request_document_symbols_with_body(self, language_server: SolidLanguageServer) -> None:
"""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"
assert "case" in body.lower(), "Function body should contain case statement"
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
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_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"]
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)
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'):
pytest.fail("Enhanced bash function detection not available - this may be due to test environment setup")
# 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"
assert "range" in location, f"Function {func['name']} location should have range"
assert "relativePath" in location, f"Function {func['name']} location should have relative path"
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
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'):
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"]
for expected_func in expected_utils:
assert expected_func in utils_function_names, f"Should detect {expected_func} function"