From 8b93e8eb3d46e7f5c9e4f7aea1c8c40d8471e66e Mon Sep 17 00:00:00 2001 From: Mads Hougesen Date: Thu, 21 Mar 2024 01:03:51 +0100 Subject: [PATCH] feat(haskell): add hindent and fourmolu (#98) * feat(haskell): add fourmolu * feat(haskell): support hindent * refactor(haskell): try fourmolo then hindent * ci: setup fourmolu and hindent --- .github/workflows/validate.yml | 13 +++ README.md | 1 + schemas/v0.0.2/mdsf.schema.json | 40 ++++++++++ src/config.rs | 8 +- src/formatters/fourmolu.rs | 44 +++++++++++ src/formatters/hindent.rs | 44 +++++++++++ src/formatters/mod.rs | 3 + src/languages/haskell.rs | 135 ++++++++++++++++++++++++++++++++ src/languages/mod.rs | 7 +- 9 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 src/formatters/fourmolu.rs create mode 100644 src/formatters/hindent.rs create mode 100644 src/languages/haskell.rs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 69efda24..1b10b7dc 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -97,6 +97,13 @@ jobs: java-version: "21" distribution: "temurin" + # fourmolo, hindent + - uses: haskell-actions/setup@v2 + with: + ghc-version: "latest" + cabal-version: "latest" + cabal-update: true + - run: rustup toolchain install stable --profile minimal - run: rustup component add rustfmt clippy @@ -181,5 +188,11 @@ jobs: - name: cljstyle run: curl -sLO https://raw.githubusercontent.com/greglook/cljstyle/main/util/install-cljstyle && chmod +x install-cljstyle && sudo ./install-cljstyle + - name: fourmolu + run: cabal install fourmolu + + - name: hindent + run: cabal install hindent + - name: run tests run: cargo test diff --git a/README.md b/README.md index 2b28efb7..bd5aaa51 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ mdsf init | Go | `gofmt`, `gofumpt`, `goimports` | | GraphQL | `prettier` | | Groovy | `npm-groovy-lint` | +| Haskell | `fourmolu`, `hindent` | | Html | `prettier` | | Java | `clang-format`, `google-java-format` | | JavaScript | `biome`, `clang-format`, `deno_fmt`, `prettier` | diff --git a/schemas/v0.0.2/mdsf.schema.json b/schemas/v0.0.2/mdsf.schema.json index f5906cac..839bb5fd 100644 --- a/schemas/v0.0.2/mdsf.schema.json +++ b/schemas/v0.0.2/mdsf.schema.json @@ -157,6 +157,17 @@ } ] }, + "haskell": { + "default": { + "enabled": true, + "formatter": [["fourmolu", "hindent"]] + }, + "allOf": [ + { + "$ref": "#/definitions/Lang_for_Haskell" + } + ] + }, "html": { "default": { "enabled": true, @@ -515,6 +526,10 @@ "type": "string", "enum": ["npm-groovy-lint"] }, + "Haskell": { + "type": "string", + "enum": ["fourmolu", "hindent"] + }, "Html": { "type": "string", "enum": ["prettier"] @@ -703,6 +718,18 @@ } } }, + "Lang_for_Haskell": { + "type": "object", + "required": ["enabled", "formatter"], + "properties": { + "enabled": { + "type": "boolean" + }, + "formatter": { + "$ref": "#/definitions/MdsfFormatter_for_Haskell" + } + } + }, "Lang_for_Html": { "type": "object", "required": ["enabled", "formatter"], @@ -1217,6 +1244,19 @@ } ] }, + "MdsfFormatter_for_Haskell": { + "anyOf": [ + { + "$ref": "#/definitions/Haskell" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/MdsfFormatter_for_Haskell" + } + } + ] + }, "MdsfFormatter_for_Html": { "anyOf": [ { diff --git a/src/config.rs b/src/config.rs index f55cbff4..cbc98421 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,8 +3,8 @@ use schemars::JsonSchema; use crate::languages::{ blade::Blade, c::C, clojure::Clojure, cpp::Cpp, crystal::Crystal, csharp::CSharp, css::Css, dart::Dart, elixir::Elixir, elm::Elm, gleam::Gleam, go::Go, graphql::GraphQL, groovy::Groovy, - html::Html, java::Java, javascript::JavaScript, json::Json, just::Just, lua::Lua, - markdown::Markdown, nim::Nim, objective_c::ObjectiveC, ocaml::OCaml, perl::Perl, + haskell::Haskell, html::Html, java::Java, javascript::JavaScript, json::Json, just::Just, + lua::Lua, markdown::Markdown, nim::Nim, objective_c::ObjectiveC, ocaml::OCaml, perl::Perl, protobuf::Protobuf, purescript::PureScript, python::Python, rescript::ReScript, roc::Roc, ruby::Ruby, rust::Rust, scala::Scala, shell::Shell, sql::Sql, toml::Toml, typescript::TypeScript, vue::Vue, xml::Xml, yaml::Yaml, zig::Zig, Lang, @@ -59,6 +59,9 @@ pub struct MdsfConfig { #[serde(default)] pub groovy: Lang, + #[serde(default)] + pub haskell: Lang, + #[serde(default)] pub html: Lang, @@ -161,6 +164,7 @@ impl Default for MdsfConfig { go: Lang::::default(), graphql: Lang::::default(), groovy: Lang::::default(), + haskell: Lang::::default(), html: Lang::::default(), java: Lang::::default(), javascript: Lang::::default(), diff --git a/src/formatters/fourmolu.rs b/src/formatters/fourmolu.rs new file mode 100644 index 00000000..63fff3ac --- /dev/null +++ b/src/formatters/fourmolu.rs @@ -0,0 +1,44 @@ +use super::execute_command; + +#[inline] +pub fn format_using_fourmolu( + snippet_path: &std::path::Path, +) -> std::io::Result<(bool, Option)> { + let mut cmd = std::process::Command::new("fourmolu"); + + cmd.arg("-i").arg(snippet_path); + + execute_command(&mut cmd, snippet_path) +} + +#[cfg(test)] +mod test_fourmolu { + use crate::{formatters::setup_snippet, languages::Language}; + + use super::format_using_fourmolu; + + #[test_with::executable(fourmolu)] + #[test] + fn it_should_format_haskell() { + let input = " +addNumbers::Int->Int->Int +addNumbers a b = do + a + b + "; + + let expected_output = "addNumbers :: Int -> Int -> Int +addNumbers a b = do + a + b +"; + + let snippet = setup_snippet(input, Language::Haskell.to_file_ext()) + .expect("it to create a snippet file"); + + let output = format_using_fourmolu(snippet.path()) + .expect("it to be successful") + .1 + .expect("it to be some"); + + assert_eq!(output, expected_output); + } +} diff --git a/src/formatters/hindent.rs b/src/formatters/hindent.rs new file mode 100644 index 00000000..a465586c --- /dev/null +++ b/src/formatters/hindent.rs @@ -0,0 +1,44 @@ +use super::execute_command; + +#[inline] +pub fn format_using_hindent( + snippet_path: &std::path::Path, +) -> std::io::Result<(bool, Option)> { + let mut cmd = std::process::Command::new("hindent"); + + cmd.arg(snippet_path); + + execute_command(&mut cmd, snippet_path) +} + +#[cfg(test)] +mod test_hindent { + use crate::{formatters::setup_snippet, languages::Language}; + + use super::format_using_hindent; + + #[test_with::executable(hindent)] + #[test] + fn it_should_format_haskell() { + let input = " +addNumbers::Int->Int->Int +addNumbers a b = do + a + b + "; + + let expected_output = "addNumbers :: Int -> Int -> Int +addNumbers a b = do + a + b +"; + + let snippet = setup_snippet(input, Language::Haskell.to_file_ext()) + .expect("it to create a snippet file"); + + let output = format_using_hindent(snippet.path()) + .expect("it to be successful") + .1 + .expect("it to be some"); + + assert_eq!(output, expected_output); + } +} diff --git a/src/formatters/mod.rs b/src/formatters/mod.rs index 43e752ce..7aba7d04 100644 --- a/src/formatters/mod.rs +++ b/src/formatters/mod.rs @@ -17,11 +17,13 @@ pub mod crystal_format; pub mod dart_format; pub mod deno_fmt; pub mod elm_format; +pub mod fourmolu; pub mod gleam_format; pub mod gofmt; pub mod gofumpt; pub mod goimports; pub mod google_java_format; +pub mod hindent; pub mod isort; pub mod just_fmt; pub mod mix_format; @@ -122,6 +124,7 @@ pub fn format_snippet(config: &MdsfConfig, language: &Language, code: &str) -> S Language::Go => config.go.format(snippet_path), Language::GraphQL => config.graphql.format(snippet_path), Language::Groovy => config.groovy.format(snippet_path), + Language::Haskell => config.haskell.format(snippet_path), Language::Html => config.html.format(snippet_path), Language::Java => config.java.format(snippet_path), Language::JavaScript => config.javascript.format(snippet_path), diff --git a/src/languages/haskell.rs b/src/languages/haskell.rs new file mode 100644 index 00000000..9adbacbf --- /dev/null +++ b/src/languages/haskell.rs @@ -0,0 +1,135 @@ +use schemars::JsonSchema; + +use crate::formatters::{ + fourmolu::format_using_fourmolu, hindent::format_using_hindent, MdsfFormatter, +}; + +use super::{Lang, LanguageFormatter}; + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub enum Haskell { + #[default] + #[serde(rename = "fourmolu")] + Fourmolu, + #[serde(rename = "hindent")] + HIndent, +} + +impl Default for Lang { + #[inline] + fn default() -> Self { + Self { + enabled: true, + formatter: MdsfFormatter::::default(), + } + } +} + +impl Default for MdsfFormatter { + #[inline] + fn default() -> Self { + Self::Multiple(vec![Self::Multiple(vec![ + Self::Single(Haskell::Fourmolu), + Self::Single(Haskell::HIndent), + ])]) + } +} + +impl LanguageFormatter for Haskell { + #[inline] + fn format_snippet( + &self, + snippet_path: &std::path::Path, + ) -> std::io::Result<(bool, Option)> { + match self { + Self::Fourmolu => format_using_fourmolu(snippet_path), + Self::HIndent => format_using_hindent(snippet_path), + } + } +} + +#[cfg(test)] +mod test_haskell { + use crate::{ + formatters::{setup_snippet, MdsfFormatter}, + languages::Lang, + }; + + use super::Haskell; + + const INPUT: &str = " +addNumbers::Int->Int->Int +addNumbers a b = do + a + b + "; + + const EXTENSION: &str = crate::languages::Language::Haskell.to_file_ext(); + + #[test] + fn it_should_be_enabled_by_default() { + assert!(Lang::::default().enabled); + } + + #[test] + fn it_should_not_format_when_enabled_is_false() { + let snippet = setup_snippet(INPUT, EXTENSION).expect("it to save the file"); + let snippet_path = snippet.path(); + + assert!(Lang:: { + enabled: false, + formatter: MdsfFormatter::Single(Haskell::default()), + } + .format(snippet_path) + .expect("it to not fail") + .is_none()); + } + + #[test_with::executable(fourmolu)] + #[test] + fn test_fourmolu() { + let l = Lang:: { + enabled: true, + formatter: MdsfFormatter::Single(Haskell::Fourmolu), + }; + + let snippet = setup_snippet(INPUT, EXTENSION).expect("it to save the file"); + let snippet_path = snippet.path(); + + let output = l + .format(snippet_path) + .expect("it to not fail") + .expect("it to be a snippet"); + + let expected_output = "addNumbers :: Int -> Int -> Int +addNumbers a b = do + a + b +"; + + assert_eq!(output, expected_output); + } + + #[test_with::executable(hindent)] + #[test] + fn test_hindent() { + let l = Lang:: { + enabled: true, + formatter: MdsfFormatter::Single(Haskell::HIndent), + }; + + let snippet = setup_snippet(INPUT, EXTENSION).expect("it to save the file"); + let snippet_path = snippet.path(); + + let output = l + .format(snippet_path) + .expect("it to not fail") + .expect("it to be a snippet"); + + let expected_output = "addNumbers :: Int -> Int -> Int +addNumbers a b = do + a + b +"; + + assert_eq!(output, expected_output); + } +} diff --git a/src/languages/mod.rs b/src/languages/mod.rs index 9cde934a..d1db1367 100644 --- a/src/languages/mod.rs +++ b/src/languages/mod.rs @@ -17,6 +17,7 @@ pub enum Language { Go, GraphQL, Groovy, + Haskell, Html, Java, JavaScript, @@ -44,7 +45,6 @@ pub enum Language { Xml, Yaml, Zig, - // TODO: Haskell, // TODO: PHP, // TODO: Kotlin, // TODO: FSharp, @@ -72,6 +72,7 @@ pub mod gleam; pub mod go; pub mod graphql; pub mod groovy; +pub mod haskell; pub mod html; pub mod java; pub mod javascript; @@ -124,7 +125,8 @@ impl Language { "go" | "golang" => Some(Self::Go), "graphql" | "gql" => Some(Self::GraphQL), "groovy" => Some(Self::Groovy), - "html" => Some(Self::Html), + "haskell" => Some(Self::Haskell), + "html" | "html5" => Some(Self::Html), "java" => Some(Self::Java), "javascript" | "js" | "jsx" => Some(Self::JavaScript), "json" | "jsonc" => Some(Self::Json), @@ -174,6 +176,7 @@ impl Language { Self::Go => ".go", Self::GraphQL => ".gql", Self::Groovy => ".groovy", + Self::Haskell => ".hs", Self::Html => ".html", Self::Java => ".java", Self::JavaScript => ".js",