mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 12:17:02 +00:00
feat: Add Haskell language server support (#697)
This commit is contained in:
1 parent
1461aff18c
commit
37c4898bd6
15 files changed
+827
-1
No files matched your search
@@ -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
@@ -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.*
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
- .
|
||||
@@ -0,0 +1 @@
|
||||
# Haskell language server tests
|
||||
@@ -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}"
|
||||
Reference in new issue
Block a user