From 1df9732390ff57524d5263de6fa69bd3ae033625 Mon Sep 17 00:00:00 2001 From: Yen <5915590+antigenius0910@users.noreply.github.com> Date: Sun, 6 Jul 2025 10:15:54 -0500 Subject: [PATCH] feature/terraform language server support (#277) Adds terraform support --- .github/workflows/pytest.yml | 35 ++- .vscode/settings.json | 3 +- pyproject.toml | 1 + .../terraform_ls/initialize_params.json | 46 ++++ .../terraform_ls/runtime_dependencies.json | 37 +++ .../terraform_ls/terraform_ls.py | 249 ++++++++++++++++++ src/solidlsp/ls.py | 5 + src/solidlsp/ls_config.py | 3 + .../repos/terraform/test_repo/data.tf | 28 ++ .../repos/terraform/test_repo/main.tf | 126 +++++++++ .../repos/terraform/test_repo/outputs.tf | 46 ++++ .../repos/terraform/test_repo/variables.tf | 61 +++++ .../terraform/test_terraform_basic.py | 55 ++++ 13 files changed, 693 insertions(+), 2 deletions(-) create mode 100644 src/solidlsp/language_servers/terraform_ls/initialize_params.json create mode 100644 src/solidlsp/language_servers/terraform_ls/runtime_dependencies.json create mode 100644 src/solidlsp/language_servers/terraform_ls/terraform_ls.py create mode 100644 test/resources/repos/terraform/test_repo/data.tf create mode 100644 test/resources/repos/terraform/test_repo/main.tf create mode 100644 test/resources/repos/terraform/test_repo/outputs.tf create mode 100644 test/resources/repos/terraform/test_repo/variables.tf create mode 100644 test/solidlsp/terraform/test_terraform_basic.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index fe9b15c..20d0567 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,7 +14,6 @@ jobs: cpu: name: Tests on ${{ matrix.os }} runs-on: ${{ matrix.os }} - timeout-minutes: 15 if: "!contains(github.event.head_commit.message, 'ci skip')" strategy: fail-fast: false @@ -52,6 +51,40 @@ jobs: uses: DeLaGuardo/setup-clojure@13.4 with: cli: latest + - name: Install Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "1.5.0" + terraform_wrapper: false + - name: Install terraform-ls (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Write-Host "Installing terraform-ls manually for Windows" + $ver = '0.32.7' + $zip = "terraform-ls_${ver}_windows_amd64.zip" + Invoke-WebRequest -Uri "https://releases.hashicorp.com/terraform-ls/$ver/$zip" -OutFile $zip + + $dest = "$env:USERPROFILE\terraform-ls" + New-Item -ItemType Directory -Force -Path $dest | Out-Null + Expand-Archive $zip -DestinationPath $dest -Force + + Write-Host "terraform-ls installed to: $dest" + echo "$dest" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install terraform-ls (Linux/macOS) + if: runner.os != 'Windows' + shell: bash + run: | + ver=0.32.7 + os=$(uname | tr '[:upper:]' '[:lower:]') + echo "Installing terraform-ls ${ver} for ${os}" + curl -sSL -o tfls.zip \ + "https://releases.hashicorp.com/terraform-ls/${ver}/terraform-ls_${ver}_${os}_amd64.zip" + mkdir -p "$HOME/bin" + unzip -q tfls.zip -d "$HOME/bin" + chmod +x "$HOME/bin/terraform-ls" + echo "$HOME/bin" >> "$GITHUB_PATH" + echo "terraform-ls installed to $HOME/bin" - name: Install uv shell: bash run: curl -LsSf https://astral.sh/uv/install.sh | sh diff --git a/.vscode/settings.json b/.vscode/settings.json index 56a7855..6d3f3ba 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,4 +11,5 @@ "sensai", "vibing" ], -} \ No newline at end of file +} + diff --git a/pyproject.toml b/pyproject.toml index 610dbd0..ad37014 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -248,6 +248,7 @@ markers = [ "php: language server running for PHP", "csharp: language server running for C#", "elixir: language server running for Elixir", + "terraform: language server running for Terraform", "snapshot: snapshot tests for symbolic editing operations", ] diff --git a/src/solidlsp/language_servers/terraform_ls/initialize_params.json b/src/solidlsp/language_servers/terraform_ls/initialize_params.json new file mode 100644 index 0000000..40592e1 --- /dev/null +++ b/src/solidlsp/language_servers/terraform_ls/initialize_params.json @@ -0,0 +1,46 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "hierarchicalDocumentSymbolSupport": true, + "symbolKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 + ] + } + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/terraform_ls/runtime_dependencies.json b/src/solidlsp/language_servers/terraform_ls/runtime_dependencies.json new file mode 100644 index 0000000..873e7ec --- /dev/null +++ b/src/solidlsp/language_servers/terraform_ls/runtime_dependencies.json @@ -0,0 +1,37 @@ +{ + "_description": "Used to download the runtime dependencies for running terraform-ls. Obtained from https://releases.hashicorp.com/terraform-ls/", + "runtimeDependencies": [ + { + "id": "TerraformLS", + "description": "terraform-ls for macOS (ARM64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip", + "platformId": "osx-arm64", + "archiveType": "zip", + "binaryName": "terraform-ls" + }, + { + "id": "TerraformLS", + "description": "terraform-ls for macOS (x64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip", + "platformId": "osx-x64", + "archiveType": "zip", + "binaryName": "terraform-ls" + }, + { + "id": "TerraformLS", + "description": "terraform-ls for Linux (x64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "terraform-ls" + }, + { + "id": "TerraformLS", + "description": "terraform-ls for Windows (x64)", + "url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip", + "platformId": "win-x64", + "archiveType": "zip", + "binaryName": "terraform-ls.exe" + } + ] +} diff --git a/src/solidlsp/language_servers/terraform_ls/terraform_ls.py b/src/solidlsp/language_servers/terraform_ls/terraform_ls.py new file mode 100644 index 0000000..10868c1 --- /dev/null +++ b/src/solidlsp/language_servers/terraform_ls/terraform_ls.py @@ -0,0 +1,249 @@ +import json +import logging +import os +import pathlib +import shutil +import stat +import subprocess +import threading + +from overrides import override + +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo + + +class TerraformLS(SolidLanguageServer): + """ + Provides Terraform specific instantiation of the LanguageServer class using terraform-ls. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # For Terraform projects, we should ignore: + # - .terraform: Terraform working directory with providers and modules + # - terraform.tfstate.d: Terraform workspace state directories + # - .git: Version control + # - node_modules: If the project has JavaScript components + return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d", "node_modules"] + + @staticmethod + def _get_terraform_version(logger=None): + """Get the installed Terraform version or None if not found.""" + if logger: + logger.log("Starting terraform version detection...", logging.DEBUG) + else: + logging.debug("Starting terraform version detection...") + + # 1. Try to find terraform using shutil.which (standard Python way) + terraform_cmd = shutil.which("terraform") + if terraform_cmd: + if logger: + logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG) + else: + logging.debug(f"Found terraform via shutil.which: {terraform_cmd}") + else: + if logger: + logger.log("terraform not found via shutil.which", logging.DEBUG) + else: + logging.debug("terraform not found via shutil.which") + + # 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action) + if not terraform_cmd: + terraform_cli_path = os.environ.get('TERRAFORM_CLI_PATH') + if terraform_cli_path: + if logger: + logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG) + else: + logging.debug(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}") + terraform_exe = os.path.join(terraform_cli_path, "terraform.exe") + if os.path.exists(terraform_exe): + terraform_cmd = terraform_exe + if logger: + logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG) + else: + logging.debug(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}") + else: + if logger: + logger.log(f"terraform.exe not found at {terraform_exe}", logging.DEBUG) + else: + logging.debug(f"terraform.exe not found at {terraform_exe}") + else: + if logger: + logger.log("TERRAFORM_CLI_PATH not set", logging.DEBUG) + else: + logging.debug("TERRAFORM_CLI_PATH not set") + + # 3. Try to run the terraform command if found + if terraform_cmd: + try: + if logger: + logger.log(f"Attempting to run: {terraform_cmd} version (with 15s timeout)", logging.DEBUG) + else: + logging.debug(f"Attempting to run: {terraform_cmd} version (with 15s timeout)") + result = subprocess.run( + [terraform_cmd, "version"], + capture_output=True, + text=True, + check=False, + timeout=15 # CRITICAL: 15 second timeout to prevent hangs + ) + if result.returncode == 0: + if logger: + logger.log("terraform version command succeeded", logging.DEBUG) + else: + logging.debug("terraform version command succeeded") + return result.stdout.strip() + else: + if logger: + logger.log(f"terraform version command failed with return code {result.returncode}", logging.DEBUG) + logger.log(f"stderr: {result.stderr}", logging.DEBUG) + else: + logging.debug(f"terraform version command failed with return code {result.returncode}") + logging.debug(f"stderr: {result.stderr}") + except subprocess.TimeoutExpired: + if logger: + logger.log("terraform version command timed out after 15 seconds", logging.ERROR) + else: + logging.error("terraform version command timed out after 15 seconds") + except (FileNotFoundError, OSError) as e: + if logger: + logger.log(f"Failed to run terraform command: {e}", logging.DEBUG) + else: + logging.debug(f"Failed to run terraform command: {e}") + else: + if logger: + logger.log("No terraform executable found", logging.DEBUG) + else: + logging.debug("No terraform executable found") + + return None + + + def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str: + """ + Setup runtime dependencies for terraform-ls. + Downloads and installs terraform-ls if not already present. + """ + # First check if Terraform is available + terraform_version = self._get_terraform_version(logger) + if not terraform_version: + raise RuntimeError( + "Terraform executable not found or failed to execute. " + "Please ensure Terraform is installed and accessible in your system's PATH.\n" + "If it's installed, check for permission issues or corrupted installation.\n" + "Download from https://www.terraform.io/downloads" + ) + + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value] + assert len(runtime_dependencies) == 1, f"Expected exactly one runtime dependency for platform {platform_id.value}, found {len(runtime_dependencies)}" + dependency = runtime_dependencies[0] + + terraform_ls_dir = os.path.join(os.path.dirname(__file__), "static", "TerraformLS") + terraform_ls_executable_path = os.path.join(terraform_ls_dir, dependency["binaryName"]) + + if not os.path.exists(terraform_ls_dir): + os.makedirs(terraform_ls_dir) + + if not os.path.exists(terraform_ls_executable_path): + logger.log(f"Downloading terraform-ls from {dependency['url']}", logging.INFO) + FileUtils.download_and_extract_archive(logger, dependency["url"], terraform_ls_dir, dependency["archiveType"]) + + assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}" + + # Make the executable file executable on Unix-like systems + if platform_id.value != "win-x64": + os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD) + + return terraform_ls_executable_path + + def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str): + """ + Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + terraform_ls_executable_path = self.setup_runtime_dependencies(logger, config) + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path), + "terraform", + ) + self.server_ready = threading.Event() + self.request_id = 0 + + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Terraform Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """Start terraform-ls server process""" + + def register_capability_handler(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + self.logger.log("Starting terraform-ls 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) + + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) + self.completions_available.set() + + # terraform-ls server is typically ready immediately after initialization + self.server_ready.set() + self.server_ready.wait() diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index a6b8be8..468f11c 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -187,6 +187,11 @@ class SolidLanguageServer(ABC): ls = ElixirTools(config, logger, repository_root_path) + elif config.code_language == Language.TERRAFORM: + from solidlsp.language_servers.terraform_ls.terraform_ls import TerraformLS + + ls = TerraformLS(config, logger, repository_root_path) + else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) raise LanguageServerException(f"Language {config.code_language} is not supported") diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 3a279d8..e5df901 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -39,6 +39,7 @@ class Language(str, Enum): PHP = "php" CLOJURE = "clojure" ELIXIR = "elixir" + TERRAFORM = "terraform" def __str__(self) -> str: return self.value @@ -77,6 +78,8 @@ class Language(str, Enum): return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") # codespell:ignore edn case self.ELIXIR: return FilenameMatcher("*.ex", "*.exs") + case self.TERRAFORM: + return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate") case _: raise ValueError(f"Unhandled language: {self}") diff --git a/test/resources/repos/terraform/test_repo/data.tf b/test/resources/repos/terraform/test_repo/data.tf new file mode 100644 index 0000000..0dd2d93 --- /dev/null +++ b/test/resources/repos/terraform/test_repo/data.tf @@ -0,0 +1,28 @@ +# Data sources for the Terraform configuration + +# Get the latest Ubuntu AMI +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] # Canonical + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +# Get available availability zones +data "aws_availability_zones" "available" { + state = "available" +} + +# Get current AWS caller identity +data "aws_caller_identity" "current" {} + +# Get current AWS region +data "aws_region" "current" {} diff --git a/test/resources/repos/terraform/test_repo/main.tf b/test/resources/repos/terraform/test_repo/main.tf new file mode 100644 index 0000000..d186c20 --- /dev/null +++ b/test/resources/repos/terraform/test_repo/main.tf @@ -0,0 +1,126 @@ +# Main Terraform configuration +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +# EC2 Instance +resource "aws_instance" "web_server" { + ami = data.aws_ami.ubuntu.id + instance_type = var.instance_type + + vpc_security_group_ids = [aws_security_group.web_sg.id] + subnet_id = aws_subnet.public.id + + user_data = <<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + systemctl start nginx + systemctl enable nginx + EOF + + tags = { + Name = "${var.project_name}-web-server" + Environment = var.environment + Project = var.project_name + } +} + +# S3 Bucket +resource "aws_s3_bucket" "app_bucket" { + bucket = "${var.project_name}-${var.environment}-bucket" + + tags = { + Name = "${var.project_name}-bucket" + Environment = var.environment + Project = var.project_name + } +} + +resource "aws_s3_bucket_versioning" "app_bucket_versioning" { + bucket = aws_s3_bucket.app_bucket.id + versioning_configuration { + status = "Enabled" + } +} + +# VPC +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.project_name}-vpc" + Environment = var.environment + Project = var.project_name + } +} + +# Internet Gateway +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.project_name}-igw" + Environment = var.environment + Project = var.project_name + } +} + +# Public Subnet +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-public-subnet" + Environment = var.environment + Project = var.project_name + } +} + +# Security Group +resource "aws_security_group" "web_sg" { + name_prefix = "${var.project_name}-web-" + vpc_id = aws_vpc.main.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-web-sg" + Environment = var.environment + Project = var.project_name + } +} diff --git a/test/resources/repos/terraform/test_repo/outputs.tf b/test/resources/repos/terraform/test_repo/outputs.tf new file mode 100644 index 0000000..856ed6d --- /dev/null +++ b/test/resources/repos/terraform/test_repo/outputs.tf @@ -0,0 +1,46 @@ +# Output values for the Terraform configuration + +output "instance_id" { + description = "ID of the EC2 instance" + value = aws_instance.web_server.id +} + +output "instance_public_ip" { + description = "Public IP address of the EC2 instance" + value = aws_instance.web_server.public_ip +} + +output "instance_public_dns" { + description = "Public DNS name of the EC2 instance" + value = aws_instance.web_server.public_dns +} + +output "s3_bucket_name" { + description = "Name of the S3 bucket" + value = aws_s3_bucket.app_bucket.bucket +} + +output "s3_bucket_arn" { + description = "ARN of the S3 bucket" + value = aws_s3_bucket.app_bucket.arn +} + +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.main.id +} + +output "subnet_id" { + description = "ID of the public subnet" + value = aws_subnet.public.id +} + +output "security_group_id" { + description = "ID of the security group" + value = aws_security_group.web_sg.id +} + +output "application_url" { + description = "URL to access the application" + value = "http://${aws_instance.web_server.public_dns}" +} diff --git a/test/resources/repos/terraform/test_repo/variables.tf b/test/resources/repos/terraform/test_repo/variables.tf new file mode 100644 index 0000000..92437bd --- /dev/null +++ b/test/resources/repos/terraform/test_repo/variables.tf @@ -0,0 +1,61 @@ +# Input variables for the Terraform configuration + +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "us-west-2" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.micro" + + validation { + condition = contains([ + "t3.micro", "t3.small", "t3.medium", + "t2.micro", "t2.small", "t2.medium" + ], var.instance_type) + error_message = "Instance type must be a valid t2 or t3 instance type." + } +} + +variable "environment" { + description = "Environment name (dev, staging, prod)" + type = string + default = "dev" + + validation { + condition = contains(["dev", "staging", "prod"], var.environment) + error_message = "Environment must be dev, staging, or prod." + } +} + +variable "project_name" { + description = "Name of the project" + type = string + default = "terraform-test" + + validation { + condition = can(regex("^[a-z0-9-]+$", var.project_name)) + error_message = "Project name must contain only lowercase letters, numbers, and hyphens." + } +} + +variable "enable_monitoring" { + description = "Enable CloudWatch monitoring" + type = bool + default = false +} + +variable "allowed_cidr_blocks" { + description = "List of CIDR blocks allowed to access the application" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "tags" { + description = "Additional tags to apply to resources" + type = map(string) + default = {} +} diff --git a/test/solidlsp/terraform/test_terraform_basic.py b/test/solidlsp/terraform/test_terraform_basic.py new file mode 100644 index 0000000..eb08aab --- /dev/null +++ b/test/solidlsp/terraform/test_terraform_basic.py @@ -0,0 +1,55 @@ +""" +Basic integration tests for the Terraform language server functionality. + +These tests validate the functionality of the language server APIs +like request_references using the test repository. +""" + +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language + + +@pytest.mark.terraform +class TestLanguageServerBasics: + """Test basic functionality of the Terraform language server.""" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_basic_definition(self, language_server: SolidLanguageServer) -> None: + """Test basic definition lookup functionality.""" + # Simple test to verify the language server is working + file_path = "main.tf" + # Just try to get document symbols - this should work without hanging + symbols = language_server.request_document_symbols(file_path) + assert len(symbols) > 0, "Should find at least some symbols in main.tf" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_request_references_aws_instance(self, language_server: SolidLanguageServer) -> None: + """Test request_references on an aws_instance resource.""" + # Get references to an aws_instance resource in main.tf + file_path = "main.tf" + # Find aws_instance resources + symbols = language_server.request_document_symbols(file_path) + aws_instance_symbol = next((s for s in symbols[0] if s.get("name") == 'resource "aws_instance" "web_server"'), None) + if not aws_instance_symbol or "selectionRange" not in aws_instance_symbol: + raise AssertionError("aws_instance symbol or its selectionRange not found") + sel_start = aws_instance_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert len(references) >= 1, "aws_instance should be referenced at least once" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_request_references_variable(self, language_server: SolidLanguageServer) -> None: + """Test request_references on a variable.""" + # Get references to a variable in variables.tf + file_path = "variables.tf" + # Find variable definitions + symbols = language_server.request_document_symbols(file_path) + var_symbol = next((s for s in symbols[0] if s.get("name") == 'variable "instance_type"'), None) + if not var_symbol or "selectionRange" not in var_symbol: + raise AssertionError("variable symbol or its selectionRange not found") + sel_start = var_symbol["selectionRange"]["start"] + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert len(references) >= 1, "variable should be referenced at least once"