Skip to content

Commit

Permalink
feat(haskell): add hindent and fourmolu (#98)
Browse files Browse the repository at this point in the history
* feat(haskell): add fourmolu

* feat(haskell): support hindent

* refactor(haskell): try fourmolo then hindent

* ci: setup fourmolu and hindent
  • Loading branch information
hougesen committed Mar 21, 2024
1 parent e73c0ac commit 8b93e8e
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 4 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/validate.yml
Expand Up @@ -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

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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` |
Expand Down
40 changes: 40 additions & 0 deletions schemas/v0.0.2/mdsf.schema.json
Expand Up @@ -157,6 +157,17 @@
}
]
},
"haskell": {
"default": {
"enabled": true,
"formatter": [["fourmolu", "hindent"]]
},
"allOf": [
{
"$ref": "#/definitions/Lang_for_Haskell"
}
]
},
"html": {
"default": {
"enabled": true,
Expand Down Expand Up @@ -515,6 +526,10 @@
"type": "string",
"enum": ["npm-groovy-lint"]
},
"Haskell": {
"type": "string",
"enum": ["fourmolu", "hindent"]
},
"Html": {
"type": "string",
"enum": ["prettier"]
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -1217,6 +1244,19 @@
}
]
},
"MdsfFormatter_for_Haskell": {
"anyOf": [
{
"$ref": "#/definitions/Haskell"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/MdsfFormatter_for_Haskell"
}
}
]
},
"MdsfFormatter_for_Html": {
"anyOf": [
{
Expand Down
8 changes: 6 additions & 2 deletions src/config.rs
Expand Up @@ -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,
Expand Down Expand Up @@ -59,6 +59,9 @@ pub struct MdsfConfig {
#[serde(default)]
pub groovy: Lang<Groovy>,

#[serde(default)]
pub haskell: Lang<Haskell>,

#[serde(default)]
pub html: Lang<Html>,

Expand Down Expand Up @@ -161,6 +164,7 @@ impl Default for MdsfConfig {
go: Lang::<Go>::default(),
graphql: Lang::<GraphQL>::default(),
groovy: Lang::<Groovy>::default(),
haskell: Lang::<Haskell>::default(),
html: Lang::<Html>::default(),
java: Lang::<Java>::default(),
javascript: Lang::<JavaScript>::default(),
Expand Down
44 changes: 44 additions & 0 deletions 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<String>)> {
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);
}
}
44 changes: 44 additions & 0 deletions 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<String>)> {
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);
}
}
3 changes: 3 additions & 0 deletions src/formatters/mod.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
135 changes: 135 additions & 0 deletions 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<Haskell> {
#[inline]
fn default() -> Self {
Self {
enabled: true,
formatter: MdsfFormatter::<Haskell>::default(),
}
}
}

impl Default for MdsfFormatter<Haskell> {
#[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<String>)> {
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::<Haskell>::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::<Haskell> {
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::<Haskell> {
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::<Haskell> {
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);
}
}

0 comments on commit 8b93e8e

Please sign in to comment.