From ec1277167b7ec26c833bc4decd5df7c7ec007da0 Mon Sep 17 00:00:00 2001 From: Yen <5915590+antigenius0910@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:41:04 -0500 Subject: [PATCH] Add comprehensive bash language support (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --------- Co-authored-by: Claude --- CHANGELOG.md | 5 + README.md | 1 + pyproject.toml | 1 + .../language_servers/bash_language_server.py | 388 ++++++++++++++++++ src/solidlsp/ls.py | 5 + src/solidlsp/ls_config.py | 3 + test/conftest.py | 3 + test/resources/repos/bash/test_repo/config.sh | 91 ++++ test/resources/repos/bash/test_repo/main.sh | 70 ++++ test/resources/repos/bash/test_repo/utils.sh | 83 ++++ test/solidlsp/bash/__init__.py | 0 test/solidlsp/bash/test_bash_basic.py | 137 +++++++ 12 files changed, 787 insertions(+) create mode 100644 src/solidlsp/language_servers/bash_language_server.py create mode 100644 test/resources/repos/bash/test_repo/config.sh create mode 100644 test/resources/repos/bash/test_repo/main.sh create mode 100644 test/resources/repos/bash/test_repo/utils.sh create mode 100644 test/solidlsp/bash/__init__.py create mode 100644 test/solidlsp/bash/test_bash_basic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dfbf52c4..a32f46cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/README.md b/README.md index 1b6777cb..1d8d1d3d 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/pyproject.toml b/pyproject.toml index d46c678f..8e4d563c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/src/solidlsp/language_servers/bash_language_server.py b/src/solidlsp/language_servers/bash_language_server.py new file mode 100644 index 00000000..04ffe355 --- /dev/null +++ b/src/solidlsp/language_servers/bash_language_server.py @@ -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) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index ded2645a..edda2d6e 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -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") diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index efb55a87..3965d066 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -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}") diff --git a/test/conftest.py b/test/conftest.py index 849af543..da91ebdd 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -144,3 +144,6 @@ def project(request: LanguageParamRequest): language = request.param yield create_default_project(language) + + + diff --git a/test/resources/repos/bash/test_repo/config.sh b/test/resources/repos/bash/test_repo/config.sh new file mode 100644 index 00000000..440867e5 --- /dev/null +++ b/test/resources/repos/bash/test_repo/config.sh @@ -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 diff --git a/test/resources/repos/bash/test_repo/main.sh b/test/resources/repos/bash/test_repo/main.sh new file mode 100644 index 00000000..56d2e72e --- /dev/null +++ b/test/resources/repos/bash/test_repo/main.sh @@ -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 [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 "$@" diff --git a/test/resources/repos/bash/test_repo/utils.sh b/test/resources/repos/bash/test_repo/utils.sh new file mode 100644 index 00000000..77fc8fed --- /dev/null +++ b/test/resources/repos/bash/test_repo/utils.sh @@ -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]+$ ]] +} diff --git a/test/solidlsp/bash/__init__.py b/test/solidlsp/bash/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/bash/test_bash_basic.py b/test/solidlsp/bash/test_bash_basic.py new file mode 100644 index 00000000..3fd1f637 --- /dev/null +++ b/test/solidlsp/bash/test_bash_basic.py @@ -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" \ No newline at end of file