ghc-clippy-plugin (empty) → 0.0.0.1
raw patch · 6 files changed
+382/−0 lines, 6 filesdep +basedep +dhalldep +ghcsetup-changed
Dependencies added: base, dhall, ghc, text, text-icu, text-regex-replace
Files
- ChangeLog.md +8/−0
- LICENSE +30/−0
- README.md +189/−0
- Setup.hs +2/−0
- ghc-clippy-plugin.cabal +45/−0
- src/Clippy.hs +108/−0
+ ChangeLog.md view
@@ -0,0 +1,8 @@+# Changelog for ghc-clippy-plugin++## 0.0.0.1++Initial release:+ - allows overriding warning and error messages emitted by ghc+ - works with stack and ghcid+ - configured using ./.clippy.dhall
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,189 @@+# GHC Clippy Plugin++[](https://travis-ci.org/ArturGajowy/ghc-clippy-plugin)+[](https://opensource.org/licenses/BSD-3-Clause)++A helpful companion to GHC.++Overrides GHC error and warning messages, to the user's liking.++Configured using (how else!) regexps. Tested with stack and ghcid.++## Showcase++<img align="right" width="430" src="https://i.imgur.com/Beay7p4.png">+<img width="430" src="https://i.imgur.com/CsYulBQ.png">++Left: without Clippy.++Right: with Clippy, using the sample config.++## But, why?++For all kinds of reasons:+ - making GHC messages more terse or more verbose+ - adding more context for beginners+ - stripping confusing / duplicated / rarely useful context for everyone+ - prototyping improvements for ghc error output+ - ever wanted GHC to speak in emoji? :smiling_imp:+ - ... or in your mother tongue?+ - ... or in mathematical notation?++## Usage++1. Add the `ghc-clippy-plugin` dependency to your project+2. Pass `-fplugin=Clippy` in GHC options++E.g. for stack:++```yaml+# file: package.yaml+dependencies:+ - ghc-clippy-plugin++ghc-options:+- -fplugin=Clippy+```++3. When building, you should see the following warning:++```+ghc-clippy-plugin: warning:+ Clippy plugin couldn't start. Cause:+ ./.clippy.dhall: openFile: does not exist (No such file or directory)+```++Make sure there was anything to compile (change a .hs file) if the warning wasn't there.++Save the [sample config](/.clippy-terse.dhall) as `.clippy.dhall` in the project's root dir+(or - more precisely - the `current directory` GHC is going to use)++4. Put an error somewhere:++```haskell+oops = print mempty+```++With the sample config, this should output:++```+./app/Main.hs:19:14-19: error:+ Type variable ‘a0’ is ambiguous in ‘mempty’.+ Can't pick an instance for ‘(Monoid a0)’.+ ---+ Maybe-fix: add type annotations to disambiguate.+ More info: compile with -fprint-potential-instances.+ |+19 | oops = print mempty+ |+```++5. Enjoy the much terser output and tweak it to your heart's content! :grin:+++## Tweaking the config++### Stack++For `--file-watch` to pick up config changes, add in `package.yaml`:+```yaml+extra-source-files:+- .clippy.dhall+```++Use `stack build --file-watch`. Make sure to have some errors handy! :)++### Ghcid++For ghcid to reload after config change, run it with `--reload=.clippy.dhall`.+I tend to put the above line in `./.ghcid`.++Ghcid may terminate if there are compile errors on startup.+If that's the case, remove your errors until ghcid starts successfully :)++With the above, for error messages, ghcid picks up `.clippy.dhall` changes immediately.++For warnings, one needs to trigger recompilation of the file triggering them.+One way around that is to enable `-Werror` for the period of config tweaking.+(I put mine in `./.ghci`.)++### Error message structure, section markers++In GHC, each error/warning message contains 3 sections in its [ErrDoc](https://hackage.haskell.org/package/ghc-8.10.1/docs/src/ErrUtils.html#ErrDoc)+('Important', 'Context', and 'Supplementary'). Each section contains a list of `MsgDoc`-s.++Before replacing the message text, Clippy wraps each section, and each of their `MsgDoc`-s, with+markers. This allows for more precise match targeting, including MsgDoc/section ends.++To see the structure of the replaced error message, comment out the `marker removing rule` in the+config.++```dhall+ rules =+ [ -- rule "(>>[ICS]>)|(<[ICS]<<)|(>[ICS]>)|(<[ICS]<)" ""+ , rule "..." "..."+ -- ...+ ]+```++Comment out all rules to see the structure of the original message. Sample result:++```+./app/Main.hs:27:11: error:+ • >>I>+ >I>+ No instance for (Num a) arising from a use of ‘+’+ Possible fix:+ add (Num a) to the context of+ the type signature for:+ bar :: forall a. a -> a -> a+ <I<+ <I<<+ • >>C>+ >C>+ In the expression: a + b+ In an equation for ‘bar’: bar a b = a + b+ <C<+ <C<<+ • >>S>+ <S<<+ |+27 | bar a b = a + b+ |+```++Replace rules can span across multiple messages in a section, but can't cross section boundaries.+For example, the following rule will remove the entire 'Context' section:++```dhall+ rule "(?s)>>C>.*?<C<<" "" -- notice the (?s) - "dot-all" regex flag+```++All-whitespace lines are removed from all the messages.++### Rule matching order++Replacement rules are applied in reverse order of the `rules` list in config.+This means the most generic and least selective rules should go at the top of the file.+In particular, the `marker removing rule` - which should be applied last - should be+the first rule in every config.+++## Acknowledgements++I'd like to thank the authors of+ - the [Scala Clippy Plugin](https://scala-clippy.org/) for the name and the idea+ - [Error Messages in Haskell, and how to Improve+ them](https://anthony.noided.media/blog/haskell/programming/2020/05/14/haskell-errors.html) for+ inspiration and some initial test cases+ - [Understanding Basic Haskell Error+Messages](http://ics.p.lodz.pl/~stolarek/_media/pl:research:stolarek_understanding_basic_haskell_error_messages.pdf)+ for test cases as well+ - the wonderful folk working on GHC, for making this possible :heart:++## Roadmap++- resolve the config from the first directory above that contains `.clippy.dhall`+- fall back to `~/.config/clippy.dhall` and then to `~/.clippy.dhall`+- cache the config instead of re-parsing for every module+- ...?
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ ghc-clippy-plugin.cabal view
@@ -0,0 +1,45 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: ecc0ca90c321825f155dc23884f20f462b01edc11978e490cca555c329e8fde5++name: ghc-clippy-plugin+version: 0.0.0.1+synopsis: Override GHC error messages to the user's liking+description: Please see the README on GitHub at <https://github.com/ArturGajowy/ghc-clippy-plugin#readme>+category: Development, Compiler Plugin+homepage: https://github.com/ArturGajowy/ghc-clippy-plugin#readme+bug-reports: https://github.com/ArturGajowy/ghc-clippy-plugin/issues+author: Artur Gajowy+maintainer: artur.gajowy@gmail.com+copyright: 2020 Artur Gajowy+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/ArturGajowy/ghc-clippy-plugin++library+ exposed-modules:+ Clippy+ other-modules:+ Paths_ghc_clippy_plugin+ hs-source-dirs:+ src+ ghc-options: -fobject-code -Wall+ build-depends:+ base >=4.7 && <5+ , dhall >=1.30.0 && <1.33+ , ghc >=8.8.2 && <8.11+ , text >=1.2.3.2 && <1.3+ , text-icu >=0.7.0 && <0.8+ , text-regex-replace >=0.1.1 && <0.2+ default-language: Haskell2010
+ src/Clippy.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Clippy+ ( plugin+ )+where++import Bag+import Control.Exception (SomeException, tryJust)+import Data.Bifunctor+import Data.Function+import Data.IORef+import Data.String (fromString)+import qualified Data.Text as T+import Data.Text.ICU (regex)+import Data.Text.ICU.Replace (replaceAll)+import Dhall+import ErrUtils+import GhcPlugins hiding (Rule, (<>))+import Prelude hiding (print)+import TcPluginM+import TcRnTypes++plugin :: Plugin+plugin =+ defaultPlugin+ { tcPlugin = const $ Just $+ TcPlugin+ { tcPluginInit = pure ()+ , tcPluginSolve = \_ _ _ _ -> pure $ TcPluginOk [] []+ , tcPluginStop = const $ loadConfig >>= either cantInitializeWarning replaceMessages+ }+ , pluginRecompile = purePlugin+ }++newtype Config = Config { rules :: [Rule] } deriving Generic+instance FromDhall Config++data Rule = Rule+ { match :: Text+ , print :: Text+ }+ deriving Generic+instance FromDhall Rule++data PEnv = PEnv+ { showMsgDoc :: MsgDoc -> Text+ , config :: Config+ }++loadConfig :: TcPluginM (Either String Config)+loadConfig = tcPluginIO+ . tryJust (Just . show @SomeException)+ $ inputFile auto "./.clippy.dhall"++cantInitializeWarning :: String -> TcPluginM ()+cantInitializeWarning cause = do+ dynFlags <- hsc_dflags <$> getTopEnv+ let warning = mkPlainWarnMsg dynFlags _span msgDoc+ _span = mkGeneralSrcSpan $ mkFastString "ghc-clippy-plugin"+ msgDoc = text "Clippy plugin couldn't start. Cause:" $$ text cause+ tcPluginIO $ printOrThrowWarnings dynFlags $ unitBag warning++replaceMessages :: Config -> TcPluginM ()+replaceMessages conf = do+ errsRef <- tcl_errs . snd <$> getEnvs+ dynFlags <- hsc_dflags <$> getTopEnv+ let _showMsgDoc = T.pack . showSDoc dynFlags+ replaceErrMsgs = fmap $ replaceErrMsgDoc $ PEnv _showMsgDoc conf+ tcPluginIO $ modifyIORef errsRef (bimap replaceErrMsgs replaceErrMsgs)++replaceErrMsgDoc :: PEnv -> ErrMsg -> ErrMsg+replaceErrMsgDoc env e = e { errMsgDoc = replaceMsgDocs env (errMsgDoc e) }++replaceMsgDocs :: PEnv -> ErrDoc -> ErrDoc+replaceMsgDocs env e = e+ { errDocImportant = replaceMsgDocsGroup env "I" (errDocImportant e)+ , errDocContext = replaceMsgDocsGroup env "C" (errDocContext e)+ , errDocSupplementary = replaceMsgDocsGroup env "S" (errDocSupplementary e)+ }++replaceMsgDocsGroup :: PEnv -> Text -> [MsgDoc] -> [MsgDoc]+replaceMsgDocsGroup env label msgDocs = text . T.unpack <$> filtered+ where+ filtered = filter (not . T.null . T.strip) (T.lines replaced)+ replaced = replaceText env wrapped+ wrapped = (env & showMsgDoc) . vcat $ wrapGroup label msgDocs++replaceText :: PEnv -> Text -> Text+replaceText env t = foldr replaceRule t (rules . config $ env)++replaceRule :: Rule -> Text -> Text+replaceRule rule = replaceAll (regex [] $ rule & match) (fromString . T.unpack $ rule & print)++wrapGroup :: Text -> [MsgDoc] -> [MsgDoc]+wrapGroup label group =+ [open $ ">" <> label] ++ (wrapDoc label <$> group) ++ [close $ label <> "<"]++wrapDoc :: Text -> MsgDoc -> MsgDoc+wrapDoc label doc = vcat [open label, doc, close label]++open :: Text -> MsgDoc+open label = text . T.unpack $ ">" <> label <> ">"++close :: Text -> MsgDoc+close label = text . T.unpack $ "<" <> label <> "<"