packages feed

glualint-1.26.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