packages feed

glualint-1.25.0: app/GLuaFixer/Cli.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}

-- | CLI interface of the glualint executable
module GLuaFixer.Cli (SettingsPath (..), Options (..), Command (..), OverriddenSettings (..), overrideSettings, runParse, legacyCliParser) where

import Control.Applicative ((<**>), (<|>))
import Data.Maybe (fromMaybe)
import GLuaFixer.LintMessage (LogFormat (..), LogFormatChoice (..))
import GLuaFixer.LintSettings (Indentation (..), LintSettings (..), StdInOrFiles (..))
import Options.Applicative (optional)
import qualified Options.Applicative as Opt

newtype SettingsPath = SettingsPath FilePath
  deriving newtype Show

-- | Command line options of glualint
data Options = Options
  { optsConfigFile :: Maybe SettingsPath
  , optsOverridden :: OverriddenSettings
  , optsCommand :: Command
  , optsFiles :: StdInOrFiles
  , optsDebug :: Bool
  } deriving (Show)

-- | Available subcommands
data Command
  = Lint
  | PrettyPrint
  | AnalyseGlobals
  | DumpLexicon
  | DumpAst
  | Test
  | PrintVersion
  deriving (Show)

-- | Settings in the config file that can be overridden through the command line.
data OverriddenSettings = OverriddenSettings
  { indentation :: Maybe Indentation
  , outputFormat :: Maybe LogFormatChoice
  } deriving Show

-- | Override settings with the options passed on the command line
overrideSettings :: OverriddenSettings -> LintSettings -> LintSettings
overrideSettings overridden settings =
  settings
    { prettyprint_indentation =
        maybe settings.prettyprint_indentation unIndentation overridden.indentation
    , log_format = fromMaybe settings.log_format overridden.outputFormat
    }

-- | Run the parser against arguments
runParse :: [String] -> Opt.ParserResult Options
runParse = Opt.execParserPure cliPreferences cliParserInfo

-- | Preferences passed to optparse-applicative
cliPreferences :: Opt.ParserPrefs
cliPreferences = Opt.defaultPrefs{Opt.prefShowHelpOnEmpty = True}

-- | Metadata of the application
cliParserInfo :: Opt.ParserInfo Options
cliParserInfo =
  Opt.info
    (cliParser <**> Opt.helper)
    ( Opt.fullDesc
        <> Opt.progDesc "Linter and pretty printer for Garry's mod's flavour of Lua."
        <> Opt.header "glualint - lint and pretty print GLua files."
    )

-- | Defines the command line interface parser
cliParser :: Opt.Parser Options
cliParser =
  Options
    <$> configOption
    <*> (OverriddenSettings <$> indentOption <*> outputFormatOption)
    <*> commandParser
    <*> parseStdInOrFiles
    <*> parseDebug
 where
  configOption :: Opt.Parser (Maybe SettingsPath)
  configOption =
    optional $
      SettingsPath
        <$> Opt.strOption
          ( Opt.long "config"
              <> Opt.metavar "PATH"
              <> Opt.help "Explicitly define config file location. By default it will search for it."
          )

  indentOption :: Opt.Parser (Maybe Indentation)
  indentOption =
    optional $
      Opt.strOption
        ( Opt.long "indentation"
            <> Opt.metavar "STR"
            <> Opt.help "What to use for indentation when pretty printing, 4 spaces by default."
        )

  outputFormatOption :: Opt.Parser (Maybe LogFormatChoice)
  outputFormatOption =
    optional $
      Opt.option
        outputFormatReader
        ( Opt.long "output-format"
            <> Opt.metavar "FORMAT"
            <> Opt.help "Logging format, either 'auto', 'standard' or 'github', defaults to 'standard'"
        )

  outputFormatReader :: Opt.ReadM LogFormatChoice
  outputFormatReader = Opt.eitherReader $ \case
    "standard" -> Right $ LogFormatChoice StandardLogFormat
    "github" -> Right $ LogFormatChoice GithubLogFormat
    "auto" -> Right AutoLogFormatChoice
    val -> Left $ "Bad output format '" <> val <> "', must be either 'auto', 'standard' or 'github'."

  parseDebug :: Opt.Parser Bool
  parseDebug =
      Opt.switch
        ( Opt.long "debug"
            <> Opt.help "Whether to enable some debug prints."
        )

  commandParser :: Opt.Parser Command
  commandParser =
    Opt.hsubparser
      ( Opt.command
          "lint"
          ( Opt.info (pure Lint) $
              Opt.progDesc "Lint the given files. Directories will be traversed recursively."
          )
          <> Opt.command
            "pretty-print"
            ( Opt.info (pure PrettyPrint) $
                Opt.progDesc "Pretty print the given files, replacing their contents with the pretty printed code."
            )
          <> Opt.command
            "analyse-globals"
            ( Opt.info (pure AnalyseGlobals) $
                Opt.progDesc "Print a list of all globals used and defined in the given files/directories."
            )
          <> Opt.command
            "dump-lexicon"
            ( Opt.info (pure DumpLexicon) $
                Opt.progDesc "Debug command to dump the tokens parsed in the Lua code."
            )
          <> Opt.command
            "dump-ast"
            ( Opt.info (pure DumpAst) $
                Opt.progDesc "Debug command to dump the internal Abstract Syntax Tree of the Lua code."
            )
          <> Opt.command
            "test"
            ( Opt.info (pure Test) $
                Opt.progDesc "Run tests on the given files. Use for testing/debugging glualint."
            )
          <> Opt.command
            "version"
            ( Opt.info (pure PrintVersion) $
                Opt.progDesc "Print the version of glualint and exit."
            )
      )

  parseStdInOrFiles :: Opt.Parser StdInOrFiles
  parseStdInOrFiles =
    Opt.flag'
      UseStdIn
      ( Opt.long "stdin"
          <> Opt.help "Use stdin instead of files."
      )
      <|> UseFiles <$> filesArgument

  filesArgument :: Opt.Parser [FilePath]
  filesArgument = Opt.many (Opt.argument Opt.str $ Opt.metavar "FILES")

