packages feed

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 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++[![Build status](https://img.shields.io/travis/ArturGajowy/ghc-clippy-plugin.svg?logo=travis)](https://travis-ci.org/ArturGajowy/ghc-clippy-plugin)+[![License](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](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 <> "<"