feat: Add Haskell language server support (#697)

This commit is contained in:
Ketema Harris authored and GitHub committed 2025-10-26 23:30:40 +01:00
1 parent 1461aff18c
commit 37c4898bd6
15 files changed
+827 -1

No files matched your search

+34
View File
@@ -154,6 +154,40 @@ jobs:
- name: Install Julia LanguageServer
shell: bash
run: julia -e 'using Pkg; Pkg.add("LanguageServer")'
- name: Setup Haskell toolchain
if: runner.os != 'Windows'
uses: haskell/ghcup-setup@v1
with:
ghc: '9.12.2'
cabal: '3.10.3.0'
hls: '2.11.0.0'
- name: Verify Haskell tools
if: runner.os != 'Windows'
run: |
echo "Verifying installed Haskell tools:"
which ghc && ghc --version
which cabal && cabal --version
# HLS verification - non-blocking in case of version incompatibility
if command -v haskell-language-server-wrapper &>/dev/null; then
echo "Found haskell-language-server-wrapper"
haskell-language-server-wrapper --version || echo "WARNING: HLS wrapper found but version check failed"
elif command -v haskell-language-server &>/dev/null; then
echo "Found haskell-language-server"
haskell-language-server --version || echo "WARNING: HLS found but version check failed"
else
echo "WARNING: HLS not found (may be incompatible with GHC 9.12.2)"
echo "This is not a critical error - tests will use HLS if available at runtime"
fi
shell: bash
- name: Pre-build Haskell test project for HLS
if: runner.os != 'Windows'
run: |
cd test/resources/repos/haskell/test_repo
cabal update
cabal build --only-dependencies
cabal build
echo "Haskell test project built successfully"
shell: bash
- name: Install Zig
uses: goto-bus-stop/setup-zig@v2
with:
+9 -1
View File
@@ -248,4 +248,12 @@ tmp/
.scala-build/
.bloop/
bootstrap
test/resources/repos/scala/.bloop/
test/resources/repos/scala/.bloop/
# Haskell
.stack-work/
*.cabal
stack.yaml.lock
dist-newstyle/
cabal.project.local*
.ghc.environment.*
+1
View File
@@ -5,6 +5,7 @@ Status of the `main` branch. Changes prior to the next official version change w
* Added `RenameSymbolTool` for renaming symbols across the codebase (if LS supports this operation).
* Language support:
* **Add support for Haskell** via Haskell Language Server (HLS) with automatic discovery via ghcup, stack, or system PATH; supports both Stack and Cabal projects
* **Add support for Scala** via Metals language server (requires some [manual setup](docs/scala_setup_guide_for_serena.md))
* **Add support for Elm** via @elm-tooling/elm-language-server (automatically downloads if not installed; requires Elm compiler)
* **Add support for Perl** via Perl::LanguageServer with LSP integration for .pl, .pm, and .t files
+1
View File
@@ -99,6 +99,7 @@ With Serena, we provide direct, out-of-the-box support for:
* Scala (requires some [manual setup](docs/scala_setup_guide_for_serena.md); uses Metals LSP)
* Erlang (requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls), experimental, might be slow or hang)
* Perl (requires installation of Perl::LanguageServer)
* Haskell (automatically locates HLS via ghcup, stack, or system PATH; supports Stack and Cabal projects)
* AL
* Markdown (must be explicitly specified via `--language markdown` when generating project config, primarily useful for documentation-heavy projects)
+1
View File
@@ -281,6 +281,7 @@ markers = [
"rego: language server running for Rego",
"markdown: language server running for Markdown",
"julia: Julia language server tests",
"haskell: Haskell language server tests",
]
[tool.codespell]
@@ -0,0 +1,388 @@
"""
Provides Haskell specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Haskell.
"""
import logging
import os
import pathlib
import shutil
import time
from overrides import override
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
class HaskellLanguageServer(SolidLanguageServer):
"""
Provides Haskell specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Haskell.
Uses Haskell Language Server (HLS) for LSP functionality.
"""
@staticmethod
def _ensure_hls_installed():
"""Ensure haskell-language-server-wrapper is available."""
# Try common locations
common_paths = [
shutil.which("haskell-language-server-wrapper"),
"/opt/homebrew/bin/haskell-language-server-wrapper",
"/usr/local/bin/haskell-language-server-wrapper",
os.path.expanduser("~/.ghcup/bin/haskell-language-server-wrapper"),
os.path.expanduser("~/.cabal/bin/haskell-language-server-wrapper"),
os.path.expanduser("~/.local/bin/haskell-language-server-wrapper"),
]
# Check Stack programs directory
stack_programs = os.path.expanduser("~/.local/share/stack/programs")
if os.path.exists(stack_programs):
try:
for arch_dir in os.listdir(stack_programs):
arch_path = os.path.join(stack_programs, arch_dir)
if os.path.isdir(arch_path):
try:
for ghc_dir in os.listdir(arch_path):
hls_path = os.path.join(arch_path, ghc_dir, "bin", "haskell-language-server-wrapper")
if os.path.isfile(hls_path) and os.access(hls_path, os.X_OK):
common_paths.append(hls_path)
except (PermissionError, OSError):
# Skip directories we can't read
continue
except (PermissionError, OSError):
# Stack programs directory not accessible
pass
for path in common_paths:
if path and os.path.isfile(path) and os.access(path, os.X_OK):
return path
raise RuntimeError(
"haskell-language-server-wrapper is not installed or not in PATH.\n"
"Searched locations:\n" + "\n".join(f" - {p}" for p in common_paths if p) + "\n"
"Please install HLS via:\n"
" - GHCup: https://www.haskell.org/ghcup/\n"
" - Stack: stack install haskell-language-server\n"
" - Cabal: cabal install haskell-language-server\n"
" - Homebrew (macOS): brew install haskell-language-server"
)
def __init__(
self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
):
"""
Creates a HaskellLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
"""
hls_executable_path = self._ensure_hls_installed()
logger.log(f"Using haskell-language-server at: {hls_executable_path}", logging.INFO)
# Check if there's a haskell subdirectory with Stack/Cabal project
haskell_subdir = os.path.join(repository_root_path, "haskell")
if os.path.exists(haskell_subdir) and (
os.path.exists(os.path.join(haskell_subdir, "stack.yaml")) or os.path.exists(os.path.join(haskell_subdir, "cabal.project"))
):
working_dir = haskell_subdir
logger.log(f"Using Haskell project directory: {working_dir}", logging.INFO)
else:
working_dir = repository_root_path
# Set up environment with GHCup bin in PATH
env = dict(os.environ)
ghcup_bin = os.path.expanduser("~/.ghcup/bin")
if ghcup_bin not in env.get("PATH", ""):
env["PATH"] = f"{ghcup_bin}{os.pathsep}{env.get('PATH', '')}"
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=[hls_executable_path, "--lsp", "--cwd", working_dir], cwd=working_dir, env=env),
"haskell",
solidlsp_settings,
)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["dist", "dist-newstyle", ".stack-work", ".cabal-sandbox"]
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Haskell Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"clientInfo": {"name": "Serena", "version": "0.1.0"},
"locale": "en",
"capabilities": {
"workspace": {
"applyEdit": True,
"workspaceEdit": {
"documentChanges": True,
"resourceOperations": ["create", "rename", "delete"],
"failureHandling": "textOnlyTransactional",
"normalizesLineEndings": True,
"changeAnnotationSupport": {"groupsOnLabel": True},
},
"configuration": True,
"didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"tagSupport": {"valueSet": [1]},
"resolveSupport": {"properties": ["location.range"]},
},
"executeCommand": {"dynamicRegistration": True},
"didChangeConfiguration": {"dynamicRegistration": True},
"workspaceFolders": True,
"semanticTokens": {"refreshSupport": True},
},
"textDocument": {
"publishDiagnostics": {
"relatedInformation": True,
"versionSupport": False,
"tagSupport": {"valueSet": [1, 2]},
"codeDescriptionSupport": True,
"dataSupport": True,
},
"synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
"completion": {
"dynamicRegistration": True,
"contextSupport": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
"preselectSupport": True,
"tagSupport": {"valueSet": [1]},
"insertReplaceSupport": True,
"resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]},
"insertTextModeSupport": {"valueSet": [1, 2]},
"labelDetailsSupport": True,
},
"insertTextMode": 2,
"completionItemKind": {
"valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
"activeParameterSupport": True,
},
"contextSupport": True,
},
"definition": {"dynamicRegistration": True, "linkSupport": True},
"references": {"dynamicRegistration": True},
"documentHighlight": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
"hierarchicalDocumentSymbolSupport": True,
"tagSupport": {"valueSet": [1]},
"labelSupport": True,
},
"codeAction": {
"dynamicRegistration": True,
"isPreferredSupport": True,
"disabledSupport": True,
"dataSupport": True,
"resolveSupport": {"properties": ["edit"]},
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"",
"quickfix",
"refactor",
"refactor.extract",
"refactor.inline",
"refactor.rewrite",
"source",
"source.organizeImports",
]
}
},
"honorsChangeAnnotations": False,
},
"formatting": {"dynamicRegistration": True},
"rangeFormatting": {"dynamicRegistration": True},
"onTypeFormatting": {"dynamicRegistration": True},
"rename": {
"dynamicRegistration": True,
"prepareSupport": True,
"prepareSupportDefaultBehavior": 1,
"honorsChangeAnnotations": True,
},
"documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
"typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
"implementation": {"dynamicRegistration": True, "linkSupport": True},
"colorProvider": {"dynamicRegistration": True},
"foldingRange": {
"dynamicRegistration": True,
"rangeLimit": 5000,
"lineFoldingOnly": True,
"foldingRangeKind": {"valueSet": ["comment", "imports", "region"]},
},
"declaration": {"dynamicRegistration": True, "linkSupport": True},
"selectionRange": {"dynamicRegistration": True},
"callHierarchy": {"dynamicRegistration": True},
"semanticTokens": {
"dynamicRegistration": True,
"tokenTypes": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
],
"tokenModifiers": [
"declaration",
"definition",
"readonly",
"static",
"deprecated",
"abstract",
"async",
"modification",
"documentation",
"defaultLibrary",
],
"formats": ["relative"],
"requests": {"range": True, "full": {"delta": True}},
"multilineTokenSupport": False,
"overlappingTokenSupport": False,
},
"linkedEditingRange": {"dynamicRegistration": True},
},
"window": {
"showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
"showDocument": {"support": True},
"workDoneProgress": True,
},
"general": {
"staleRequestSupport": {"cancel": True, "retryOnContentModified": []},
"regularExpressions": {"engine": "ECMAScript", "version": "ES2020"},
"markdown": {
"parser": "marked",
"version": "1.1.0",
},
"positionEncodings": ["utf-16"],
},
},
"initializationOptions": {
"haskell": {
"formattingProvider": "ormolu",
"checkProject": True,
}
},
"trace": "verbose",
"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 Haskell Language Server
"""
def do_nothing(params):
return
def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
def register_capability_handler(params):
"""Handle dynamic capability registration from HLS"""
if "registrations" in params:
for registration in params.get("registrations", []):
method = registration.get("method", "")
self.logger.log(f"HLS registered capability: {method}", logging.INFO)
return
def workspace_configuration_handler(params):
"""Handle workspace/configuration requests from HLS"""
self.logger.log(f"HLS requesting configuration: {params}", logging.INFO)
# Configuration matching VS Code settings and initialization options
haskell_config = {
"formattingProvider": "ormolu",
"checkProject": True,
"plugin": {"importLens": {"codeActionsOn": False, "codeLensOn": False}, "hlint": {"codeActionsOn": False}},
}
# HLS expects array of config items matching requested sections
if isinstance(params, dict) and "items" in params:
result = []
for item in params["items"]:
section = item.get("section", "")
if section == "haskell":
result.append(haskell_config)
else:
result.append({})
self.logger.log(f"Returning configuration: {result}", logging.INFO)
return result
# Fallback: return single config
return [haskell_config]
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.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_request("workspace/configuration", workspace_configuration_handler)
self.logger.log("Starting Haskell Language 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)
# Log capabilities returned by HLS
capabilities = init_response.get("capabilities", {})
self.logger.log(f"HLS capabilities: {list(capabilities.keys())}", logging.INFO)
self.server.notify.initialized({})
self.completions_available.set()
# Give HLS time to index the project
# HLS can be slow to index, especially on first run
self.logger.log("Waiting for HLS to index project...", logging.INFO)
time.sleep(5)
self.logger.log("Haskell Language Server initialized successfully", logging.INFO)
+7
View File
@@ -58,6 +58,7 @@ class Language(str, Enum):
REGO = "rego"
SCALA = "scala"
JULIA = "julia"
HASKELL = "haskell"
# 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"""
@@ -158,6 +159,8 @@ class Language(str, Enum):
return FilenameMatcher("*.scala", "*.sbt")
case self.JULIA:
return FilenameMatcher("*.jl")
case self.HASKELL:
return FilenameMatcher("*.hs", "*.lhs")
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -291,6 +294,10 @@ class Language(str, Enum):
from solidlsp.language_servers.julia_server import JuliaLanguageServer
return JuliaLanguageServer
case self.HASKELL:
from solidlsp.language_servers.haskell_language_server import HaskellLanguageServer
return HaskellLanguageServer
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -0,0 +1,26 @@
module Main (main) where
import Calculator
import Helper
main :: IO ()
main = do
let calc = Calculator "TestCalc" 1
putStrLn $ "Using " ++ calcName calc ++ " version " ++ show (calcVersion calc)
-- Test add function (cross-file reference)
let result1 = add 5 3
putStrLn $ "5 + 3 = " ++ show result1
-- Test subtract (uses validateNumber from Helper)
let result2 = Calculator.subtract 10 4
putStrLn $ "10 - 4 = " ++ show result2
-- Test calculate function
case calculate calc "multiply" 6 7 of
Just result -> putStrLn $ "6 * 7 = " ++ show result
Nothing -> putStrLn "Calculation failed"
-- Test helper functions directly
putStrLn $ "Is 5 positive? " ++ show (isPositive 5)
putStrLn $ "Absolute of -10: " ++ show (absolute (-10))
@@ -0,0 +1,45 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.38.1.
--
-- see: https://github.com/sol/hpack
name: haskell-test-repo
version: 0.1.0.0
homepage: https://github.com/test/haskell-test-repo#readme
bug-reports: https://github.com/test/haskell-test-repo/issues
author: Test Author
maintainer: test@example.com
license: BSD3
build-type: Simple
source-repository head
type: git
location: https://github.com/test/haskell-test-repo
library
exposed-modules:
Calculator
Helper
other-modules:
Paths_haskell_test_repo
hs-source-dirs:
src
default-extensions:
OverloadedStrings
build-depends:
base >=4.7 && <5
default-language: Haskell2010
executable haskell-test-repo-exe
main-is: Main.hs
other-modules:
Paths_haskell_test_repo
hs-source-dirs:
app
default-extensions:
OverloadedStrings
build-depends:
base >=4.7 && <5
, haskell-test-repo
default-language: Haskell2010
@@ -0,0 +1,25 @@
name: haskell-test-repo
version: 0.1.0.0
github: "test/haskell-test-repo"
license: BSD3
author: "Test Author"
maintainer: "test@example.com"
dependencies:
- base >= 4.7 && < 5
library:
source-dirs: src
exposed-modules:
- Calculator
- Helper
executables:
haskell-test-repo-exe:
main: Main.hs
source-dirs: app
dependencies:
- haskell-test-repo
default-extensions:
- OverloadedStrings
@@ -0,0 +1,43 @@
module Calculator
( Calculator(..)
, add
, subtract
, multiply
, divide
, calculate
) where
import Prelude hiding (subtract)
import Helper (validateNumber)
-- | A simple calculator data type
data Calculator = Calculator
{ calcName :: String
, calcVersion :: Int
} deriving (Show, Eq)
-- | Add two numbers
add :: Int -> Int -> Int
add x y = validateNumber x + validateNumber y
-- | Subtract two numbers
subtract :: Int -> Int -> Int
subtract x y = validateNumber x - validateNumber y
-- | Multiply two numbers
multiply :: Int -> Int -> Int
multiply x y = x * y
-- | Divide two numbers (returns Maybe to handle division by zero)
divide :: Int -> Int -> Maybe Int
divide _ 0 = Nothing
divide x y = Just (x `div` y)
-- | Perform a calculation based on operator
calculate :: Calculator -> String -> Int -> Int -> Maybe Int
calculate calc op x y = case op of
"add" -> Just (add x y)
"subtract" -> Just (subtract x y)
"multiply" -> Just (multiply x y)
"divide" -> divide x y
_ -> Nothing
@@ -0,0 +1,22 @@
module Helper
( validateNumber
, isPositive
, isNegative
, absolute
) where
-- | Validate that a number is not zero (for demonstration)
validateNumber :: Int -> Int
validateNumber x = if x == 0 then error "Zero not allowed" else x
-- | Check if a number is positive
isPositive :: Int -> Bool
isPositive x = x > 0
-- | Check if a number is negative
isNegative :: Int -> Bool
isNegative x = x < 0
-- | Get absolute value
absolute :: Int -> Int
absolute x = if isNegative x then negate x else x
@@ -0,0 +1,5 @@
resolver: ghc-9.8.4
system-ghc: true
install-ghc: false
packages:
- .
+1
View File
@@ -0,0 +1 @@
# Haskell language server tests
+219
View File
@@ -0,0 +1,219 @@
"""
Rigorous tests for Haskell Language Server integration with Serena.
Tests prove that Serena's symbol tools can:
1. Discover all expected symbols with precise matching
2. Track cross-file references accurately
3. Identify data type structures and record fields
4. Navigate between definitions and usages
Test Repository Structure:
- src/Calculator.hs: Calculator data type, arithmetic functions (add, subtract, multiply, divide, calculate)
- src/Helper.hs: Helper functions (validateNumber, isPositive, isNegative, absolute)
- app/Main.hs: Main entry point using Calculator and Helper modules
"""
import sys
import pytest
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language
@pytest.mark.haskell
@pytest.mark.skipif(sys.platform == "win32", reason="HLS not installed on Windows CI")
class TestHaskellLanguageServer:
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_calculator_module_symbols(self, language_server: SolidLanguageServer):
"""
Test precise symbol discovery in Calculator.hs.
Verifies that Serena can identify:
- Data type definition (Calculator with record fields)
- All exported functions with correct names
- Module structure
"""
all_symbols, _ = language_server.request_document_symbols("src/Calculator.hs")
symbol_names = {s["name"] for s in all_symbols}
# Verify exact set of expected top-level symbols
expected_symbols = {
"Calculator", # Data type
"add", # Function: Int -> Int -> Int
"subtract", # Function: Int -> Int -> Int
"multiply", # Function: Int -> Int -> Int
"divide", # Function: Int -> Int -> Maybe Int
"calculate", # Function: Calculator -> String -> Int -> Int -> Maybe Int
}
# Verify all expected symbols are present
missing = expected_symbols - symbol_names
assert not missing, f"Missing expected symbols in Calculator.hs: {missing}"
# Verify Calculator data type exists
calculator_symbol = next((s for s in all_symbols if s["name"] == "Calculator"), None)
assert calculator_symbol is not None, "Calculator data type not found"
# The Calculator should be identified as a data type
# HLS may use different SymbolKind values (1=File, 5=Class, 23=Struct)
assert calculator_symbol["kind"] in [
1,
5,
23,
], f"Calculator should be a data type (kind 1, 5, or 23), got kind {calculator_symbol['kind']}"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_helper_module_symbols(self, language_server: SolidLanguageServer):
"""
Test precise symbol discovery in Helper.hs.
Verifies Serena identifies all helper functions that are imported
and used by Calculator module.
"""
all_symbols, _ = language_server.request_document_symbols("src/Helper.hs")
symbol_names = {s["name"] for s in all_symbols}
# Verify expected helper functions (module name may also appear)
expected_symbols = {
"validateNumber", # Function used by Calculator.add and Calculator.subtract
"isPositive", # Predicate function
"isNegative", # Predicate function used by absolute
"absolute", # Function that uses isNegative
}
# All expected symbols should be present (module name is optional)
missing = expected_symbols - symbol_names
assert not missing, f"Missing expected symbols in Helper.hs: {missing}"
# Verify no unexpected symbols beyond the module name
extra = symbol_names - expected_symbols - {"Helper"}
assert not extra, f"Unexpected symbols in Helper.hs: {extra}"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_main_module_imports(self, language_server: SolidLanguageServer):
"""
Test that Main.hs properly references both Calculator and Helper modules.
Verifies Serena can identify cross-module dependencies.
"""
all_symbols, _ = language_server.request_document_symbols("app/Main.hs")
symbol_names = {s["name"] for s in all_symbols}
# Main.hs should have the main function
assert "main" in symbol_names, "Main.hs should contain 'main' function"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_cross_file_references_validateNumber(self, language_server: SolidLanguageServer):
"""
Test cross-file reference tracking for validateNumber function.
validateNumber is defined in Helper.hs:9 and used in:
- Calculator.hs:21 (in add function)
- Calculator.hs:25 (in subtract function)
This proves Serena can track function usage across module boundaries.
"""
# Get references to validateNumber (defined at line 9, 0-indexed = line 8)
references = language_server.request_references("src/Helper.hs", line=8, column=0)
# Should find at least: definition in Helper.hs + 2 usages in Calculator.hs
assert len(references) >= 2, f"Expected at least 2 references to validateNumber (used in add and subtract), got {len(references)}"
# Verify we have references in Calculator.hs
reference_paths = [ref["relativePath"] for ref in references]
calculator_refs = [path for path in reference_paths if "Calculator.hs" in path]
assert len(calculator_refs) >= 2, (
f"Expected at least 2 references in Calculator.hs (add and subtract functions), "
f"got {len(calculator_refs)} references in Calculator.hs"
)
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_within_file_references_isNegative(self, language_server: SolidLanguageServer):
"""
Test within-file reference tracking for isNegative function.
isNegative is defined in Helper.hs:17 and used in Helper.hs:22 (absolute function).
This proves Serena can track intra-module function calls.
"""
# isNegative defined at line 17 (0-indexed = line 16)
references = language_server.request_references("src/Helper.hs", line=16, column=0)
# Should find: definition + usage in absolute function
assert len(references) >= 1, f"Expected at least 1 reference to isNegative (used in absolute), got {len(references)}"
# All references should be in Helper.hs
reference_paths = [ref["relativePath"] for ref in references]
assert all(
"Helper.hs" in path for path in reference_paths
), f"All isNegative references should be in Helper.hs, got: {reference_paths}"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_function_references_from_main(self, language_server: SolidLanguageServer):
"""
Test that functions used in Main.hs can be traced back to their definitions.
Main.hs:12 calls 'add' from Calculator module.
Main.hs:25 calls 'isPositive' from Helper module.
Main.hs:26 calls 'absolute' from Helper module.
This proves Serena can track cross-module function calls from executable code.
"""
# Test 'add' function references (defined in Calculator.hs:20, 0-indexed = line 19)
add_refs = language_server.request_references("src/Calculator.hs", line=19, column=0)
# Should find references in Main.hs and possibly Calculator.hs (calculate function uses it)
assert len(add_refs) >= 1, f"Expected at least 1 reference to 'add', got {len(add_refs)}"
add_ref_paths = [ref["relativePath"] for ref in add_refs]
# Should have at least one reference in Main.hs or Calculator.hs
assert any(
"Main.hs" in path or "Calculator.hs" in path for path in add_ref_paths
), f"Expected 'add' to be referenced in Main.hs or Calculator.hs, got: {add_ref_paths}"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_multiply_function_usage_in_calculate(self, language_server: SolidLanguageServer):
"""
Test that multiply function usage is tracked within Calculator module.
multiply is defined in Calculator.hs:28 and used in:
- Calculator.hs:41 (in calculate function via pattern matching)
- Main.hs:20 (via calculate call with "multiply" operator)
This proves Serena can track function references even when called indirectly.
"""
# multiply defined at line 28 (0-indexed = line 27)
multiply_refs = language_server.request_references("src/Calculator.hs", line=27, column=0)
# Should find at least the usage in calculate function
assert len(multiply_refs) >= 1, f"Expected at least 1 reference to 'multiply', got {len(multiply_refs)}"
# Should have reference in Calculator.hs (calculate function)
multiply_ref_paths = [ref["relativePath"] for ref in multiply_refs]
assert any(
"Calculator.hs" in path for path in multiply_ref_paths
), f"Expected 'multiply' to be referenced in Calculator.hs, got: {multiply_ref_paths}"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
def test_data_type_constructor_references(self, language_server: SolidLanguageServer):
"""
Test that Calculator data type constructor usage is tracked.
Calculator is defined in Calculator.hs:14 and used in:
- Main.hs:8 (constructor call: Calculator "TestCalc" 1)
- Calculator.hs:37 (type signature for calculate function)
This proves Serena can track data type constructor references.
"""
# Calculator data type defined at line 14 (0-indexed = line 13)
calculator_refs = language_server.request_references("src/Calculator.hs", line=13, column=5)
# Should find usage in Main.hs
assert len(calculator_refs) >= 1, f"Expected at least 1 reference to Calculator constructor, got {len(calculator_refs)}"
# Should have at least one reference in Main.hs or Calculator.hs
calc_ref_paths = [ref["relativePath"] for ref in calculator_refs]
assert any(
"Main.hs" in path or "Calculator.hs" in path for path in calc_ref_paths
), f"Expected Calculator to be referenced in Main.hs or Calculator.hs, got: {calc_ref_paths}"