{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PartialTypeSignatures #-} module Main where import Control.Exception (Exception, catch, throw) import Data.Aeson qualified as A import Data.ByteString.Lazy (ByteString) import Data.ByteString.Lazy qualified as LB import Data.ByteString.Lazy.Char8 qualified as LB8 import Data.Maybe qualified as Maybe import Data.String qualified as String import GHC.Generics (Generic) import Options.Applicative ((<**>)) import Options.Applicative qualified as O import System.Exit (ExitCode (ExitFailure), exitWith) import System.FilePath as F import System.IO (hPutStrLn, stderr) import System.Process.Typed qualified as P data Options = Options { optCommand :: Command } deriving (Show) data Command = List | Show deriving (Show) optionsParser :: O.Parser Options optionsParser = Options <$> commandParser commandParser :: O.Parser Command commandParser = O.subparser ( O.command "list" (O.info listCommandParser (O.progDesc "List all issues")) <> O.command "show" (O.info showCommandParser (O.progDesc "Show details of all issues")) ) listCommandParser :: O.Parser Command listCommandParser = pure List showCommandParser :: O.Parser Command showCommandParser = pure Show main :: IO () main = do options <- O.execParser (O.info (commandParser <**> O.helper) O.idm) files <- getFiles issues <- catch ( fmap Maybe.catMaybes $ mapM (\filename -> catch (fmap Just (getIssues filename)) (forgetGetIssuesExceptions)) files ) ( \(InvalidTreeGrepperResult error) -> do hPutStrLn stderr error exitWith (ExitFailure 1) ) mapM_ putStrLn $ fmap file $ concat issues data UnknownFileExtension = UnknownFileExtension { extension :: String } deriving (Show) instance Exception UnknownFileExtension data InvalidTreeGrepperResult = InvalidTreeGrepperResult { error :: String } deriving (Show) instance Exception InvalidTreeGrepperResult forgetGetIssuesExceptions :: UnknownFileExtension -> IO (Maybe _) forgetGetIssuesExceptions _ = pure Nothing data Issue = Issue {} data TreeGrepperResult = TreeGrepperResult { file :: String, file_type :: String, matches :: [Match] } deriving (Show, Generic) instance A.FromJSON TreeGrepperResult data Match = Match { kind :: String, name :: String, text :: String, start :: Position, end :: Position } deriving (Show, Generic) instance A.FromJSON Match data Position = Position { row :: Int, column :: Int } deriving (Show, Generic) instance A.FromJSON Position getIssues :: String -> IO [TreeGrepperResult] getIssues filename = let extension = F.takeExtension filename treeGrepperLanguage = case extension of ".elm" -> "elm" ".nix" -> "nix" ".sh" -> "sh" _ -> throw (UnknownFileExtension extension) treeGrepperQuery = case extension of ".elm" -> "([(line_comment) (block_comment)])" ".nix" -> "(comment)" ".sh" -> "(comment)" _ -> throw (UnknownFileExtension extension) decode raw = case A.eitherDecode raw of Left error -> throw (InvalidTreeGrepperResult error) Right treeGrepperResult -> treeGrepperResult in fmap (decode . snd) $ P.readProcessStdout ( String.fromString ( "tree-grepper --query '" ++ treeGrepperLanguage ++ "' '" ++ treeGrepperQuery ++ "' --format json '" ++ filename ++ "'" ) ) getFiles :: IO [String] getFiles = fmap (lines . LB8.unpack . snd) $ P.readProcessStdout "git ls-files --cached --exclude-standard --other"