module Main where import Agda.Unused.Check (checkUnused, checkUnusedLocal) import qualified Agda.Unused.Monad.Error as E import Agda.Unused.Parse (parseConfig) import qualified Agda.Unused.Print as P import Agda.Unused.Types.Name (Name(..), NamePart(..), QName(..)) import Agda.Unused.Utils (liftMaybe, mapLeft) import Control.Monad.Except (MonadError, liftEither, runExceptT, throwError) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Aeson (Value(..), (.=), object) import Data.Aeson.Text (encodeToLazyText) import Data.Bool (bool) import Data.List (stripPrefix) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as I import Data.Text.Lazy (toStrict) import Options.Applicative (InfoMod, Parser, ParserInfo, execParser, fullDesc, header, help, helper, info, long, metavar, optional, progDesc, short, strOption, switch) import System.Directory (doesFileExist, getCurrentDirectory, makeAbsolute) import System.Exit (exitFailure, exitSuccess) import System.FilePath ((), splitDirectories, stripExtension, takeDirectory) import System.IO (stderr) -- ## Options data Options = Options { optionsRoot :: !(Maybe FilePath) -- ^ The project root path. , optionsFile :: !(Maybe FilePath) -- ^ A file path to check locally. , optionsJSON :: !Bool -- ^ Whether to output JSON. } deriving Show optionsParser :: Parser Options optionsParser = Options <$> optional (strOption $ short 'r' <> long "root" <> metavar "ROOT" <> help "Path of project root directory") <*> optional (strOption $ short 'l' <> long "local" <> metavar "FILE" <> help "Path of file to check locally") <*> (switch $ short 'j' <> long "json" <> help "Format output as JSON") optionsInfo :: InfoMod a optionsInfo = fullDesc <> progDesc "Check for unused code in project with root directory ROOT" <> header "agda-unused - check for unused code in an Agda project" options :: ParserInfo Options options = info (helper <*> optionsParser) optionsInfo -- ## Errors data Error where ErrorFile :: !FilePath -> Error ErrorLocal :: !FilePath -> Error ErrorParse :: !Text -> Error deriving Show -- ## Check check :: Options -> IO () check o@(Options _ _ j) = runExceptT (check' o) >>= either (printErrorWith j) (const (pure ())) check' :: MonadError Error m => MonadIO m => Options -> m () check' o@(Options _ Nothing j) = do rootPath <- liftIO (getRootDirectory o) configPath <- pure (rootPath ".agda-roots") exists <- liftIO (doesFileExist configPath) _ <- bool (throwError (ErrorFile configPath)) (pure ()) exists contents <- liftIO (I.readFile configPath) roots <- liftEither (mapLeft ErrorParse (parseConfig contents)) checkResult <- liftIO (checkUnused rootPath roots) _ <- liftIO (printResult j P.printUnused checkResult) pure () check' o@(Options _ (Just f) j) = do rootPath <- liftIO (getRootDirectory o) rootPath' <- pure (splitDirectories rootPath) filePath <- liftIO (makeAbsolute f >>= \f' -> pure (splitDirectories f')) localModule <- liftMaybe (ErrorLocal f) (stripPrefix rootPath' filePath >>= pathModule) checkResult <- liftIO (checkUnusedLocal rootPath localModule) _ <- liftIO (printResult j P.printUnusedItems checkResult) pure () pathModule :: [FilePath] -> Maybe QName pathModule [] = Nothing pathModule (p : []) = QName . name <$> stripExtension "agda" p pathModule (p : ps@(_ : _)) = Qual (name p) <$> pathModule ps name :: String -> Name name p = Name [Id p] -- ## Print printResult :: Bool -- ^ Whether to output JSON. -> (a -> Maybe Text) -> Either E.Error a -> IO () printResult False _ (Left e) = I.hPutStrLn stderr (P.printError e) >> exitFailure printResult False p (Right x) = I.putStrLn (maybe P.printNothing id (p x)) >> exitSuccess printResult True p x = I.putStrLn (toStrict (encodeToLazyText (printResultJSON p x))) printErrorWith :: Bool -- ^ Whether to output JSON. -> Error -> IO () printErrorWith False e = I.hPutStrLn stderr (printError e) >> exitFailure printErrorWith True e = I.putStrLn (toStrict (encodeToLazyText (printErrorJSON e))) printError :: Error -> Text printError (ErrorFile p) = "Error: .agda-roots file not found " <> parens (T.pack p) <> "." printError (ErrorLocal l) = "Error: Invalid local path " <> parens (T.pack l) <> "." printError (ErrorParse t) = t parens :: Text -> Text parens t = "(" <> t <> ")" -- ## JSON printErrorJSON :: Error -> Value printErrorJSON e = encodeMessage "error" (printError e) printResultJSON :: (a -> Maybe Text) -> Either E.Error a -> Value printResultJSON _ (Left e) = encodeMessage "error" (P.printError e) printResultJSON p (Right u) = maybe (encodeMessage "none" P.printNothing) (encodeMessage "unused") (p u) encodeMessage :: Text -- ^ Type of message. -> Text -- ^ Contents of message. -> Value encodeMessage t m = object ["type" .= t, "message" .= m] -- ## Root getRootDirectory :: Options -> IO FilePath getRootDirectory (Options Nothing Nothing _) = getCurrentDirectory >>= \p -> getRootDirectoryFrom p p getRootDirectory (Options Nothing (Just p) _) = makeAbsolute p >>= \p' -> getRootDirectoryFrom (takeDirectory p') (takeDirectory p') getRootDirectory (Options (Just p) _ _) = makeAbsolute p -- Search recursively upwards for project root directory. getRootDirectoryFrom :: FilePath -- ^ Default directory. -> FilePath -- ^ Starting directory. -> IO FilePath getRootDirectoryFrom d p = doesFileExist (p ".agda-roots") >>= getRootDirectoryWith d p getRootDirectoryWith :: FilePath -- ^ Default directory. -> FilePath -- ^ Starting directory. -> Bool -- ^ Whether the starting directory contains an .agda-roots file. -> IO FilePath getRootDirectoryWith _ p True = pure p getRootDirectoryWith d p False | takeDirectory p == p = pure d getRootDirectoryWith d p False = getRootDirectoryFrom d (takeDirectory p) -- ## Main main :: IO () main = execParser options >>= check