diff --git a/.hlint.yaml b/.hlint.yaml
--- a/.hlint.yaml
+++ b/.hlint.yaml
@@ -5,6 +5,13 @@
 # Hints that apply only to the HLint source code
 
 #####################################################################
+## GROUPS OF HINTS WE TURN ON
+
+- group: {name: future, enabled: true}
+- group: {name: extra, enabled: true}
+
+
+#####################################################################
 ## RESTRICTIONS
 
 - extensions:
@@ -17,6 +24,7 @@
   - name: [PackageImports]
   - name: [ConstraintKinds, RankNTypes, TypeFamilies]
   - name: [TemplateHaskell]
+  - name: [DerivingVia, DeriveGeneric, DataKinds]
   - {name: CPP, within: [HsColour, Config.Yaml, Test.Annotations]} # so it can be disabled to avoid GPL code
 
 - flags:
diff --git a/CHANGES.txt b/CHANGES.txt
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,16 @@
 Changelog for HLint (* = breaking change)
 
+3.4.1, released 2022-07-10
+    #1345, add --generate-config to generate a complete config
+    #1345, add --generate-summary-json
+    #1377, make anyM/allM on [] produce pure, rather than return
+    #1377, add a pure rule for every return variant
+    #1380, add counts as comments for --default
+    #1372, remove unnecessary brackets when suggesting forM_
+    #1372, suggest void (forM x y) to forM_ without the void
+    #1394, replace maximum [a, b] ==> max a b (and for min)
+    #1393, for QuickCheck, join . elements ==> oneOf
+    #1387, bypass camelCase hint for new tasty_... custom test prefix
 3.4, released 2022-04-24
     #1360, make -XHaskell2010 still obey CPP pragmas
     #1368, make TH quotation bracket rule off by default
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -201,6 +201,25 @@
 
 HLint uses the `hlint.yaml` file it ships with by default (containing things like the `concatMap` hint above), along with the first `.hlint.yaml` file it finds in the current directory or any parent thereof. To include other hints, pass `--hint=filename.yaml`.
 
