splint (empty) → 1.0.0.0
raw patch · 4 files changed
+211/−0 lines, 4 filesdep +basedep +ghcdep +hlint
Dependencies added: base, ghc, hlint
Files
- LICENSE.markdown +13/−0
- README.markdown +102/−0
- splint.cabal +27/−0
- src/lib/Splint.hs +69/−0
+ LICENSE.markdown view
@@ -0,0 +1,13 @@+Copyright 2020 Taylor Fausak++Permission to use, copy, modify, and/or distribute this software for any+purpose with or without fee is hereby granted, provided that the above+copyright notice and this permission notice appear in all copies.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR+PERFORMANCE OF THIS SOFTWARE.
+ README.markdown view
@@ -0,0 +1,102 @@+# Splint++[](https://travis-ci.org/tfausak/splint)+[](https://hackage.haskell.org/package/splint)+[](https://www.stackage.org/package/splint)++Splint is a proof of concept, showing how to use [HLint 3][] as a [GHC source+plugin][]. It is similar to [hlint-source-plugin][] by Ollie Charles, except+that it doesn't have to re-parse the module in order to lint it.++To use Splint, pass `-fplugin=Splint` to GHC. Any ideas suggested by HLint will+be reported as warnings by GHC. For example, if you define `Main.hs` as:++``` hs+main = print . concat $ map pure [ 'a' .. 'z' ]+```++You would expect HLint to tell you to use `concatMap`. Normally you would need+to both compile your module with GHC and lint it with HLint. However with+Splint you can compile it and get suggestions from HLint all at once by+running:++``` sh+ghc -fplugin=Splint Main.hs+```++Among all the usual output from GHC, you should see this new warning:++```+Main.hs:1:8: warning:+ Use concatMap+ Perhaps: print (concatMap pure ['a' .. 'z'])+ |+1 | main = print . concat $ map pure [ 'a' .. 'z' ]+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^+```++And that's all there is to it! HLint suggestions as part of your normal build+process. What a time to be alive.++If you want to pass arguments through to HLint, you can use+`-fplugin-opt=Splint:arg`. For example you can ignore the warning above with+`-fplugin-opt=Splint:'--ignore=Use concatMap'`.++## Trade offs++Running HLint as a GHC source plugin has some upsides:++- Modules are only linted when they're compiled, so if they haven't changed+ they won't be linted again.++- HLint's suggestions are reported just like GHC's warnings. They're+ formatted the same way and they're reported at the same time.++- Each module is only parsed once.++- Parsing is done by GHC instead of something like `haskell-src-exts`. HLint+ already works like this, but by using a plugin you can be sure that all of+ the versions and options line up correctly.++However it's also got some downsides:++- Using Splint means adding it as a dependency to the targets you want to+ lint. Normally HLint is either a test dependency or just installed on the+ system.++ You may be able to lessen the impact of this by providing a flag to control+ linting. That way you can enable it locally and in CI, but not require+ everything downstream of you to depend on HLint.++ ``` cabal+ flag lint+ default: False+ manual: True+ library+ if flag(lint)+ build-depends: splint+ ghc-options: -fplugin=Splint+ ```++- It's slower. I've found that it adds about a tenth of a second per module.++- You can't use the automated refactorings that HLint provides.++- Using plugins marks every module as unsafe.++## To do++As stated, Splint is basically a tech demo. Although it's usable in its current+form, there is some low hanging fruit to fix before it should be considered+ready for production:++- [ ] Work with versions of GHC older than 8.10.+- [x] Accept command-line options using `-fplugin-opt=Splint:something`.+- [x] Reliably read HLint configuration.+- [x] Avoid re-reading HLint config for each source file.+- [x] Figure out a good output format for the warnings.+- [x] Publish to Hackage.++[HLint 3]: https://neilmitchell.blogspot.com/2020/05/hlint-30.html+[GHC source plugin]: https://downloads.haskell.org/~ghc/8.10.1/docs/html/users_guide/extending_ghc.html#source-plugins+[hlint-source-plugin]: https://github.com/ocharles/hlint-source-plugin
+ splint.cabal view
@@ -0,0 +1,27 @@+name: splint+version: 1.0.0.0++build-type: Simple+cabal-version: >= 1.10+category: Development+description: Splint is HLint as a GHC source plugin.+extra-source-files: README.markdown+license-file: LICENSE.markdown+license: ISC+maintainer: Taylor Fausak+synopsis: HLint as a GHC source plugin.++library+ build-depends:+ base >= 4.14.0 && < 4.15+ , ghc >= 8.10.1 && < 8.11+ , hlint >= 3.0 && < 3.2+ default-language: Haskell2010+ exposed-modules: Splint+ ghc-options:+ -Weverything+ -Wno-implicit-prelude+ -Wno-missing-safe-haskell-mode+ -Wno-prepositive-qualified-module+ -Wno-unsafe+ hs-source-dirs: src/lib
+ src/lib/Splint.hs view
@@ -0,0 +1,69 @@+module Splint ( plugin ) where++import qualified Bag as GHC+import qualified Data.IORef as IORef+import qualified ErrUtils as GHC+import qualified GhcPlugins as GHC+import qualified Language.Haskell.HLint as HLint+import qualified System.IO.Unsafe as Unsafe++plugin :: GHC.Plugin+plugin = GHC.defaultPlugin+ { GHC.parsedResultAction = action+ , GHC.pluginRecompile = GHC.purePlugin+ }++action+ :: [GHC.CommandLineOption]+ -> GHC.ModSummary+ -> GHC.HsParsedModule+ -> GHC.Hsc GHC.HsParsedModule+action commandLineOptions _modSummary hsParsedModule = do+ dynFlags <- GHC.getDynFlags+ GHC.liftIO $ do+ (_parseFlags, classifies, hint) <- getSettings commandLineOptions+ let+ apiAnns = GHC.hpm_annotations hsParsedModule+ hsModule = GHC.hpm_module hsParsedModule+ moduleEx = HLint.createModuleEx apiAnns hsModule+ ideas = HLint.applyHints classifies hint [moduleEx]+ GHC.printOrThrowWarnings dynFlags+ . GHC.listToBag+ . fmap (ideaToWarnMsg dynFlags)+ $ filter ((/= HLint.Ignore) . HLint.ideaSeverity) ideas+ pure hsParsedModule++type Settings = (HLint.ParseFlags, [HLint.Classify], HLint.Hint)++getSettings :: [GHC.CommandLineOption] -> IO Settings+getSettings commandLineOptions = do+ maybeSettings <- IORef.readIORef settingsRef+ case maybeSettings of+ Just settings -> pure settings+ Nothing -> do+ settings <- HLint.argsSettings commandLineOptions+ IORef.writeIORef settingsRef $ Just settings+ pure settings++{-# NOINLINE settingsRef #-}+settingsRef :: IORef.IORef (Maybe Settings)+settingsRef = Unsafe.unsafePerformIO $ IORef.newIORef Nothing++ideaToWarnMsg :: GHC.DynFlags -> HLint.Idea -> GHC.WarnMsg+ideaToWarnMsg dynFlags idea =+ let+ mkErrMsg = case HLint.ideaSeverity idea of+ HLint.Error -> GHC.mkPlainErrMsg+ _ -> GHC.mkPlainWarnMsg+ srcSpan = HLint.ideaSpan idea+ msgDoc = ideaToMsgDoc idea+ in mkErrMsg dynFlags srcSpan msgDoc++ideaToMsgDoc :: HLint.Idea -> GHC.MsgDoc+ideaToMsgDoc idea = GHC.vcat+ [ GHC.text $ HLint.ideaHint idea+ , case HLint.ideaTo idea of+ Just to | not $ null to -> GHC.text $ "Perhaps: " <> to+ _ -> GHC.empty+ , GHC.vcat . fmap (GHC.text . mappend "Note: " . show) $ HLint.ideaNote idea+ ]