# 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_ } - ignore: { name: Functor law } # 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 } - ignore: { name: Fuse traverse/<$> } # 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: ["MyPrelude"] 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: ["MyPrelude"] 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 }