+#### Are there any extra hints available?
+
+There are a few groups of hints that are shipped with HLint, but disabled by default. These are:
+
+* `future`, which suggests switching `return` for `pure`.
+* `extra`, which suggests replacements which introduce a dependency on the [`extra` library](https://hackage.haskell.org/package/extra).
+* `use-lens`, which suggests replacements which introduce a dependency on the [`lens` library](https://hackage.haskell.org/package/lens).
+* `use-th-quotes`, which suggests using `[| x |]` where possible.
+* `generalise`, which suggests more generic methods, e.g. `fmap` instead of `map`.
+* `generalise-for-conciseness`, which suggests more generic methods, but only when they are shorter, e.g. `maybe True` becomes `all`.
+* `dollar` which suggests `a $ b $ c` is replaced with `a . b $ c`.
+* `teaching` which encourages a simple beginner friendly style, learning about related functions.
+
+These can be enabled by passing `--with-group=future` or adding the following to your `.hlint.yaml` file:
+
+```yaml
+- group: {name: future, enabled: true}
+```
+
 ### Design
 
 #### Why are hints not applied recursively?
@@ -247,15 +266,21 @@
 hlint --default > .hlint.yaml
 ```
 
-This default configuration contains lots of examples, including:
+This default configuration shows lots of examples (as `# comments`) of how to:
 
-* Adding command line arguments to all runs, e.g. `--color` or `-XNoMagicHash`.
-* Ignoring certain hints, perhaps within certain modules/functions.
-* Restricting use of GHC flags/extensions/functions, e.g. banning `Arrows` and `unsafePerformIO`.
-* Adding additional project-specific hints.
+* Add command line arguments to all runs, e.g. `--color` or `-XNoMagicHash`.
+* Ignore certain hints, perhaps within certain modules/functions.
+* Restrict the use of GHC flags/extensions/functions, e.g. banning `Arrows` and `unsafePerformIO`.
+* Add additional project-specific hints.
 
-You can see the output of `--default` [here](https://github.com/ndmitchell/hlint/blob/master/data/default.yaml).
+You can see the output of `--default` for a clean lint [here](https://github.com/ndmitchell/hlint/blob/master/data/default.yaml) but for a dirty project `--default` output includes an extra warnings section that counts and ignores any hints it finds:
 
+  ```yaml
+  # Warnings currently triggered by your code
+  - ignore: {name: "Redundant $"} # 20 hints
+  - ignore: {name: "Unused LANGUAGE pragma"} # 29 hints
+  ```
+
 If you wish to use the [Dhall configuration language](https://github.com/dhall-lang/dhall-lang) to customize HLint, there [is an example](https://kowainik.github.io/posts/2018-09-09-dhall-to-hlint) and [type definition](https://github.com/kowainik/relude/blob/master/hlint/Rule.dhall).
 
 ### Finding the name of a hint
@@ -355,7 +380,9 @@
   - {name: unsafePerformIO, within: CompatLayer}
 ```
 
-This declares that the `nub` function can't be used in any modules, and thus is banned from the code. That's probably a good idea, as most people should use an alternative that isn't _O(n^2)_ (e.g. [`nubOrd`](https://hackage.haskell.org/package/extra/docs/Data-List-Extra.html#v:nubOrd)). We also whitelist where `unsafePerformIO` can occur, ensuring that there can be a centrally reviewed location to declare all such instances. Finally, we can restrict the use of modules with:
+This declares that the `nub` function can't be used in any modules, and thus is banned from the code. That's probably a good idea, as most people should use an alternative that isn't _O(n^2)_ (e.g. [`nubOrd`](https://hackage.haskell.org/package/extra/docs/Data-List-Extra.html#v:nubOrd)). We also whitelist where `unsafePerformIO` can occur, ensuring that there can be a centrally reviewed location to declare all such instances. Function names can be given qualified, e.g. `Data.List.head`, but note that functions available through multiple exports (e.g. `head` is also available from `Prelude`) should be listed through all paths they are likely to be obtained, as the HLint qualified matching is unaware of re-exports.
+
+Finally, we can restrict the use of modules with:
 
 ```yaml
 - modules:
diff --git a/data/hlint.yaml b/data/hlint.yaml
--- a/data/hlint.yaml
+++ b/data/hlint.yaml
@@ -52,6 +52,11 @@
     - import Data.Attoparsec.ByteString
 
 - package:
+    name: quickcheck
+    modules:
+    - import Test.QuickCheck
+
+- package:
     name: codeworld-api
     modules:
     - import CodeWorld
@@ -118,6 +123,8 @@
     - warn: {lhs: if a > b then a else b, rhs: max a b}
     - warn: {lhs: if a <= b then a else b, rhs: min a b}
     - warn: {lhs: if a < b then a else b, rhs: min a b}
+    - warn: {lhs: "maximum [a, b]", rhs: max a b}
+    - warn: {lhs: "minimum [a, b]", rhs: min a b}
 
 
     # READ/SHOW
@@ -289,6 +296,7 @@
 
     # FOLDS
 
+    - warn: {lhs: foldr  (>>) (pure ()), rhs: sequence_}
     - warn: {lhs: foldr  (>>) (return ()), rhs: sequence_}
     - warn: {lhs: foldr  (&&) True, rhs: and}
     - warn: {lhs: foldl  (&&) True, rhs: and, note: IncreasesLaziness}
@@ -439,20 +447,28 @@
 
     # APPLICATIVE
 
-    - hint: {lhs: return x <*> y, rhs: x <$> y}
     - hint: {lhs: pure x <*> y, rhs: x <$> y}
+    - hint: {lhs: return x <*> y, rhs: x <$> y}
     - warn: {lhs: x <* pure y, rhs: x}
+    - warn: {lhs: x <* return y, rhs: x}
     - warn: {lhs: pure x *> y, rhs: "y"}
+    - warn: {lhs: return x *> y, rhs: "y"}
 
     # MONAD
 
+    - warn: {lhs: pure a >>= f, rhs: f a, name: "Monad law, left identity"}
     - warn: {lhs: return a >>= f, rhs: f a, name: "Monad law, left identity"}
+    - warn: {lhs: f =<< pure a, rhs: f a, name: "Monad law, left identity"}
     - warn: {lhs: f =<< return a, rhs: f a, name: "Monad law, left identity"}
+    - warn: {lhs: m >>= pure, rhs: m, name: "Monad law, right identity"}
     - warn: {lhs: m >>= return, rhs: m, name: "Monad law, right identity"}
+    - warn: {lhs: pure =<< m, rhs: m, name: "Monad law, right identity"}
     - warn: {lhs: return =<< m, rhs: m, name: "Monad law, right identity"}
     - warn: {lhs: liftM, rhs: fmap}
     - warn: {lhs: liftA, rhs: fmap}
+    - hint: {lhs: m >>= pure . f, rhs: m Data.Functor.<&> f}
     - hint: {lhs: m >>= return . f, rhs: m Data.Functor.<&> f}
+    - hint: {lhs: pure . f =<< m, rhs: f <$> m}
     - hint: {lhs: return . f =<< m, rhs: f <$> m}
     - warn: {lhs: fmap f x >>= g, rhs: x >>= g . f}
     - warn: {lhs: f <$> x >>= g, rhs: x >>= g . f}
@@ -460,9 +476,13 @@
     - warn: {lhs: g =<< fmap f x, rhs: g . f =<< x}
     - warn: {lhs: g =<< f <$> x, rhs: g . f =<< x}
     - warn: {lhs: g =<< (x Data.Functor.<&> f), rhs: g . f =<< x}
+    - warn: {lhs: if x then y else pure (), rhs: Control.Monad.when x $ _noParen_ y, side: not (isAtom y)}
     - warn: {lhs: if x then y else return (), rhs: Control.Monad.when x $ _noParen_ y, side: not (isAtom y)}
+    - warn: {lhs: if x then y else pure (), rhs: Control.Monad.when x y, side: isAtom y}
     - warn: {lhs: if x then y else return (), rhs: Control.Monad.when x y, side: isAtom y}
+    - warn: {lhs: if x then pure () else y, rhs: Control.Monad.unless x $ _noParen_ y, side: isAtom y}
     - warn: {lhs: if x then return () else y, rhs: Control.Monad.unless x $ _noParen_ y, side: isAtom y}
+    - warn: {lhs: if x then pure () else y, rhs: Control.Monad.unless x y, side: isAtom y}
     - warn: {lhs: if x then return () else y, rhs: Control.Monad.unless x y, side: isAtom y}
     - warn: {lhs: sequence (map f x), rhs: mapM f x}
     - warn: {lhs: sequence_ (map f x), rhs: mapM_ f x}
@@ -482,6 +502,7 @@
     - warn: {lhs: id =<< x, rhs: Control.Monad.join x}
     - hint: {lhs: join (f <$> x), rhs: f =<< x}
     - hint: {lhs: join (fmap f x), rhs: f =<< x}
+    - hint: {lhs: a >> pure (), rhs: Control.Monad.void a, side: isAtom a || isApp a}
     - hint: {lhs: a >> return (), rhs: Control.Monad.void a, side: isAtom a || isApp a}
     - warn: {lhs: fmap (const ()), rhs: Control.Monad.void}
     - warn: {lhs: const () <$> x, rhs: Control.Monad.void x}
@@ -496,16 +517,21 @@
     - hint: {lhs: (f =<<) . g, rhs: f Control.Monad.<=< g}
     - warn: {lhs: a >> forever a, rhs: forever a}
     - hint: {lhs: liftM2 id, rhs: ap}
+    - warn: {lhs: liftM2 f (pure x), rhs: fmap (f x)}
     - warn: {lhs: liftA2 f (return x), rhs: fmap (f x)}
     - warn: {lhs: liftM2 f (pure x), rhs: fmap (f x)}
     - warn: {lhs: liftM2 f (return x), rhs: fmap (f x)}
+    - warn: {lhs: fmap f (pure x), rhs: pure (f x)}
     - warn: {lhs: fmap f (return x), rhs: return (f x)}
+    - warn: {lhs: f <$> pure x, rhs: pure (f x)}
     - warn: {lhs: f <$> return x, rhs: return (f x)}
     - warn: {lhs: mapM (uncurry f) (zip l m), rhs: zipWithM f l m}
     - warn: {lhs: mapM_ (void . f), rhs: mapM_ f}
     - warn: {lhs: forM_ x (void . f), rhs: forM_ x f}
     - warn: {lhs: a >>= \_ -> b, rhs: a >> b}
+    - warn: {lhs: m <* pure x, rhs: m}
     - warn: {lhs: m <* return x, rhs: m}
+    - warn: {lhs: pure x *> m, rhs: m}
     - warn: {lhs: return x *> m, rhs: m}
     - warn: {lhs: pure x >> m, rhs: m}
     - warn: {lhs: return x >> m, rhs: m}
@@ -543,14 +569,13 @@
     - warn: {lhs: flip traverse_, rhs: for_}
     - warn: {lhs: flip for_, rhs: traverse_}
     - warn: {lhs: foldr (*>) (pure ()), rhs: sequenceA_}
+    - warn: {lhs: foldr (*>) (return ()), rhs: sequenceA_}
     - warn: {lhs: foldr (<|>) empty, rhs: asum}
     - warn: {lhs: liftA2 (flip ($)), rhs: (<**>)}
     - warn: {lhs: liftA2 f (pure x), rhs: fmap (f x)}
-    - warn: {lhs: fmap f (pure x), rhs: pure (f x)}
-    - warn: {lhs: f <$> pure x, rhs: pure (f x)}
+    - warn: {lhs: liftA2 f (return x), rhs: fmap (f x)}
     - warn: {lhs: Just <$> a <|> pure Nothing, rhs: optional a}
-    - hint: {lhs: m >>= pure . f, rhs: m Data.Functor.<&> f}
-    - hint: {lhs: pure . f =<< m, rhs: f <$> m}
+    - warn: {lhs: Just <$> a <|> return Nothing, rhs: optional a}
     - warn: {lhs: empty <|> x, rhs: x, name: "Alternative law, left identity"}
     - warn: {lhs: x <|> empty, rhs: x, name: "Alternative law, right identity"}
     - warn: {lhs: traverse id, rhs: sequenceA}
@@ -746,8 +771,11 @@
 
     # FOLDABLE
 
+    - warn: {lhs: case m of Nothing -> pure (); Just x -> f x, rhs: Data.Foldable.forM_ m f}
     - warn: {lhs: case m of Nothing -> return (); Just x -> f x, rhs: Data.Foldable.forM_ m f}
+    - warn: {lhs: case m of Just x -> f x; Nothing -> pure (), rhs: Data.Foldable.forM_ m f}
     - warn: {lhs: case m of Just x -> f x; Nothing -> return (), rhs: Data.Foldable.forM_ m f}
+    - warn: {lhs: case m of Just x -> f x; _ -> pure (), rhs: Data.Foldable.forM_ m f}
     - warn: {lhs: case m of Just x -> f x; _ -> return (), rhs: Data.Foldable.forM_ m f}
     - warn: {lhs: when (isJust m) (f (fromJust m)), rhs: Data.Foldable.forM_ m f}
 
@@ -915,6 +943,15 @@
     - warn: {lhs: Data.Attoparsec.ByteString.option Nothing (Just <$> p), rhs: optional p}
 
 - group:
+    name: quickcheck
+    enabled: true
+    imports:
+    - package base
+    - package quickcheck
+    rules:
+    - warn: {lhs: Control.Monad.join (Test.QuickCheck.elements l), rhs: Test.QuickCheck.oneof l}
+
+- group:
     name: generalise
     enabled: false
     imports:
@@ -979,20 +1016,24 @@
     - warn: {lhs: "maybeM a pure x", rhs: "fromMaybeM a b"}
     - warn: {lhs: "maybeM a return x", rhs: "fromMaybeM a b"}
     - warn: {lhs: "either a b =<< c", rhs: "eitherM a b c"}
-    - warn: {lhs: "fold1M a b >> return ()", rhs: "fold1M_ a b"}
     - warn: {lhs: "fold1M a b >> pure ()", rhs: "fold1M_ a b"}
+    - warn: {lhs: "fold1M a b >> return ()", rhs: "fold1M_ a b"}
     - warn: {lhs: "flip concatMapM", rhs: "concatForM"}
     - warn: {lhs: "liftM mconcat (mapM a b)", rhs: "mconcatMapM a b"}
+    - warn: {lhs: "ifM a b (pure ())", rhs: "whenM a b"}
     - warn: {lhs: "ifM a b (return ())", rhs: "whenM a b"}
+    - warn: {lhs: "ifM a (pure ()) b", rhs: "unlessM a b"}
     - warn: {lhs: "ifM a (return ()) b", rhs: "unlessM a b"}
+    - warn: {lhs: "ifM a (pure True) b", rhs: "(||^) a b"}
     - warn: {lhs: "ifM a (return True) b", rhs: "(||^) a b"}
+    - warn: {lhs: "ifM a b (pure False)", rhs: "(&&^) a b"}
     - warn: {lhs: "ifM a b (return False)", rhs: "(&&^) a b"}
     - warn: {lhs: "anyM id", rhs: "orM"}
     - warn: {lhs: "allM id", rhs: "andM"}
     - warn: {lhs: "allM f [a]", rhs: f a, name: Evaluate}
-    - warn: {lhs: "allM f []", rhs: return True, name: Evaluate}
+    - warn: {lhs: "allM f []", rhs: pure True, name: Evaluate}
     - warn: {lhs: "anyM f [a]", rhs: f a, name: Evaluate}
-    - warn: {lhs: "anyM f []", rhs: return False, name: Evaluate}
+    - warn: {lhs: "anyM f []", rhs: pure False, name: Evaluate}
     - warn: {lhs: "either id id", rhs: "fromEither"}
     - warn: {lhs: "either (const Nothing) Just", rhs: "eitherToMaybe"}
     - warn: {lhs: "either (Left . a) Right", rhs: "mapLeft a"}
diff --git a/hlint.cabal b/hlint.cabal
--- a/hlint.cabal
+++ b/hlint.cabal
@@ -1,7 +1,7 @@
 cabal-version:      1.18
 build-type:         Simple
 name:               hlint
-version:            3.4
+version:            3.4.1
 license:            BSD3
 license-file:       LICENSE
 category:           Development
@@ -77,7 +77,8 @@
         ansi-terminal >= 0.8.1,
         extra >= 1.7.3,
         refact >= 0.3,
-        aeson >= 1.1.2.0,
+        aeson >= 1.3,
+        deriving-aeson >= 0.2,
         filepattern >= 0.1.1
 
     if !flag(ghc-lib) && impl(ghc >= 9.2.2) && impl(ghc < 9.3.0)
diff --git a/src/CmdLine.hs b/src/CmdLine.hs
--- a/src/CmdLine.hs
+++ b/src/CmdLine.hs
@@ -39,6 +39,7 @@
 import Paths_hlint
 import Data.Version
 import Prelude
+import Config.Type (Severity (Warning))
 
 
 getCmd :: [String] -> IO Cmd
@@ -125,7 +126,9 @@
         ,cmdRefactorOptions :: String   -- ^ Options to pass to the `refactor` executable.
         ,cmdWithRefactor :: FilePath    -- ^ Path to refactor tool
         ,cmdIgnoreGlob :: [FilePattern]
-        ,cmdGenerateSummary :: [FilePath]  -- ^ Generate a summary of available hints
+        ,cmdGenerateMdSummary :: [FilePath]  -- ^ Generate a summary of available hints, in Markdown format
+        ,cmdGenerateJsonSummary :: [FilePath]  -- ^ Generate a summary of built-in hints, in JSON format
+        ,cmdGenerateExhaustiveConf :: [Severity]  -- ^ Generate a hlint config file with all built-in hints set to the specified level
         ,cmdTest :: Bool
         }
     deriving (Data,Typeable,Show)
@@ -164,7 +167,9 @@
         ,cmdRefactorOptions = nam_ "refactor-options" &= typ "OPTIONS" &= help "Options to pass to the `refactor` executable"
         ,cmdWithRefactor = nam_ "with-refactor" &= help "Give the path to refactor"
         ,cmdIgnoreGlob = nam_ "ignore-glob" &= help "Ignore paths matching glob pattern (e.g. foo/bar/*.hs)"
-        ,cmdGenerateSummary = nam_ "generate-summary" &= opt "hints.md" &= help "Generate a summary of built-in hints"
+        ,cmdGenerateMdSummary = nam_ "generate-summary" &= opt "hints.md" &= help "Generate a summary of available hints, in Mardown format"
+        ,cmdGenerateJsonSummary = nam_ "generate-summary-json" &= opt "hints.json" &= help "Generate a summary of available hints, in JSON format"
+        ,cmdGenerateExhaustiveConf = nam_ "generate-config" &= opt Warning &= typ "LEVEL" &= help "Generate a .hlint.yaml config file with all hints set to the specified severity level (default level: warn, alternatives: ignore, suggest, error)"
         ,cmdTest = nam_ "test" &= help "Run the test suite"
         } &= auto &= explicit &= name "lint"
         &= details ["HLint gives hints on how to improve Haskell code."
@@ -188,7 +193,10 @@
         fail $ unlines $ "Failed to find requested hint files:" : map ("  "++) bad
 
     -- if the user has given any explicit hints, ignore the local ones
-    implicit <- if explicit /= [] || cmdGenerateSummary cmd /= [] then pure Nothing else do
+    implicit <- if explicit /= []
+                  || cmdGenerateMdSummary cmd /= []
+                  || cmdGenerateJsonSummary cmd /= []
+                  || cmdGenerateExhaustiveConf cmd /= [] then pure Nothing else do
         -- we follow the stylish-haskell config file search policy
         -- 1) current directory or its ancestors; 2) home directory
         curdir <- getCurrentDirectory
diff --git a/src/Config/Type.hs b/src/Config/Type.hs
--- a/src/Config/Type.hs
+++ b/src/Config/Type.hs
@@ -1,3 +1,9 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module Config.Type(
     Severity(..), Classify(..), HintRule(..), Note(..), Setting(..),
@@ -16,6 +22,9 @@
 import Fixity
 import GHC.Util
 import Language.Haskell.GhclibParserEx.GHC.Hs.ExtendInstances
+import Deriving.Aeson
+import System.Console.CmdArgs.Implicit
+import Data.Aeson hiding (Error)
 
 getSeverity :: String -> Maybe Severity
 getSeverity "ignore" = Just Ignore
@@ -44,7 +53,8 @@
     | Suggestion -- ^ Suggestions are things that some people may consider improvements, but some may not.
     | Warning -- ^ Warnings are suggestions that are nearly always a good idea to apply.
     | Error -- ^ Available as a setting for the user. Only parse errors have this setting by default.
-      deriving (Eq,Ord,Show,Read,Bounded,Enum)
+      deriving (Eq,Ord,Show,Read,Bounded,Enum,Generic,Data)
+      deriving (ToJSON) via CustomJSON '[FieldLabelModifier CamelToSnake] Severity
 
 
 -- Any 1-letter variable names are assumed to be unification variables
@@ -105,6 +115,13 @@
     ,hintRuleSide :: Maybe (HsExtendInstances (GHC.Hs.LHsExpr GHC.Hs.GhcPs))  -- ^ Side condition (GHC parse tree).
     }
     deriving Show
+
+instance ToJSON HintRule where
+    toJSON HintRule{..} = object
+        [ "name" .= hintRuleName
+        , "lhs" .= show hintRuleLHS
+        , "rhs" .= show hintRuleRHS
+        ]
 
 data RestrictType = RestrictModule | RestrictExtension | RestrictFlag | RestrictFunction deriving (Show,Eq,Ord)
 
diff --git a/src/EmbedData.hs b/src/EmbedData.hs
--- a/src/EmbedData.hs
+++ b/src/EmbedData.hs
@@ -10,11 +10,16 @@
 import Data.ByteString.UTF8
 import Data.FileEmbed
 
+-- Use NOINLINE below to avoid dirtying too much when these files change
+
+{-# NOINLINE hlintYaml #-}
 hlintYaml :: (FilePath, Maybe String)
 hlintYaml = ("data/hlint.yaml", Just $ toString $(embedFile "data/hlint.yaml"))
 
+{-# NOINLINE defaultYaml #-}
 defaultYaml :: String
 defaultYaml = toString $(embedFile "data/default.yaml")
 
+{-# NOINLINE reportTemplate #-}
 reportTemplate :: String
 reportTemplate = toString $(embedFile "data/report_template.html")
diff --git a/src/HLint.hs b/src/HLint.hs
--- a/src/HLint.hs
+++ b/src/HLint.hs
@@ -81,18 +81,36 @@
     | cmdDefault = do
         ideas <- if null cmdFiles then pure [] else withVerbosity Quiet $
             runHlintMain args cmd{cmdJson=False,cmdSerialise=False,cmdRefactor=False} Nothing
-        let bad = nubOrd $ map ideaHint ideas
+        let bad = group $ sort $ map ideaHint ideas
         if null bad then putStr defaultYaml else do
             let group1:groups = splitOn ["",""] $ lines defaultYaml
             let group2 = "# Warnings currently triggered by your code" :
-                         ["- ignore: {name: " ++ show x ++ "}" | x <- bad]
+                         ["- ignore: {name: " ++ show x ++ "} # "
+                         ++ if null tl then "1 hint" else show (length xs) ++ " hints"
+                         | xs@(x : tl) <- bad
+                         ]
+
             putStr $ unlines $ intercalate ["",""] $ group1:group2:groups
         pure []
-    | cmdGenerateSummary /= [] = do
-        forM_ cmdGenerateSummary $ \file -> timedIO "Summary" file $ do
-            whenNormal $ putStrLn $ "Writing summary to " ++ file ++ " ..."
-            summary <- generateSummary . snd =<< readAllSettings args cmd
+    | cmdGenerateMdSummary /= [] = do
+        forM_ cmdGenerateMdSummary $ \file -> timedIO "Summary" file $ do
+            whenNormal $ putStrLn $ "Writing Markdown summary to " ++ file ++ " ..."
+            summary <- generateMdSummary . snd =<< readAllSettings args cmd
             writeFileBinary file summary
+        pure []
+    | cmdGenerateJsonSummary /= [] = do
+        forM_ cmdGenerateJsonSummary $ \file -> timedIO "Summary" file $ do
+            whenNormal $ putStrLn $ "Writing JSON summary to " ++ file ++ " ..."
+            summary <- generateJsonSummary . snd =<< readAllSettings args cmd
+            writeFileBinary file summary
+        pure []
+    | cmdGenerateExhaustiveConf /= [] = do
+        forM_ cmdGenerateExhaustiveConf $ \severity ->
+            let file = show severity ++ "-all.yaml"
+             in timedIO "Exhaustive config file" file $ do
+                whenNormal $ putStrLn $ "Writing " ++ show severity ++ "-all list to " ++ file ++ " ..."
+                exhaustiveConfig <- generateExhaustiveConfig severity . snd =<< readAllSettings args cmd
+                writeFileBinary file exhaustiveConfig
         pure []
     | null cmdFiles && not (null cmdFindHints) = do
         hints <- concatMapM (resolveFile cmd Nothing) cmdFindHints
diff --git a/src/Hint/Monad.hs b/src/Hint/Monad.hs
--- a/src/Hint/Monad.hs
+++ b/src/Hint/Monad.hs
@@ -1,4 +1,10 @@
-{-# LANGUAGE LambdaCase, ViewPatterns, PatternGuards, FlexibleContexts #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE PatternGuards #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE ViewPatterns #-}
+
 {-
     Find and match:
 
@@ -45,7 +51,9 @@
 main = do f a $ sleep 10 -- @Ignore
 main = do foo x; return 3; bar z --
 main = void $ forM_ f xs -- forM_ f xs
-main = void $ forM f xs -- void $ forM_ f xs
+main = void (forM_ f xs) -- forM_ f xs
+main = void $ forM f xs -- forM_ f xs
+main = void (forM f xs) -- forM_ f xs
 main = do _ <- forM_ f xs; bar -- forM_ f xs
 main = do bar; forM_ f xs; return () -- do bar; forM_ f xs
 main = do a; when b c; return () -- do a; when b c
@@ -125,8 +133,27 @@
     _ -> []
   where
     f = monadNoResult (fromMaybe "" decl) id
-    seenVoid wrap x = monadNoResult (fromMaybe "" decl) wrap x
-      ++ [warn "Redundant void" (reLoc (wrap x)) (reLoc x) [Replace Expr (toSSA (wrap x)) [("a", toSSA x)] "a"] | returnsUnit x]
+    seenVoid wrap (L l (HsPar x y)) = seenVoid (wrap . L l . HsPar x) y
+    seenVoid wrap x =
+      -- Suggest `traverse_ f x` given `void $ traverse_ f x`
+      [warn "Redundant void" (reLoc (wrap x)) (reLoc x) [Replace Expr (toSSA (wrap x)) [("a", toSSA x)] "a"] | returnsUnit x]
+        -- Suggest `traverse_ f x` given `void $ traverse f x`
+        ++ ( case modifyAppHead
+               ( \fun@(L l name) ->
+                   ( if occNameStr name `elem` badFuncs
+                       then L l (mkRdrUnqual (mkVarOcc (occNameStr name ++ "_")))
+                       else fun,
+                     fun
+                   )
+               )
+               x of
+               (x', Just fun@(L l name)) | occNameStr name `elem` badFuncs ->
+                  let fun_ = occNameStr name ++ "_"
+                   in [warn ("Use " ++ fun_) (reLoc (wrap x)) (reLoc x')
+                        [Replace Expr (toSSA (wrap x)) [("a", toSSA x)] "a",
+                         Replace Expr (toSSA fun) [] fun_]]
+               _ -> []
+           )
     doSpan doOrMDo = \case
       UnhelpfulSpan s -> UnhelpfulSpan s
       RealSrcSpan s _ ->
@@ -154,13 +181,23 @@
     = srcSpanStartCol a == srcSpanStartCol b
 doAsAvoidingIndentation parent self = False
 
+-- Apply a function to the application head, including `head arg` and `head $ arg`, which modifies
+-- the head and returns a value. Sees through parentheses.
+modifyAppHead :: forall a. (LIdP GhcPs -> (LIdP GhcPs, a)) -> LHsExpr GhcPs -> (LHsExpr GhcPs, Maybe a)
+modifyAppHead f = go id
+  where
+    go :: (LHsExpr GhcPs -> LHsExpr GhcPs) -> LHsExpr GhcPs -> (LHsExpr GhcPs, Maybe a)
+    go wrap (L l (HsPar _ x)) = go (wrap . L l . HsPar EpAnnNotUsed) x
+    go wrap (L l (HsApp _ x y)) = go (\x -> wrap $ L l (HsApp EpAnnNotUsed x y)) x
+    go wrap (L l (OpApp _ x op y)) | isDol op = go (\x -> wrap $ L l (OpApp EpAnnNotUsed x op y)) x
+    go wrap (L l (HsVar _ x)) = (wrap (L l (HsVar NoExtField x')), Just a)
+      where (x', a) = f x
+    go _ expr = (expr, Nothing)
 
 returnsUnit :: LHsExpr GhcPs -> Bool
-returnsUnit (L _ (HsPar _ x)) = returnsUnit x
-returnsUnit (L _ (HsApp _ x _)) = returnsUnit x
-returnsUnit (L _ (OpApp _ x op _)) | isDol op = returnsUnit x
-returnsUnit (L _ (HsVar _ (L _ x))) = occNameStr x `elem` map (++ "_") badFuncs ++ unitFuncs
-returnsUnit _ = False
+returnsUnit = fromMaybe False
+            . snd
+            . modifyAppHead (\x -> (x, occNameStr (unLoc x) `elem` map (++ "_") badFuncs ++ unitFuncs))
 
 -- See through HsPar, and down HsIf/HsCase, return the name to use in
 -- the hint, and the revised expression.
diff --git a/src/Hint/Naming.hs b/src/Hint/Naming.hs
--- a/src/Hint/Naming.hs
+++ b/src/Hint/Naming.hs
@@ -122,7 +122,7 @@
 suggestName :: String -> Maybe String
 suggestName original
     | isSym original || good || not (any isLower original) || any isDigit original ||
-        any (`isPrefixOf` original) ["prop_","case_","unit_","test_","spec_","scprop_","hprop_"] = Nothing
+        any (`isPrefixOf` original) ["prop_","case_","unit_","test_","spec_","scprop_","hprop_","tasty_"] = Nothing
     | otherwise = Just $ f original
     where
         good = all isAlphaNum $ drp '_' $ drp '#' $ reverse $ filter (/= '\'') $ drp '_' original
diff --git a/src/Summary.hs b/src/Summary.hs
--- a/src/Summary.hs
+++ b/src/Summary.hs
@@ -1,6 +1,9 @@
-{-# LANGUAGE RecordWildCards, TupleSections #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE DataKinds #-}
 
-module Summary (generateSummary) where
+module Summary (generateMdSummary, generateJsonSummary, generateExhaustiveConfig) where
 
 import qualified Data.Map as Map
 import Control.Monad.Extra
@@ -14,73 +17,127 @@
 import Hint.All
 import Config.Type
 import Test.Annotations
-
-
-data BuiltinKey = BuiltinKey
-  { builtinName :: !String
-  , builtinSeverity :: !Severity
-  , builtinRefactoring :: !Bool
-  } deriving (Eq, Ord)
+import Deriving.Aeson
+import Data.Aeson (encode)
+import Data.ByteString.Char8 (unpack)
+import Data.ByteString.Lazy (toStrict)
 
-data BuiltinValue = BuiltinValue
-    { builtinInp :: !String
-    , builtinFrom :: !String
-    , builtinTo :: !(Maybe String)
-    }
+data Summary = Summary
+  { sBuiltinRules :: ![BuiltinHint]
+  , sLhsRhsRules :: ![HintRule]
+  } deriving (Show, Generic)
+  deriving (ToJSON) via CustomJSON '[FieldLabelModifier (StripPrefix "s", CamelToSnake)] Summary
 
+data BuiltinHint = BuiltinHint
+  { hName :: !String
+  , hSeverity :: !Severity
+  , hRefactoring :: !Bool
+  , hCategory :: !String
+  , hExamples :: ![BuiltinExample]
+  } deriving (Show, Eq, Ord, Generic)
+  deriving (ToJSON) via CustomJSON '[FieldLabelModifier (StripPrefix "h", CamelToSnake)] BuiltinHint
 
-dedupeBuiltin :: [(BuiltinKey, BuiltinValue)] -> [(BuiltinKey, BuiltinValue)]
-dedupeBuiltin = Map.toAscList . Map.fromListWith (\_ old -> old)
+data BuiltinKey = BuiltinKey
+  { kName :: !String
+  , kSeverity :: !Severity
+  , kRefactoring :: !Bool
+  , kCategory :: !String
+  } deriving (Show, Eq, Ord)
 
+data BuiltinExample = BuiltinExample
+    { eContext :: !String
+    , eFrom :: !String
+    , eTo :: !(Maybe String)
+    } deriving (Show, Eq, Ord, Generic)
+    deriving (ToJSON) via CustomJSON '[FieldLabelModifier (StripPrefix "e", CamelToSnake)] BuiltinExample
 
--- | Generate a summary of hints, including built-in hints and YAML-configured hints
--- from @data/hlint.yaml@.
-generateSummary :: [Setting] -> IO String
-generateSummary settings = do
-    -- Do not insert if the key already exists in the map. This has the effect
-    -- of picking the first test case of a hint as the example in the summary.
-    builtinHints <- mkBuiltinSummary
-    let lhsRhsHints = [hint | SettingMatchExp hint <- settings]
-    pure $ genBuiltinSummaryMd builtinHints lhsRhsHints
+dedupBuiltin :: [(BuiltinKey, BuiltinExample)] -> [BuiltinHint]
+dedupBuiltin = fmap makeHint . Map.toAscList . Map.fromListWith (<>) . fmap exampleToList where
+  exampleToList (k, e) = (k, [e])
+  makeHint (BuiltinKey{..}, examples) = BuiltinHint
+    kName
+    kSeverity
+    kRefactoring
+    kCategory
+    examples
 
 -- | The summary of built-in hints is generated by running the test cases in
--- @src/Hint/*.hs@. One entry per (hint name, severity, does it support refactoring).
-mkBuiltinSummary :: IO [(String, [(BuiltinKey, BuiltinValue)])]
-mkBuiltinSummary = forM builtinHints $ \(name, hint) -> (name,) <$> do
-    let file = "src/Hint" </> name <.> "hs"
+-- @src/Hint/*.hs@.
+mkBuiltinSummary :: IO [BuiltinHint]
+mkBuiltinSummary = concatForM builtinHints $ \(category, hint) -> do
+    let file = "src/Hint" </> category <.> "hs"
     b <- doesFileExist file
     if not b then do
         putStrLn $ "Couldn't find source hint file " ++ file ++ ", some hints will be missing"
         pure []
      else do
         tests <- parseTestFile file
-        fmap dedupeBuiltin <$> concatForM tests $ \(TestCase _ _ inp _ _) -> do
+        fmap dedupBuiltin <$> concatForM tests $ \(TestCase _ _ inp _ _) -> do
             m <- parseModuleEx defaultParseFlags file (Just inp)
             pure $ case m of
-                Right m -> map (ideaToValue inp) $ applyHints [] hint [m]
+                Right m -> map (ideaToValue category inp) $ applyHints [] hint [m]
                 Left _ -> []
     where
-        ideaToValue :: String -> Idea -> (BuiltinKey, BuiltinValue)
-        ideaToValue inp Idea{..} = (k, v)
+        ideaToValue :: String -> String -> Idea -> (BuiltinKey, BuiltinExample)
+        ideaToValue category inp Idea{..} = (k, v)
             where
                 -- make sure Windows/Linux don't differ on path separators
                 to = fmap (\x -> if "Combine with " `isPrefixOf` x then replace "\\" "/" x else x) ideaTo
-                k = BuiltinKey ideaHint ideaSeverity (notNull ideaRefactoring)
-                v = BuiltinValue inp ideaFrom to
+                k = BuiltinKey ideaHint ideaSeverity (notNull ideaRefactoring) category
+                v = BuiltinExample inp ideaFrom to
 
+getSummary :: [Setting] -> IO Summary
+getSummary settings = do
+  builtinHints <- mkBuiltinSummary
+  let lhsRhsHints = [hint | SettingMatchExp hint <- settings]
+  pure $ Summary builtinHints lhsRhsHints
 
-genBuiltinSummaryMd :: [(String, [(BuiltinKey, BuiltinValue)])] -> [HintRule] -> String
-genBuiltinSummaryMd builtins lhsRhs = unlines $
+jsonToString :: ToJSON a => a -> String
+jsonToString = unpack . toStrict . encode
+
+-- | Generate a summary of hints, including built-in hints and YAML-configured hints
+generateMdSummary :: [Setting] -> IO String
+generateMdSummary = fmap genSummaryMd . getSummary
+
+generateJsonSummary :: [Setting] -> IO String
+generateJsonSummary = fmap jsonToString . getSummary
+
+generateExhaustiveConfig :: Severity -> [Setting] -> IO String
+generateExhaustiveConfig severity = fmap (genExhaustiveConfig severity) . getSummary
+
+genExhaustiveConfig :: Severity -> Summary -> String
+genExhaustiveConfig severity Summary{..} = unlines $
+  [ "# HLint configuration file"
+  , "# https://github.com/ndmitchell/hlint"
+  , "##########################"
+  , ""
+  , "# This file contains a template configuration file, which is typically"
+  , "# placed as .hlint.yaml in the root of your project"
+  , ""
+  , "# All built-in hints"
+  ]
+    ++ (mkLine <$> sortDedup (hName <$> sBuiltinRules))
+    ++ ["", "# All LHS/RHS hints"]
+    ++ (mkLine <$> sortDedup (hintRuleName <$> sLhsRhsRules))
+  where
+    sortDedup = fmap head . group . sort
+    mkLine name = "- " <> show severity <> ": {name: " <> jsonToString name <> "}"
+
+genSummaryMd :: Summary -> String
+genSummaryMd Summary{..} = unlines $
   [ "# Summary of Hints"
   , ""
   , "This page is auto-generated from `hlint --generate-summary`."
   ] ++
-  concat ["" : ("## Builtin " ++ group ) : "" : builtinTable hints |  (group, hints) <- builtins] ++
+  concat ["" : ("## Builtin " ++ group ) : "" : builtinTable hints | (group, hints) <- groupHintsByCategory sBuiltinRules] ++
   [ ""
   , "## Configured hints"
   , ""
   ]
-  ++ lhsRhsTable lhsRhs
+  ++ lhsRhsTable sLhsRhsRules
+  where
+    groupHintsByCategory = Map.toAscList . Map.fromListWith (<>) . fmap keyCategory
+    keyCategory hint = (hCategory hint, [hint])
 
 row :: [String] -> [String]
 row xs = ["<tr>"] ++ xs ++ ["</tr>"]
@@ -91,34 +148,35 @@
   | '\n' `elem` s = ["<pre>", s, "</pre>"]
   | otherwise = ["<code>", s, "</code>", "<br>"]
 
-builtinTable :: [(BuiltinKey, BuiltinValue)] -> [String]
+builtinTable :: [BuiltinHint] -> [String]
 builtinTable builtins =
   ["<table>"]
   ++ row ["<th>Hint Name</th>", "<th>Hint</th>", "<th>Severity</th>"]
-  ++ concatMap (uncurry showBuiltin) builtins
+  ++ concatMap showBuiltin builtins
   ++ ["</table>"]
 
-showBuiltin :: BuiltinKey -> BuiltinValue -> [String]
-showBuiltin BuiltinKey{..} BuiltinValue{..} = row1
+showBuiltin :: BuiltinHint -> [String]
+showBuiltin BuiltinHint{..} = row1
   where
     row1 = row $
-      [ "<td>" ++ builtinName ++ "</td>"
-      , "<td>"
-      , "Example:"
-      ]
-      ++ haskell builtinInp
-      ++ ["Found:"]
-      ++ haskell builtinFrom
-      ++ ["Suggestion:"]
-      ++ haskell to
-      ++ ["Does not support refactoring." | not builtinRefactoring]
+      [ "<td>" ++ hName ++ "</td>", "<td>"]
+      ++ showExample (head hExamples)
+      ++ ["Does not support refactoring." | not hRefactoring]
       ++ ["</td>"] ++
-      [ "<td>" ++ show builtinSeverity ++ "</td>"
+      [ "<td>" ++ show hSeverity ++ "</td>"
       ]
-    to = case builtinTo of
-      Nothing -> ""
-      Just "" -> "Perhaps you should remove it."
-      Just s -> s
+    showExample BuiltinExample{..} =
+      ["Example: "]
+        ++ haskell eContext
+        ++ ["Found:"]
+        ++ haskell eFrom
+        ++ ["Suggestion:"]
+        ++ haskell eTo'
+      where
+      eTo' = case eTo of
+        Nothing -> ""
+        Just "" -> "Perhaps you should remove it."
+        Just s -> s
 
 lhsRhsTable :: [HintRule] -> [String]
 lhsRhsTable hints =
diff --git a/tests/flag-no-summary.test b/tests/flag-no-summary.test
--- a/tests/flag-no-summary.test
+++ b/tests/flag-no-summary.test
@@ -13,6 +13,6 @@
 ---------------------------------------------------------------------
 RUN tests/flag-no-summary2.hs --no-summary
 FILE tests/flag-no-summary2.hs
-main = return ()
+main = pure ()
 OUTPUT
 EXIT 0
diff --git a/tests/json.test b/tests/json.test
--- a/tests/json.test
+++ b/tests/json.test
@@ -18,10 +18,10 @@
 FILE tests/json-two.hs
 foo = (+1)
 bar x = foo x
-baz = getLine >>= return . map toUpper
+baz = getLine >>= pure . upper
 OUTPUT
 [{"module":["Main"],"decl":["bar"],"severity":"Warning","hint":"Eta reduce","file":"tests/json-two.hs","startLine":2,"startColumn":1,"endLine":2,"endColumn":14,"from":"bar x = foo x","to":"bar = foo","note":[],"refactorings":"[Replace {rtype = Decl, pos = SrcSpan {startLine = 2, startCol = 1, endLine = 2, endCol = 14}, subts = [(\"body\",SrcSpan {startLine = 2, startCol = 9, endLine = 2, endCol = 12})], orig = \"bar = body\"}]"}
-,{"module":["Main"],"decl":["baz"],"severity":"Suggestion","hint":"Use <&>","file":"tests/json-two.hs","startLine":3,"startColumn":7,"endLine":3,"endColumn":39,"from":"getLine >>= return . map toUpper","to":"getLine Data.Functor.<&> map toUpper","note":[],"refactorings":"[Replace {rtype = Expr, pos = SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 39}, subts = [(\"f\",SrcSpan {startLine = 3, startCol = 28, endLine = 3, endCol = 39}),(\"m\",SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 14})], orig = \"m Data.Functor.<&> f\"}]"}]
+,{"module":["Main"],"decl":["baz"],"severity":"Suggestion","hint":"Use <&>","file":"tests/json-two.hs","startLine":3,"startColumn":7,"endLine":3,"endColumn":31,"from":"getLine >>= pure . upper","to":"getLine Data.Functor.<&> upper","note":[],"refactorings":"[Replace {rtype = Expr, pos = SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 31}, subts = [(\"f\",SrcSpan {startLine = 3, startCol = 26, endLine = 3, endCol = 31}),(\"m\",SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 14})], orig = \"m Data.Functor.<&> f\"}]"}]
 
 ---------------------------------------------------------------------
 RUN tests/json-parse-error.hs --json
diff --git a/tests/serialise.test b/tests/serialise.test
--- a/tests/serialise.test
+++ b/tests/serialise.test
@@ -18,9 +18,9 @@
 FILE tests/serialise-two.hs
 foo = (+1)
 bar x = foo x
-baz = getLine >>= return . map toUpper
+baz = getLine >>= pure . upper
 OUTPUT
-[("tests/serialise-two.hs:2:1-13: Warning: Eta reduce\nFound:\n  bar x = foo x\nPerhaps:\n  bar = foo\n",[Replace {rtype = Decl, pos = SrcSpan {startLine = 2, startCol = 1, endLine = 2, endCol = 14}, subts = [("body",SrcSpan {startLine = 2, startCol = 9, endLine = 2, endCol = 12})], orig = "bar = body"}]),("tests/serialise-two.hs:3:7-38: Suggestion: Use <&>\nFound:\n  getLine >>= return . map toUpper\nPerhaps:\n  getLine Data.Functor.<&> map toUpper\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 39}, subts = [("f",SrcSpan {startLine = 3, startCol = 28, endLine = 3, endCol = 39}),("m",SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 14})], orig = "m Data.Functor.<&> f"}])]
+[("tests/serialise-two.hs:2:1-13: Warning: Eta reduce\nFound:\n  bar x = foo x\nPerhaps:\n  bar = foo\n",[Replace {rtype = Decl, pos = SrcSpan {startLine = 2, startCol = 1, endLine = 2, endCol = 14}, subts = [("body",SrcSpan {startLine = 2, startCol = 9, endLine = 2, endCol = 12})], orig = "bar = body"}]),("tests/serialise-two.hs:3:7-30: Suggestion: Use <&>\nFound:\n  getLine >>= pure . upper\nPerhaps:\n  getLine Data.Functor.<&> upper\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 31}, subts = [("f",SrcSpan {startLine = 3, startCol = 26, endLine = 3, endCol = 31}),("m",SrcSpan {startLine = 3, startCol = 7, endLine = 3, endCol = 14})], orig = "m Data.Functor.<&> f"}])]
 
 
 ---------------------------------------------------------------------
