@@ -4,14 +4,20 @@ import Prelude
44
55import Data.Array as Array
66import Data.Either (Either(..))
7+import Data.Enum (fromEnum)
78import Data.Foldable (for_)
89import Data.Maybe (Maybe(..))
910import Data.Newtype (unwrap)
1011import Data.String.CodeUnits as String
12+import Data.Tuple.Nested ((/\))
1113import Effect (Effect)
1214import Effect.Console (log)
1315import Node.FS.Simple as FS
1416import Node.CLI as Process
17+import Options.Applicative (Parser, argument, execParserPure, fullDesc, header, help, helper, info, long, many, metavar, progDesc, str, switch, (<**>))
18+import Options.Applicative.Builder (defaultPrefs)
19+import Options.Applicative.Types (CompletionResult(..), ParserFailure(ParserFailure), ParserResult(..))
20+import Options.Applicative.Help.Types (ParserHelp(ParserHelp), renderHelp)
1521import Purslint.Config (defaultConfig, filterRules, parseConfig)
1622import Purslint.Fix (applyAllFixes)
1723import Purslint.Rule (Rule)
@@ -99,6 +105,13 @@ import Purslint.Rules.RedundantParens (redundantParensRule)
99105import Purslint.Runner (runRules)
100106import Purslint.Types (LintResult(..), LintWarning(..), Severity(..), SourceCode(..), Suggestion(..))
101107
108+type CliOptions =
109+ { fix :: Boolean
110+ , listRules :: Boolean
111+ , initConfig :: Boolean
112+ , paths :: Array String
113+ }
114+
102115-- | All available rules (81 total)
103116allRules :: Array Rule
104117allRules =
@@ -185,12 +198,12 @@ allRules =
185198 , whenNotRule
186199 ]
187200
188−-- | Load config from .purelintrc file
201+-- | Load config from .purslintrc file
189202loadConfig :: Effect _
190203loadConfig = do
191− hasConfig <- FS.exists ".purelintrc"
204+ hasConfig <- FS.exists ".purslintrc"
192205 if hasConfig then do
193− content <- FS.readTextFile ".purelintrc"
206+ content <- FS.readTextFile ".purslintrc"
194207 pure $ parseConfig content
195208 else
196209 pure defaultConfig
@@ -294,24 +307,24 @@ fixPath rules path = do
294307-- | Show usage
295308showUsage :: Effect Unit
296309showUsage = do
297− log "purelint - a linter for PureScript (70 rules)"
310+ log "purslint - a linter for PureScript (81 rules)"
298311 log ""
299− log "Usage: purelint [OPTIONS] [FILES/DIRECTORIES...]"
312+ log "Usage: purslint [OPTIONS] [FILES/DIRECTORIES...]"
300313 log ""
301314 log "Options:"
302315 log " --help Show this help"
303316 log " --fix Auto-fix issues (where possible)"
304− log " --init Create a sample .purelintrc"
317+ log " --init Create a sample .purslintrc"
305318 log " --list List all available rules"
306319 log ""
307320 log "Configuration:"
308− log " Create .purelintrc in your project root to disable rules."
321+ log " Create .purslintrc in your project root to disable rules."
309322 log " Add one rule name per line (lines starting with # are comments)."
310323 log ""
311324 log "Examples:"
312− log " purelint src/ Lint all .purs files in src/"
313− log " purelint --fix src/ Auto-fix issues in src/"
314− log " purelint src/Main.purs Lint a single file"
325+ log " purslint src/ Lint all .purs files in src/"
326+ log " purslint --fix src/ Auto-fix issues in src/"
327+ log " purslint src/Main.purs Lint a single file"
315328
316329-- | List all available rules
317330listRules :: Effect Unit
@@ -324,51 +337,83 @@ listRules = do
324337-- | Create sample config file
325338initConfig :: Effect Unit
326339initConfig = do
327− hasConfig <- FS.exists ".purelintrc"
340+ hasConfig <- FS.exists ".purslintrc"
328341 if hasConfig then
329− log ".purelintrc already exists"
342+ log ".purslintrc already exists"
330343 else do
331− let sampleConfig = """# purelint configuration
344+ let sampleConfig = """# purslint configuration
332345# Add rule names (one per line) to disable them
333346# Example:
334347# EtaReduce
335348# LetToWhere
336349"""
337− FS.writeTextFile ".purelintrc" sampleConfig
338− log "Created .purelintrc"
350+ FS.writeTextFile ".purslintrc" sampleConfig
351+ log "Created .purslintrc"
352+
353+parseFlags :: Array String -> Effect (Maybe CliOptions)
354+parseFlags args = case parseFlags' (Array.drop 2 args) of
355+ Success cli -> pure (Just cli)
356+ Failure (ParserFailure pf) -> case pf programName of
357+ ph@(ParserHelp _) /\ exitCode /\ i /\ _unit -> do
358+ log $ renderHelp i ph
359+ Process.exit (fromEnum exitCode)
360+ pure Nothing
361+ CompletionInvoked (CompletionResult { execCompletion }) -> do
362+ executedCompletion <- execCompletion programName
363+ log executedCompletion
364+ Process.exit 0
365+ pure Nothing
366+ where
367+ programName = "purslint"
368+
369+parseFlags' :: Array String -> ParserResult CliOptions
370+parseFlags' = execParserPure defaultPrefs opts
371+ where
372+ opts =
373+ info (flagsParser <**> helper) $
374+ fullDesc
375+ <> progDesc "Lint PureScript source files"
376+ <> header "purslint - a linter for PureScript"
377+
378+flagsParser :: Parser CliOptions
379+flagsParser = ado
380+ fix <- switch $ long "fix" <> help "Auto-fix issues (where possible)"
381+ listRules <- switch $ long "list" <> help "List all available rules"
382+ initConfig <- switch $ long "init" <> help "Create a sample .purslintrc"
383+ pathsList <- many $ argument str $ metavar "PATH..."
384+ let paths = Array.fromFoldable pathsList
385+ in { fix, listRules, initConfig, paths }
339386
340387main :: Effect Unit
341388main = do
342389 args <- Process.argv
343− -- args[0] is node, args[1] is the script, args[2..] are the actual arguments
344− let userArgs = Array.drop 1 args
345− case userArgs of
346− [] -> showUsage
347− ["--help"] -> showUsage
348− ["-h"] -> showUsage
349− ["--list"] -> listRules
350− ["--init"] -> initConfig
351− _ -> do
352− -- Check for --fix flag
353− let hasFix = Array.elem "--fix" userArgs
354− let paths = Array.filter (\a -> a /= "--fix") userArgs
355−
356− -- Load config and filter rules
357− config <- loadConfig
358− let enabledRules = filterRules allRules config
359−
360− if hasFix then do
361− -- Fix mode
362− _ <- Array.foldM (\acc path -> do
363− count <- fixPath enabledRules path
364− pure (acc + count)) 0 paths
365− pure unit
390+ maybeCli <- parseFlags args
391+ case maybeCli of
392+ Nothing -> pure unit
393+ Just cli ->
394+ if cli.listRules then
395+ listRules
396+ else if cli.initConfig then
397+ initConfig
398+ else if Array.null cli.paths then
399+ showUsage
366400 else do
367− -- Lint mode
368− totalWarnings <- Array.foldM (\acc path -> do
369− count <- lintPathWithConfig enabledRules path
370− pure (acc + count)) 0 paths
371− when (totalWarnings > 0) do
372− log ""
373− log $ "Total: " <> show totalWarnings <> " warning" <> if totalWarnings == 1 then "" else "s"
374− Process.exit 1
401+ -- Load config and filter rules
402+ config <- loadConfig
403+ let enabledRules = filterRules allRules config
404+
405+ if cli.fix then do
406+ -- Fix mode
407+ _ <- Array.foldM (\acc path -> do
408+ count <- fixPath enabledRules path
409+ pure (acc + count)) 0 cli.paths
410+ pure unit
411+ else do
412+ -- Lint mode
413+ totalWarnings <- Array.foldM (\acc path -> do
414+ count <- lintPathWithConfig enabledRules path
415+ pure (acc + count)) 0 cli.paths
416+ when (totalWarnings > 0) do
417+ log ""
418+ log $ "Total: " <> show totalWarnings <> " warning" <> if totalWarnings == 1 then "" else "s"
419+ Process.exit 1