{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-} -- : out lint -- -- these are actually runtime deps, but bild doesn't (yet) distinguish between -- rundeps and sysdeps: -- -- : sys ormolu -- : sys hlint -- : sys black -- : sys ruff -- : sys deadnix -- : sys shellcheck -- : sys indent module Biz.Lint (main) where import Alpha import qualified Biz.Cli as Cli import qualified Biz.Log as Log import Biz.Namespace (Ext (..), Namespace (..)) import qualified Biz.Namespace as Namespace import Biz.Test ((@=?)) import qualified Biz.Test as Test import qualified Data.Map as Map import qualified Data.String as String import qualified Data.Text as Text import qualified System.Directory as Directory import qualified System.Environment as Environment import qualified System.Exit as Exit import qualified System.Process as Process main :: IO () main = Cli.main <| Cli.Plan help move test pure move :: Cli.Arguments -> IO () move args = Environment.getEnv "BIZ_ROOT" +> \root -> case Cli.getAllArgs args (Cli.argument "file") of [] -> changedFiles +> traverse Directory.makeAbsolute /> map (Namespace.fromPath root) /> catMaybes /> Namespace.groupByExt +> run mode +> exit files -> files |> filter (not <. Namespace.isCab) |> traverse Directory.makeAbsolute +> filterM Directory.doesFileExist /> map (Namespace.fromPath root) /> catMaybes /> Namespace.groupByExt +> run mode +> exit where mode = args `Cli.has` Cli.longOption "fix" ?: (Fix, Check) test :: Test.Tree test = Test.group "Biz.Lint" [ Test.unit "haskell files return two Results" <| do results <- run Check <| Map.singleton Hs <| [Namespace ["Biz", "Lint"] Hs] length results @=? 2 ] help :: Cli.Docopt help = [Cli.docopt| all your lint are belong to us Usage: lint test lint [--fix] [...] lint -h, --help |] exit :: [Result] -> IO () exit results = Exit.exitWith <| if n > 0 then Exit.ExitFailure n else Exit.ExitSuccess where n = length <| filter bad results bad = \case (Warn _) -> False Done {status = Bad _} -> True _ -> False printResult :: Result -> IO Result printResult r@(Warn err) = Log.warn ["lint", err] >> Log.br >> pure r printResult r@(Done Linter {..} (Bad err)) = Log.fail ["lint", exe] >> Log.br >> if err == "" then pure r else putText (Text.pack err) >> pure r printResult r@(Done Linter {..} Good) = Log.good ["lint", exe] >> Log.br >> pure r printResult r@(NoOp ext) = Log.info ["lint", "noop", show ext] >> pure r changedFiles :: IO [FilePath] changedFiles = git ["merge-base", "HEAD", "origin/master"] /> filter (/= '\n') +> (\mb -> git ["diff", "--name-only", "--diff-filter=d", mb]) /> String.lines where git args = Process.readProcess "git" args "" data Mode = -- | Just check the files and return an exit code. Check | -- | Fix the files in place, return 0 if successful, otherwise return 1. Fix data Linter = Linter { exe :: Text, -- | Arguments added when in the "check" mode. checkArgs :: [Text], -- | Arguments added when in the "fix" mode. fixArgs :: Maybe [Text] } deriving (Show) ormolu :: Linter ormolu = Linter { exe = "ormolu", checkArgs = ["--mode", "check", "--no-cabal"], fixArgs = Just ["--mode", "inplace", "--no-cabal"] } hlint :: Linter hlint = Linter { exe = "hlint", checkArgs = [], -- needs apply-refact >0.9.1.0, which needs ghc >9 -- fixArgs = Just ["--refactor", "--refactor-options=-i"] fixArgs = Nothing } black :: Linter black = Linter { exe = "black", checkArgs = ["--check"], fixArgs = Just [] } ruff :: Linter ruff = Linter { exe = "ruff", checkArgs = ["check"], fixArgs = Just ["check", "--fix"] } deadnix :: Linter deadnix = Linter { exe = "deadnix", checkArgs = "--fail" : commonArgs, fixArgs = Just <| "--edit" : commonArgs } where commonArgs = ["--no-underscore", "--no-lambda-pattern-names"] shellcheck :: Linter shellcheck = Linter {exe = "shellcheck", checkArgs = [], fixArgs = Nothing} indent :: Linter indent = Linter {exe = "indent", checkArgs = [], fixArgs = Nothing} data Status = Good | Bad String deriving (Show) data Result = Done {linter :: Linter, status :: Status} | Warn Text | NoOp Namespace.Ext deriving (Show) run :: Mode -> Map Namespace.Ext [Namespace] -> IO [Result] run mode nsmap = nsmap |> Map.assocs |> traverse (runOne mode) /> concat runOne :: Mode -> (Ext, [Namespace]) -> IO [Result] runOne mode (ext, ns's) = results +> traverse_ printResult >> results where results = sequence <| case ext of Namespace.Hs -> [ lint mode ormolu ns's, lint mode hlint ns's ] Namespace.Py -> [ lint mode black ns's, lint mode ruff ns's ] Namespace.Sh -> [lint mode shellcheck ns's] Namespace.Nix -> [lint mode deadnix ns's] Namespace.C -> [lint mode indent ns's] _ -> [pure <. Warn <| "no linter for " <> show ext] lint :: Mode -> Linter -> [Namespace] -> IO Result lint mode linter@Linter {..} ns's = Process.readProcessWithExitCode (Text.unpack exe) args "" /> \case (Exit.ExitSuccess, _, _) -> Done linter Good (Exit.ExitFailure _, msg, _) -> Done linter <| Bad msg where args = case (mode, fixArgs) of (Fix, Just args_) -> map Text.unpack args_ ++ map Namespace.toPath ns's (Fix, Nothing) -> map Namespace.toPath ns's (Check, _) -> map Text.unpack checkArgs ++ map Namespace.toPath ns's