server, CI: use ormolu as a formatter for Haskell sources

### Description

- sets up a Makefile target for running ormolu to format and check source code
- updates CI to run ormolu instead of stylish-haskell (and to check instead of format actively)

Compare #1679.

Here's the plan for merging this:
1. merge this PR; at this point, all PRs will fail CI unless they have the `ignore-server-format-checks` label set
2. merge follow-up PR #2404 that does nothing but actually reformats the codebase
3. tag the merge commit as `post-ormolu` (also on `graphql-engine`, for the benefits of community contributors)
4. provide the following script to any devs in order to update their branches:
   ```
   $ git checkout my-feature-branch
   $ git merge post-ormolu^
   $ make format
   $ git commit -a -m "reformat with ormolu"
   $ git merge -s ours post-ormolu
   ```
   (I'll put this in the commit message)

https://github.com/hasura/graphql-engine-mono/pull/2020

Co-authored-by: Philip Lykke Carlsen <358550+plcplc@users.noreply.github.com>
Co-authored-by: Swann Moreau <62569634+evertedsphere@users.noreply.github.com>
GitOrigin-RevId: 130f480a6d79967c8d045b7f3a6dec30b10472a7
This commit is contained in:
Robert 2021-09-23 23:22:27 +02:00 committed by hasura-bot
parent f1daabab04
commit a206d04062
7 changed files with 99 additions and 546 deletions

View File

@ -1,34 +0,0 @@
name: server format
on:
label:
pull_request:
paths:
- 'server/**'
jobs:
stylish-haskell:
runs-on: ubuntu-latest
if: "!contains(github.event.pull_request.labels.*.name, 'ignore-server-format-checks') && github.event.label.name != 'ignore-server-format-checks'"
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Run stylish-haskell
shell: bash
run: |
if git show -q --pretty=format:"%s" | grep -q "\[no-format\]"; then
echo "Found [no-format], skipping."
else
CHANGED_HS_FILES=$(git diff --name-only origin/${{github.base_ref}}...${{github.sha}} -- "./server/*.hs" | xargs -i -d '\n' sh -c 'ls -d {} 2>/dev/null || true' | sed 's%^server/%%')
echo "Changed Haskell files are:"
echo "$CHANGED_HS_FILES"
cd server
curl -sL https://raw.github.com/jaspervdj/stylish-haskell/master/scripts/latest.sh | sh -s $(echo $CHANGED_HS_FILES) -i
if [ -n "$(git status --porcelain)" ] ; then
echo "Files that needed formatting:"
git diff --name-only --diff-filter=M
for hfile in $(git status --porcelain | sed -n 's/^ M //p'); do
echo "::warning file=server/$hfile::This file needs to be formatted using stylish-haskell.\nRun `cd server && stylish-haskell $hfile` to do so."
done
fi
fi

52
Makefile Normal file
View File

@ -0,0 +1,52 @@
# skip contrib with its generated .hs file because it doesn't
# come with a cabal file, which can trigger a bug in ormolu
FORMAT_HS_FILES = $(shell git ls-files '*.hs' '*.hs-boot' | grep -v '^contrib/')
FORMAT_CHANGED_HS_FILES = $(shell git diff --diff-filter=d --name-only `git merge-base HEAD origin/main` \
| grep '.*\(hs\|hs-boot\)$$' | grep -v '^contrib/')
ORMOLU_CHECK_VERSION = 0.3.0.0
ORMOLU_ARGS = --cabal-default-extensions
ORMOLU = ormolu
ORMOLU_VERSION = $(shell $(ORMOLU) --version | awk 'NR==1 { print $$2 }')
# default target
.PHONY: help
## help: prints help message
help:
@echo "Usage:"
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
.PHONY: check-ormolu-version
check-ormolu-version:
@if ! [ "$(ORMOLU_VERSION)" = "$(ORMOLU_CHECK_VERSION)" ]; then \
echo "WARNING: ormolu version mismatch, expected $(ORMOLU_CHECK_VERSION)"; \
fi
.PHONY: format-hs
## format-hs: auto-format Haskell source code using ormolu
format-hs: check-ormolu-version
@echo running ormolu --mode inplace
@$(ORMOLU) $(ORMOLU_ARGS) --mode inplace $(FORMAT_HS_FILES)
.PHONY: format-hs-changed
## format-hs-changed: auto-format Haskell source code using ormolu (changed files only)
format-hs-changed: check-ormolu-version
@echo running ormolu --mode inplace
@if [ -n "$(FORMAT_CHANGED_HS_FILES)" ]; then \
$(ORMOLU) $(ORMOLU_ARGS) --mode inplace $(FORMAT_CHANGED_HS_FILES); \
fi
.PHONY: check-format-hs
## check-format-hs: check Haskell source code formatting using ormolu
check-format-hs: check-ormolu-version
@echo running ormolu --mode check
@$(ORMOLU) $(ORMOLU_ARGS) --mode check $(FORMAT_HS_FILES)
.PHONY: format
format: format-hs
.PHONY: format-changed
format-changed: format-hs-changed
.PHONY: check-format
check-format: check-format-hs

View File

@ -1,395 +0,0 @@
# stylish-haskell configuration file
# ==================================
# The stylish-haskell tool is mainly configured by specifying steps. These steps
# are a list, so they have an order, and one specific step may appear more than
# once (if needed). Each file is processed by these steps in the given order.
steps:
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
# by default.
# - unicode_syntax:
# # In order to make this work, we also need to insert the UnicodeSyntax
# # language pragma. If this flag is set to true, we insert it when it's
# # not already present. You may want to disable it if you configure
# # language extensions using some other method than pragmas. Default:
# # true.
# add_language_pragma: true
# Format module header
#
# Currently, this option is not configurable and will format all exports and
# module declarations to minimize diffs
#
# - module_header:
# # How many spaces use for indentation in the module header.
# indent: 4
#
# # Should export lists be sorted? Sorting is only performed within the
# # export section, as delineated by Haddock comments.
# sort: true
#
# # See `separate_lists` for the `imports` step.
# separate_lists: true
# Format record definitions. This is disabled by default.
#
# You can control the layout of record fields. The only rules that can't be configured
# are these:
#
# - "|" is always aligned with "="
# - "," in fields is always aligned with "{"
# - "}" is likewise always aligned with "{"
#
# - records:
# # How to format equals sign between type constructor and data constructor.
# # Possible values:
# # - "same_line" -- leave "=" AND data constructor on the same line as the type constructor.
# # - "indent N" -- insert a new line and N spaces from the beginning of the next line.
# equals: "indent 2"
#
# # How to format first field of each record constructor.
# # Possible values:
# # - "same_line" -- "{" and first field goes on the same line as the data constructor.
# # - "indent N" -- insert a new line and N spaces from the beginning of the data constructor
# first_field: "indent 2"
#
# # How many spaces to insert between the column with "," and the beginning of the comment in the next line.
# field_comment: 0
#
# # How many spaces to insert before "deriving" clause. Deriving clauses are always on separate lines.
# deriving: 2
#
# # How many spaces to insert before "via" clause counted from indentation of deriving clause
# # Possible values:
# # - "same_line" -- "via" part goes on the same line as "deriving" keyword.
# # - "indent N" -- insert a new line and N spaces from the beginning of "deriving" keyword.
# via: "indent 2"
#
# # Sort typeclass names in the "deriving" list alphabetically.
# sort_deriving: false
#
# # Wheter or not to break enums onto several lines
# #
# # Default: false
# break_enums: true
#
# # Whether or not to break single constructor data types before `=` sign
# #
# # Default: true
# break_single_constructors: false
#
# # Whether or not to curry constraints on function.
# #
# # E.g: @allValues :: Enum a => Bounded a => Proxy a -> [a]@
# #
# # Instead of @allValues :: (Enum a, Bounded a) => Proxy a -> [a]@
# #
# # Default: false
# curried_context: false
# Align the right hand side of some elements. This is quite conservative
# and only applies to statements where each element occupies a single
# line.
# Possible values:
# - always - Always align statements.
# - adjacent - Align statements that are on adjacent lines in groups.
# - never - Never align statements.
# All default to always.
- simple_align:
cases: always
top_level_patterns: always
records: always
multi_way_if: always
# Import cleanup
- imports:
# There are different ways we can align names and lists.
#
# - global: Align the import names and import list throughout the entire
# file.
#
# - file: Like global, but don't add padding when there are no qualified
# imports in the file.
#
# - group: Only align the imports per group (a group is formed by adjacent
# import lines).
#
# - none: Do not perform any alignment.
#
# Default: global.
align: global
# The following options affect only import list alignment.
#
# List align has following options:
#
# - after_alias: Import list is aligned with end of import including
# 'as' and 'hiding' keywords.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_alias: Import list is aligned with start of alias or hiding.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_module_name: Import list is aligned `list_padding` spaces after
# the module name.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# init, last, length)
#
# This is mainly intended for use with `pad_module_names: false`.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# init, last, length, scanl, scanr, take, drop,
# sort, nub)
#
# - new_line: Import list starts always on new line.
#
# > import qualified Data.List as List
# > (concat, foldl, foldr, head, init, last, length)
#
# - repeat: Repeat the module name to align the import list.
#
# > import qualified Data.List as List (concat, foldl, foldr, head)
# > import qualified Data.List as List (init, last, length)
#
# Default: after_alias
list_align: after_alias
# Right-pad the module names to align imports in a group:
#
# - true: a little more readable
#
# > import qualified Data.List as List (concat, foldl, foldr,
# > init, last, length)
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
# > init, last, length)
#
# - false: diff-safe
#
# > import qualified Data.List as List (concat, foldl, foldr, init,
# > last, length)
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
# > init, last, length)
#
# Default: true
pad_module_names: true
# Long list align style takes effect when import is too long. This is
# determined by 'columns' setting.
#
# - inline: This option will put as much specs on same line as possible.
#
# - new_line: Import list will start on new line.
#
# - new_line_multiline: Import list will start on new line when it's
# short enough to fit to single line. Otherwise it'll be multiline.
#
# - multiline: One line per import list entry.
# Type with constructor list acts like single import.
#
# > import qualified Data.Map as M
# > ( empty
# > , singleton
# > , ...
# > , delete
# > )
#
# Default: inline
long_list_align: inline
# Align empty list (importing instances)
#
# Empty list align has following options
#
# - inherit: inherit list_align setting
#
# - right_after: () is right after the module name:
#
# > import Vector.Instances ()
#
# Default: inherit
empty_list_align: inherit
# List padding determines indentation of import list on lines after import.
# This option affects 'long_list_align'.
#
# - <integer>: constant value
#
# - module_name: align under start of module name.
# Useful for 'file' and 'group' align settings.
#
# Default: 4
list_padding: 4
# Separate lists option affects formatting of import list for type
# or class. The only difference is single space between type and list
# of constructors, selectors and class functions.
#
# - true: There is single space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
#
# - false: There is no space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
#
# Default: true
separate_lists: true
# Space surround option affects formatting of import lists on a single
# line. The only difference is single space after the initial
# parenthesis and a single space before the terminal parenthesis.
#
# - true: There is single space associated with the enclosing
# parenthesis.
#
# > import Data.Foo ( foo )
#
# - false: There is no space associated with the enclosing parenthesis
#
# > import Data.Foo (foo)
#
# Default: false
space_surround: false
# Enabling this argument will use the new GHC lib parse to format imports.
#
# This currently assumes a few things, it will assume that you want post
# qualified imports. It is also not as feature complete as the old
# imports formatting.
#
# It does not remove redundant lines or merge lines. As such, the full
# feature scope is still pending.
#
# It _is_ however, a fine alternative if you are using features that are
# not parseable by haskell src extensions and you're comfortable with the
# presets.
#
# Default: false
ghc_lib_parser: false
# Language pragmas
- language_pragmas:
# We can generate different styles of language pragma lists.
#
# - vertical: Vertical-spaced language pragmas, one per line.
#
# - compact: A more compact style.
#
# - compact_line: Similar to compact, but wrap each line with
# `{-#LANGUAGE #-}'.
#
# Default: vertical.
style: vertical
# Align affects alignment of closing pragma brackets.
#
# - true: Brackets are aligned in same column.
#
# - false: Brackets are not aligned together. There is only one space
# between actual import and closing bracket.
#
# Default: true
align: true
# stylish-haskell can detect redundancy of some language pragmas. If this
# is set to true, it will remove those redundant pragmas. Default: true.
remove_redundant: true
# Language prefix to be used for pragma declaration, this allows you to
# use other options non case-sensitive like "language" or "Language".
# If a non correct String is provided, it will default to: LANGUAGE.
language_prefix: LANGUAGE
# Replace tabs by spaces. This is disabled by default.
# - tabs:
# # Number of spaces to use for each tab. Default: 8, as specified by the
# # Haskell report.
# spaces: 8
# Remove trailing whitespace
- trailing_whitespace: {}
# Squash multiple spaces between the left and right hand sides of some
# elements into single spaces. Basically, this undoes the effect of
# simple_align but is a bit less conservative.
# - squash: {}
# A common setting is the number of columns (parts of) code will be wrapped
# to. Different steps take this into account.
#
# Set this to null to disable all line wrapping.
#
# Default: 80.
columns: 100
# By default, line endings are converted according to the OS. You can override
# preferred format here.
#
# - native: Native newline format. CRLF on Windows, LF on other OSes.
#
# - lf: Convert to LF ("\n").
#
# - crlf: Convert to CRLF ("\r\n").
#
# Default: native.
newline: native
# Sometimes, language extensions are specified in a cabal file or from the
# command line instead of using language pragmas in the file. stylish-haskell
# needs to be aware of these, so it can parse the file correctly.
#
# No language extensions are enabled by default.
language_extensions:
- ApplicativeDo
- BangPatterns
- BlockArguments
- ConstraintKinds
- DataKinds
- DefaultSignatures
- DeriveDataTypeable
- DeriveFoldable
- DeriveFunctor
- DeriveGeneric
- DeriveLift
- DeriveTraversable
- DerivingVia
- EmptyCase
- ExistentialQuantification
- FlexibleContexts
- FlexibleInstances
- FunctionalDependencies
- GADTs
- GeneralizedNewtypeDeriving
- InstanceSigs
- LambdaCase
- MultiParamTypeClasses
- MultiWayIf
- NamedFieldPuns
- NoImplicitPrelude
- OverloadedStrings
- QuantifiedConstraints
- QuasiQuotes
- RankNTypes
- RecordWildCards
- RoleAnnotations
- ScopedTypeVariables
- StandaloneDeriving
- TemplateHaskell
- TupleSections
- TypeApplications
- TypeFamilies
- TypeFamilyDependencies
- TypeOperators
# Attempt to find the cabal file in ancestors of the current directory, and
# parse options (currently only language extensions) from that.
#
# Default: true
cabal: true

View File

@ -301,9 +301,17 @@ The backend-specific and common test suites are disjoint; for example, run `pyte
## Code conventions
This helps enforce a uniform style for all committers.
The following conventions help us maintain a uniform style for all committers:
make sure your contributions are in line with them.
- Compiler warnings are turned on, make sure your code has no warnings.
- Use [hlint](https://github.com/ndmitchell/hlint) to make sure your code has no warnings.
You can use our custom hlint config with `$ hlint --hint=server/.hlint.yaml .`
- Use [stylish-haskell](https://github.com/jaspervdj/stylish-haskell) to format your code.
We enforce these by means of CI hooks which will fail the build if any of these
are not met.
- No compiler warnings: Make sure your code builds with no warnings (adding
`-Werror` to `ghc-options` in your `cabal.project` is a good way of checking
this.)
- No lint failures: Use [hlint](https://github.com/ndmitchell/hlint) with our
custom config to validate your code, using `hlint --hint=server/.hlint.yaml`.
- Consistent formatting: Use [ormolu](https://github.com/tweag/ormolu) to
format your code. `ormolu -ei '*.hs'` will format all files with a `.hs`
extension in the current directory.

View File

@ -1,117 +1,29 @@
# Code conventions and style guide
This is a short document describing the preferred coding style for this project. We've tried to cover the major areas of formatting and naming. When something isn't covered by this guide, err on the side of consistency with existing code.
This is a short document describing the preferred coding style for this
project. We've tried to cover the major areas of formatting and naming. When
something isn't covered by this guide, err on the side of consistency with
existing code.
### Formatting
Use [stylish-haskell](https://github.com/jaspervdj/stylish-haskell) to format your code.
We use [ormolu](https://github.com/tweag/ormolu) to format our code. The
top-level `Makefile` has targets `format` and `check-format` that can be
used for this.
#### Line Length
We do not enforce a hard limit on line length, but we try to keep lines under 80 characters where possible to make it easier to tile files horizontally. Its okay if long lines spill into the 80100 character range if wrapping them would harm readability. In particular, we dont want to discourage descriptive variable names, nor do we want to discourage introducing local variables, so consider 80 characters a target length rather than a limit.
We do not enforce a hard limit on line length, but we try to keep lines under
80 characters where possible to make it easier to tile files horizontally.
Its okay if long lines spill into the 80100 character range if wrapping
them would harm readability. In particular, we dont want to discourage
descriptive variable names, nor do we want to discourage introducing local
variables, so consider 80 characters a target length rather than a limit.
One exception to the above is multiline comments, which should almost always be wrapped to 80 characters. Most editors support automatically reflowing blocks of text with a configurable line length; consider learning the relevant hotkey in your editor of choice.
#### Whitespace
Do not use tabs. Use spaces for indenting. Indent your code blocks with **2
spaces**. Indent the `where` keyword two spaces to set it apart from the rest of
the code and indent the definitions in a `where` clause 2 spaces. Some examples:
```haskell
sayHello :: IO ()
sayHello = do
name <- getLine
putStrLn $ greeting name
where
greeting name = "Hello, " ++ name ++ "!"
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
| p x = x : filter p xs
| otherwise = filter p xs
```
Include newlines at the ends of files. Do not include other trailing whitespace on the end of lines.
#### Data Declarations
Align the constructors in a data type definition. Example:
```haskell
data Tree a
= Branch !a !(Tree a) !(Tree a)
| Leaf
```
```haskell
data HttpException
= InvalidStatusCode Int
| MissingContentHeader
```
Format records as follows:
```haskell
data Person = Person
{ firstName :: !String -- ^ First name
, lastName :: !String -- ^ Last name
, age :: !Int -- ^ Age
} deriving (Eq, Show)
```
#### List Declarations
Align the elements in the list. Example:
```haskell
exceptions =
[ InvalidStatusCode
, MissingContentHeader
, InternalServerError
]
```
Optionally, you can skip the first newline. Use your judgement.
```haskell
directions = [ North
, East
, South
, West
]
```
#### Export Lists
Format export lists as follows:
```haskell
module Data.Set
( -- * The @Set@ type
Set
, empty
, singleton
-- * Querying
, member
) where
```
### Imports
Imports should be grouped in the following order:
1. `Hasura.Prelude`
2. `qualified` imports of standard library / third party imports
3. unqualified standard library imports / related third party imports
4. `qualified` imports of local application/library specific imports
5. unqualified local application/library specific imports
Put a blank line between each group of imports. The imports in each group should
be sorted alphabetically, by module name (this is done by `stylish-haskell`
automatically).
One exception to the above is multiline comments, which should almost always
be wrapped to 80 characters. Most editors support automatically reflowing
blocks of text with a configurable line length; consider learning the
relevant hotkey in your editor of choice.
### Naming
@ -306,7 +218,8 @@ Use line comments (comments that start with `--`) for short comments (13
lines). For longer comments, use multiline comments (comments that begin with
`{-` and end with `-}`).
Use [Haddock syntax](https://haskell-haddock.readthedocs.io/en/latest/markup.html) for documentation comments. Running Haddock should always complete successfully.
Use [Haddock syntax](https://haskell-haddock.readthedocs.io/en/latest/markup.html)
for documentation comments. Running Haddock should always complete successfully.
#### Module headers
@ -463,7 +376,11 @@ runSelectQuery tables constraints cache shouldPrepare =
#### Out-of-line `Note`s
For especially tricky details that deserve thorough explanation and may need to be referenced from multiple places, we emulate [GHCs Notes](https://www.stackbuilders.com/news/the-notes-of-ghc). A `Note` is an out-of-line comment written at the top-level of a module, written with a short title header:
For especially tricky details that deserve thorough explanation and may need
to be referenced from multiple places, we emulate [GHCs
Notes](https://www.stackbuilders.com/news/the-notes-of-ghc). A `Note` is an
out-of-line comment written at the top-level of a module, written with a
short title header:
```haskell
{- Note [Checking metadata consistency in run_sql]
@ -476,7 +393,8 @@ metadata to track the table under its new name instead of its old one.
... -}
```
At any point where the comment is relevant, we add a short comment referring to the note:
At any point where the comment is relevant, we add a short comment referring
to the note:
```haskell
-- see Note [Checking metadata consistency in run_sql]
@ -484,9 +402,14 @@ containsDDLKeyword :: Text -> Bool
containsDDLKeyword = TDFA.match $$(quoteRegex ...)
```
A key advantage of notes is that they can be referenced from multiple places, so information does not necessarily need to be connected to any particular binding the way it must be for Haddock comments.
A key advantage of notes is that they can be referenced from multiple places,
so information does not necessarily need to be connected to any particular
binding the way it must be for Haddock comments.
When updating a piece of code that includes a reference to a `Note`, take care to ensure the `Note` is updated as well if necessary! Inevitably, some will get stale and go out of sync with the code, but thats okay—just fix them up when you find some information is outdated.
When updating a piece of code that includes a reference to a `Note`, take
care to ensure the `Note` is updated as well if necessary! Inevitably, some
will get stale and go out of sync with the code, but thats okay—just fix
them up when you find some information is outdated.
### Misc

View File

@ -57,6 +57,7 @@ common common-all
FunctionalDependencies
GADTs
GeneralizedNewtypeDeriving
ImportQualifiedPost
InstanceSigs
LambdaCase
MultiParamTypeClasses

View File

@ -6,8 +6,6 @@ module Data.Tuple.Extended
, uncurry4
) where
import Prelude
import Data.Tuple
curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d