--
-- Legacy
--

{- | Deprecated and naive command line interface, kept in place for backwards
compatibility.
-}
legacyCliParser :: [String] -> Maybe Options
legacyCliParser args = case go False emptyOptions args of
  (False, _) -> Nothing
  (True, opts@Options{optsCommand = Lint, optsFiles = files}) -> case files of
    UseFiles [] -> Nothing
    _ -> Just opts
  (True, opts) -> Just opts
 where
  emptyOptions =
    Options
      { optsConfigFile = Nothing
      , optsOverridden = OverriddenSettings Nothing Nothing
      , optsCommand = Lint
      , optsFiles = UseFiles []
      , optsDebug = False
      }

  go hasSuccessfulParse options = \case
    ["--config"] -> (False, options)
    -- fail when a subcommand of the new parser is given as argument
    "lint" : _ -> (False, options)
    "pretty-print" : _ -> (False, options)
    "analyse-globals" : _ -> (False, options)
    "dump-ast" : _ -> (False, options)
    "test" : _ -> (False, options)
    "version" : _ -> (False, options)
    "--bash-completion-script" : _ -> (False, options)
    "--fish-completion-script" : _ -> (False, options)
    "--zsh-completion-script" : _ -> (False, options)
    -- End of recursion case
    [] -> (hasSuccessfulParse, options)
    "--debug" : xs -> go True options{optsDebug = True} xs
    "--pretty-print-files" : xs -> go True options{optsCommand = PrettyPrint} xs
    "--pretty-print" : xs -> go True options{optsCommand = PrettyPrint, optsFiles = UseStdIn} xs
    "--analyse-globals" : xs -> go True options{optsCommand = AnalyseGlobals} xs
    "--dump-ast" : xs -> go True options{optsCommand = DumpAst} xs
    "--version" : xs -> go True options{optsCommand = PrintVersion} xs
    "--test" : xs -> go True options{optsCommand = Test} xs
    "--stdin" : xs -> go True options{optsFiles = UseStdIn} xs
    "--config" : f : xs -> go True options{optsConfigFile = Just $ SettingsPath f} xs
    ('-' : '-' : 'i' : 'n' : 'd' : 'e' : 'n' : 't' : 'a' : 't' : 'i' : 'o' : 'n' : '=' : '\'' : ind') : xs ->
      go True options{optsOverridden = options.optsOverridden{indentation = Just $ Indentation ind'}} xs
    ('-' : '-' : 'i' : 'n' : 'd' : 'e' : 'n' : 't' : 'a' : 't' : 'i' : 'o' : 'n' : '=' : ind') : xs ->
      go True options{optsOverridden = options.optsOverridden{indentation = Just $ Indentation ind'}} xs
    f : xs -> case optsFiles options of
      UseStdIn -> go True options{optsFiles = UseFiles [f]} xs
      UseFiles fs -> go True options{optsFiles = UseFiles $ f : fs} xs