Repository: pre-commit/pre-commit
Pull request #2932 (2023-07-18 17:50:45)

Add haskell language support to pre-commit.
I'm attempting to follow the other languages and documentation in CONTRIBUTING (which seems outdated) to add Haskell support to pre-commit so I can add hooks for several useful Haskell tools.

I'm running into issues when trying to run tests of my new language, and I'm trying to figure out where I'm going wrong. Can someone provide guidance?

I'm assuming the system has Haskell installed already, the same as Python and attempting to follow the documented interface in CONTRIBUTING as well as I can without adding too many things for a first pass. I'm hitting strange errors where my mock project can be listed, but it seems to be something other than the CWD for the installation (no project file found to do the install of '.').

Please let me know if there is more up-to-date documentation or something I should try. Once I pass the tests, I'll be ready for deeper integration with some tools to see how things go.

alunduil: @asottile, thanks for the pointers. Let me know if you have any other stylistic or architectural guidance. This is now passing, and I am confident I do what I need to start playing with other repositories to add hooks for this language.

Patch:
diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml
index 7e97158..5a6ae9c 100644
--- a/.github/workflows/languages.yaml
+++ b/.github/workflows/languages.yaml
@@ -63,6 +63,8 @@ jobs:
         echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH"
       shell: bash
       if: matrix.os == 'windows-latest' && matrix.language == 'perl'
+    - uses: haskell/actions/setup@v2
+      if: matrix.language == 'haskell'
 
     - name: install deps
       run: python -mpip install -e . -r requirements-dev.txt
diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py
index 2bed706..476bad9 100644
--- a/pre_commit/all_languages.py
+++ b/pre_commit/all_languages.py
@@ -9,6 +9,7 @@ from pre_commit.languages import docker_image
 from pre_commit.languages import dotnet
 from pre_commit.languages import fail
 from pre_commit.languages import golang
+from pre_commit.languages import haskell
 from pre_commit.languages import lua
 from pre_commit.languages import node
 from pre_commit.languages import perl
@@ -31,6 +32,7 @@ languages: dict[str, Language] = {
     'dotnet': dotnet,
     'fail': fail,
     'golang': golang,
+    'haskell': haskell,
     'lua': lua,
     'node': node,
     'perl': perl,
diff --git a/pre_commit/languages/haskell.py b/pre_commit/languages/haskell.py
new file mode 100644
index 0000000..76442eb
--- /dev/null
+++ b/pre_commit/languages/haskell.py
@@ -0,0 +1,56 @@
+from __future__ import annotations
+
+import contextlib
+import os.path
+from typing import Generator
+from typing import Sequence
+
+from pre_commit import lang_base
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.errors import FatalError
+from pre_commit.prefix import Prefix
+
+ENVIRONMENT_DIR = 'hs_env'
+get_default_version = lang_base.basic_get_default_version
+health_check = lang_base.basic_health_check
+run_hook = lang_base.basic_run_hook
+
+
+def get_env_patch(target_dir: str) -> PatchesT:
+    bin_path = os.path.join(target_dir, 'bin')
+    return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
+
+
+@contextlib.contextmanager
+def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
+    envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+    with envcontext(get_env_patch(envdir)):
+        yield
+
+
+def install_environment(
+    prefix: Prefix,
+    version: str,
+    additional_dependencies: Sequence[str],
+) -> None:
+    lang_base.assert_version_default('haskell', version)
+    envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
+
+    pkgs = [*prefix.star('.cabal'), *additional_dependencies]
+    if not pkgs:
+        raise FatalError('Expected .cabal files or additional_dependencies')
+
+    bindir = os.path.join(envdir, 'bin')
+    os.makedirs(bindir, exist_ok=True)
+    lang_base.setup_cmd(prefix, ('cabal', 'update'))
+    lang_base.setup_cmd(
+        prefix,
+        (
+            'cabal', 'install',
+            '--install-method', 'copy',
+            '--installdir', bindir,
+            *pkgs,
+        ),
+    )


Test patch:
diff --git a/tests/languages/haskell_test.py b/tests/languages/haskell_test.py
new file mode 100644
index 0000000..f888109
--- /dev/null
+++ b/tests/languages/haskell_test.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit.errors import FatalError
+from pre_commit.languages import haskell
+from pre_commit.util import win_exe
+from testing.language_helpers import run_language
+
+
+def test_run_example_executable(tmp_path):
+    example_cabal = '''\
+cabal-version:      2.4
+name:               example
+version:            0.1.0.0
+
+executable example
+    main-is:          Main.hs
+
+    build-depends:    base >=4
+    default-language: Haskell2010
+'''
+    main_hs = '''\
+module Main where
+
+main :: IO ()
+main = putStrLn "Hello, Haskell!"
+'''
+    tmp_path.joinpath('example.cabal').write_text(example_cabal)
+    tmp_path.joinpath('Main.hs').write_text(main_hs)
+
+    result = run_language(tmp_path, haskell, 'example')
+    assert result == (0, b'Hello, Haskell!\n')
+
+    # should not symlink things into environments
+    exe = tmp_path.joinpath(win_exe('hs_env-default/bin/example'))
+    assert exe.is_file()
+    assert not exe.is_symlink()
+
+
+def test_run_dep(tmp_path):
+    result = run_language(tmp_path, haskell, 'hello', deps=['hello'])
+    assert result == (0, b'Hello, World!\n')
+
+
+def test_run_empty(tmp_path):
+    with pytest.raises(FatalError) as excinfo:
+        run_language(tmp_path, haskell, 'example')
+    msg, = excinfo.value.args
+    assert msg == 'Expected .cabal files or additional_dependencies'