356 lines
12 KiB
YAML
356 lines
12 KiB
YAML
|
# HLint configuration file
|
|||
|
# https://github.com/ndmitchell/hlint
|
|||
|
# Run `hlint --default` to see the example configuration file.
|
|||
|
##########################
|
|||
|
|
|||
|
# WARNING: These need to be synced with the default-extensions field
|
|||
|
# in the cabal file.
|
|||
|
- arguments: [-XGHC2021, -XOverloadedRecordDot]
|
|||
|
|
|||
|
# Ignore some builtin hints
|
|||
|
|
|||
|
# often functions are more readable with explicit arguments
|
|||
|
- ignore: { name: Eta reduce }
|
|||
|
|
|||
|
# these redundancy warnings are just completely irrelevant
|
|||
|
- ignore: { name: Redundant bracket }
|
|||
|
- ignore: { name: Move brackets to avoid $ }
|
|||
|
- ignore: { name: Redundant $ }
|
|||
|
- ignore: { name: Redundant do }
|
|||
|
- ignore: { name: Redundant multi-way if }
|
|||
|
|
|||
|
# allow case-matching on bool, because why not
|
|||
|
- ignore: { name: Use if }
|
|||
|
|
|||
|
# hlint cannot distinguish actual newtypes from data types
|
|||
|
# that accidentally have only one field
|
|||
|
# (but might have more in the future).
|
|||
|
# Since it’s a mostly irrelevant runtime optimization, we don’t care.
|
|||
|
- ignore: { name: Use newtype instead of data }
|
|||
|
|
|||
|
# these lead to harder-to-read/more implicit code
|
|||
|
- ignore: { name: Use fmap }
|
|||
|
- ignore: { name: Use <$> }
|
|||
|
- ignore: { name: Use tuple-section }
|
|||
|
- ignore: { name: Use forM_ }
|
|||
|
# fst and snd are usually a code smell and should be explicit matches, _naming the ignored side.
|
|||
|
- ignore: { name: Use fst }
|
|||
|
- ignore: { name: Use snd }
|
|||
|
- ignore: { name: Use fromMaybe }
|
|||
|
- ignore: { name: Use const }
|
|||
|
- ignore: { name: Replace case with maybe }
|
|||
|
- ignore: { name: Replace case with fromMaybe }
|
|||
|
- ignore: { name: Avoid lambda }
|
|||
|
- ignore: { name: Avoid lambda using `infix` }
|
|||
|
- ignore: { name: Use curry }
|
|||
|
- ignore: { name: Use uncurry }
|
|||
|
- ignore: { name: Use first }
|
|||
|
- ignore: { name: Redundant first }
|
|||
|
- ignore: { name: Use second }
|
|||
|
- ignore: { name: Use bimap }
|
|||
|
# just use `not x`
|
|||
|
- ignore: { name: Use unless }
|
|||
|
- ignore: { name: Redundant <&> }
|
|||
|
|
|||
|
# list comprehensions are a seldomly used part of the Haskell language
|
|||
|
# and they introduce syntactic overhead that is usually not worth the conciseness
|
|||
|
- ignore: { name: Use list comprehension }
|
|||
|
|
|||
|
# Seems to be buggy in cases
|
|||
|
- ignore: { name: Use section }
|
|||
|
|
|||
|
# multiple maps in a row are usually used for clarity,
|
|||
|
# and the compiler will optimize them away, thank you very much.
|
|||
|
- ignore: { name: Use map once }
|
|||
|
- ignore: { name: Fuse foldr/map }
|
|||
|
- ignore: { name: Fuse traverse/map }
|
|||
|
- ignore: { name: Fuse traverse_/map }
|
|||
|
|
|||
|
# this is silly, why would I use a special function if I can just (heh) `== Nothing`
|
|||
|
- ignore: { name: Use isNothing }
|
|||
|
|
|||
|
# The duplication heuristic is not very smart
|
|||
|
# and more annoying than helpful.
|
|||
|
# see https://github.com/ndmitchell/hlint/issues/1009
|
|||
|
- ignore: { name: Reduce duplication }
|
|||
|
|
|||
|
# Stops the pattern match trick
|
|||
|
- ignore: { name: Use record patterns }
|
|||
|
- ignore: { name: Use null }
|
|||
|
- ignore: { name: Use uncurry }
|
|||
|
|
|||
|
# we don’t want void, see below
|
|||
|
- ignore: { name: Use void }
|
|||
|
|
|||
|
- functions:
|
|||
|
# disallow Enum instance functions, they are partial
|
|||
|
- name: Prelude.succ
|
|||
|
within: [Relude.Extra.Enum]
|
|||
|
message: "Dangerous, will fail for highest element"
|
|||
|
- name: Prelude.pred
|
|||
|
within: [Relude.Extra.Enum]
|
|||
|
message: "Dangerous, will fail for lowest element"
|
|||
|
- name: Prelude.toEnum
|
|||
|
within: []
|
|||
|
message: "Extremely partial"
|
|||
|
- name: Prelude.fromEnum
|
|||
|
within: []
|
|||
|
message: "Dangerous for most uses"
|
|||
|
- name: Prelude.enumFrom
|
|||
|
within: []
|
|||
|
- name: Prelude.enumFromThen
|
|||
|
within: []
|
|||
|
- name: Prelude.enumFromThenTo
|
|||
|
within: []
|
|||
|
- name: Prelude.oundedEnumFrom
|
|||
|
within: []
|
|||
|
- name: Prelude.boundedEnumFromThen
|
|||
|
within: []
|
|||
|
|
|||
|
- name: Text.Read.readMaybe
|
|||
|
within:
|
|||
|
# The BSON ObjectId depends on Read for parsing
|
|||
|
- Milkmap.Milkmap
|
|||
|
- Milkmap.FieldData.Value
|
|||
|
message: "`readMaybe` is probably not what you want for parsing values, please use the `FieldParser` module."
|
|||
|
|
|||
|
# `void` discards its argument and is polymorphic,
|
|||
|
# thus making it brittle in the face of code changes.
|
|||
|
# (see https://tech.freckle.com/2020/09/23/void-is-a-smell/)
|
|||
|
# Use an explicit `_ <- …` instead.
|
|||
|
- name: Data.Functor.void
|
|||
|
within: []
|
|||
|
message: "`void` leads to bugs. Use an explicit `_ <- …` instead"
|
|||
|
|
|||
|
- name: Data.Foldable.length
|
|||
|
within: []
|
|||
|
message: "`Data.Foldable.length` is dangerous to use, because it also works on types you wouldn’t expect, like `length (3,4) == 1` and `length (Just 2) == 1`. Use the `length` function for your specific type instead, for example `List.length` or `Map.length`."
|
|||
|
|
|||
|
- name: Prelude.length
|
|||
|
within: []
|
|||
|
message: "`Prelude.length` is dangerous to use, because it also works on types you wouldn’t expect, like `length (3,4) == 1` and `length (Just 2) == 1`. Use the `length` function for your specific type instead, for example `List.length` or `Map.length`."
|
|||
|
|
|||
|
# Using an explicit lambda with its argument “underscored”
|
|||
|
# is more clear in every case.
|
|||
|
# e.g. `const True` => `\_request -> True`
|
|||
|
# shows the reader that the ignored argument was a request.
|
|||
|
- name: Prelude.const
|
|||
|
within: []
|
|||
|
message: "Replace `const` with an explicit lambda with type annotation for code clarity and type safety, e.g.: `const True` => `\\(_ :: Request) -> True`. If you really don’t want to spell out the type (which might lead to bugs!), you can also use something like `\_request -> True`."
|
|||
|
|
|||
|
- name: Data.List.nub
|
|||
|
within: []
|
|||
|
message: "O(n²), use `Data.Containers.ListUtils.nubOrd"
|
|||
|
|
|||
|
- name: Prelude.maximum
|
|||
|
within: []
|
|||
|
message: "`maximum` crashes on empty list; use non-empty lists and `maximum1`"
|
|||
|
|
|||
|
- name: Data.List.maximum
|
|||
|
within: []
|
|||
|
message: "`maximum` crashes on empty list; use non-empty lists and `maximum1`"
|
|||
|
|
|||
|
- name: Prelude.minimum
|
|||
|
within: []
|
|||
|
message: "`minimum` crashes on empty list; use non-empty lists and `minimum1`"
|
|||
|
|
|||
|
- name: Data.List.minimum
|
|||
|
within: []
|
|||
|
message: "`minimum` crashes on empty list; use non-empty lists and `minimum1`"
|
|||
|
|
|||
|
- name: Data.Foldable.maximum
|
|||
|
within: []
|
|||
|
message: "`maximum` crashes on empty foldable stucture; use Foldable1 and `maximum1`."
|
|||
|
|
|||
|
- name: Data.Foldable.minimum
|
|||
|
within: []
|
|||
|
message: "`minimum` crashes on empty foldable stucture; use Foldable1 and `minimum1`."
|
|||
|
|
|||
|
# Using prelude functions instead of stdlib functions
|
|||
|
|
|||
|
- name: "Data.Text.Encoding.encodeUtf8"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `textToBytesUtf8`"
|
|||
|
|
|||
|
- name: "Data.Text.Lazy.Encoding.encodeUtf8"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `textToBytesUtf8Lazy`"
|
|||
|
|
|||
|
- name: "Data.Text.Encoding.decodeUtf8'"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `bytesToTextUtf8`"
|
|||
|
|
|||
|
- name: "Data.Text.Encoding.Lazy.decodeUtf8'"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `bytesToTextUtf8Lazy`"
|
|||
|
|
|||
|
- name: "Data.Text.Encoding.decodeUtf8"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Either check for errors with `bytesToTextUtf8`, decode leniently with unicode replacement characters with `bytesToTextUtf8Lenient` or use the crashing version `bytesToTextUtf8Unsafe` (discouraged)."
|
|||
|
|
|||
|
- name: "Data.Text.Encoding.Lazy.decodeUtf8"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Either check for errors with `bytesToTextUtf8Lazy`, decode leniently with unicode replacement characters with `bytesToTextUtf8LenientLazy` or use the crashing version `bytesToTextUtf8UnsafeLazy` (discouraged)."
|
|||
|
|
|||
|
- name: "Data.Text.Lazy.toStrict"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `toStrict`"
|
|||
|
|
|||
|
- name: "Data.Text.Lazy.fromStrict"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `toLazy`"
|
|||
|
|
|||
|
- name: "Data.ByteString.Lazy.toStrict"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `toStrictBytes`"
|
|||
|
|
|||
|
- name: "Data.ByteString.Lazy.fromStrict"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `toLazyBytes`"
|
|||
|
|
|||
|
- name: "Data.Text.unpack"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `textToString`"
|
|||
|
|
|||
|
- name: "Data.Text.pack"
|
|||
|
within: ["MyPrelude"]
|
|||
|
message: "Use `stringToText`"
|
|||
|
|
|||
|
- name: "Data.Maybe.listToMaybe"
|
|||
|
within: []
|
|||
|
message: |
|
|||
|
`listToMaybe`` throws away everything but the first element of a list (it is essentially `safeHead`).
|
|||
|
If that is what you want, please use a pattern match like
|
|||
|
|
|||
|
```
|
|||
|
case xs of
|
|||
|
[] -> …
|
|||
|
(x:_) -> …
|
|||
|
```
|
|||
|
|
|||
|
- name: "Data.List.head"
|
|||
|
within: []
|
|||
|
message: |
|
|||
|
`List.head` fails on an empty list. I didn’t think I have to say this, but please use a pattern match on the list, like:
|
|||
|
|
|||
|
```
|
|||
|
case xs of
|
|||
|
[] -> … error handling …
|
|||
|
(x:_) -> …
|
|||
|
```
|
|||
|
|
|||
|
Also think about why the rest of the list should be ignored.
|
|||
|
|
|||
|
- name: "Prelude.head"
|
|||
|
within: []
|
|||
|
message: |
|
|||
|
`List.head` fails on an empty list. I didn’t think I have to say this, but please use a pattern match on the list, like.
|
|||
|
|
|||
|
```
|
|||
|
case xs of
|
|||
|
[] -> … error handling …
|
|||
|
(x:_) -> …
|
|||
|
```
|
|||
|
|
|||
|
Also think about why the rest of the list should be ignored.
|
|||
|
|
|||
|
- name: "Data.Maybe.fromJust"
|
|||
|
within: []
|
|||
|
message: |
|
|||
|
`Maybe.fromJust` is obviously partial. Please use a pattern match.
|
|||
|
|
|||
|
In case you actually want to throw an error on an empty list,
|
|||
|
please add an error message, like so:
|
|||
|
|
|||
|
```
|
|||
|
myMaybe & annotate "my error message" & unwrapError
|
|||
|
```
|
|||
|
|
|||
|
If you are in `IO`, use `unwrapIOError` instead,
|
|||
|
or throw a monad-specific error.
|
|||
|
|
|||
|
- name: "Data.Either.fromLeft"
|
|||
|
within: []
|
|||
|
message: |
|
|||
|
`Either.fromLeft` is obviously partial. Please use a pattern match.
|
|||
|
|
|||
|
- name: "Data.Either.fromRight"
|
|||
|
within: []
|
|||
|
message: |
|
|||
|
`Either.fromRight` is obviously partial. Please use a pattern match.
|
|||
|
|
|||
|
# Make restricted functions into an error if found
|
|||
|
- error: { name: "Avoid restricted function, see comment in .hlint.yaml" }
|
|||
|
|
|||
|
# Some functions that have (more modern) aliases.
|
|||
|
# They are not dangerous per se,
|
|||
|
# but we want to make it easier to read our code so we should
|
|||
|
# make sure we don’t use too many things that are renames.
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "undefined"
|
|||
|
rhs: "todo"
|
|||
|
note: "`undefined` is a silent error, `todo` will display a warning as long as it exists in the code."
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "return"
|
|||
|
rhs: "pure"
|
|||
|
note: "Use `pure` from `Applicative` instead, it’s the exact same function."
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "mapM"
|
|||
|
rhs: "traverse"
|
|||
|
note: "Use `traverse` from `Traversable` instead. It’s the exact same function."
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "mapM_"
|
|||
|
rhs: "traverse_"
|
|||
|
note: "Use `traverse_` from `Traversable` instead. It’s the exact same function."
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "forM"
|
|||
|
rhs: "for"
|
|||
|
note: "Use `for` from `Traversable` instead. It’s the exact same function."
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "forM_"
|
|||
|
rhs: "for_"
|
|||
|
note: "Use `for_` from `Traversable` instead. It’s the exact same function."
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "stringToText (show x)"
|
|||
|
rhs: "showToText x"
|
|||
|
|
|||
|
- hint:
|
|||
|
lhs: "Data.Set.toList (Data.Set.fromList x)"
|
|||
|
rhs: "List.nubOrd x"
|
|||
|
note: "`nubOrd` removes duplicate elements from a list."
|
|||
|
|
|||
|
- modules:
|
|||
|
# Disallowed Modules
|
|||
|
- name: Data.Map
|
|||
|
within: []
|
|||
|
message: "Lazy maps leak space, use `import Data.Map.Strict as Map` instead"
|
|||
|
- name: Control.Monad.Writer
|
|||
|
within: []
|
|||
|
message: "Lazy writers leak space, use `Control.Monad.Trans.Writer.CPS` instead"
|
|||
|
- name: Control.Monad.Trans.Writer.Lazy
|
|||
|
within: []
|
|||
|
message: "Lazy writers leak space, use `Control.Monad.Trans.Writer.CPS` instead"
|
|||
|
- name: Control.Monad.Trans.Writer.Strict
|
|||
|
within: []
|
|||
|
message: "Even strict writers leak space, use `Control.Monad.Trans.Writer.CPS` instead"
|
|||
|
|
|||
|
# Qualified module imports
|
|||
|
- { name: Data.Map.Strict, as: Map }
|
|||
|
- { name: Data.HashMap.Strict, as: HashMap }
|
|||
|
- { name: Data.Set, as: Set }
|
|||
|
- { name: Data.ByteString.Char8, as: Char8 }
|
|||
|
- { name: Data.ByteString.Lazy.Char8, as: Char8.Lazy }
|
|||
|
- { name: Data.Text, as: Text }
|
|||
|
- { name: Data.Vector, as: Vector }
|
|||
|
- { name: Data.Vault.Lazy, as: Vault }
|
|||
|
- { name: Data.Aeson, as: Json }
|
|||
|
- { name: Data.Aeson.Types, as: Json }
|
|||
|
- { name: Data.Aeson.BetterErrors as Json }
|