content: {-
      Copyright 2012-2024 Vidar Holen
  
      This file is part of ShellCheck.
      https://www.shellcheck.net
  
      ShellCheck is free software: you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
      the Free Software Foundation, either version 3 of the License, or
      (at your option) any later version.
  
      ShellCheck is distributed in the hope that it will be useful,
      but WITHOUT ANY WARRANTY; without even the implied warranty of
      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      GNU General Public License for more details.
  
      You should have received a copy of the GNU General Public License
      along with this program.  If not, see <https://www.gnu.org/licenses/>.
  -}
  {-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
  module ShellCheck.Interface
      (
      SystemInterface(..)
      , CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks)
      , CheckResult(crFilename, crComments)
      , ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
      , ParseResult(prComments, prTokenPositions, prRoot)
      , AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
      , AnalysisResult(arComments)
      , FormatterOptions(foColorOption, foWikiLinkCount)
      , Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
      , ExecutionMode(Executed, Sourced)
      , ErrorMessage
      , Code
      , Severity(ErrorC, WarningC, InfoC, StyleC)
      , Position(posFile, posLine, posColumn)
      , Comment(cSeverity, cCode, cMessage)
      , PositionedComment(pcStartPos , pcEndPos , pcComment, pcFix)
      , ColorOption(ColorAuto, ColorAlways, ColorNever)
      , TokenComment(tcId, tcComment, tcFix)
      , emptyCheckResult
      , newAnalysisResult
      , newAnalysisSpec
      , newFormatterOptions
      , newParseResult
      , newPosition
      , newSystemInterface
      , newTokenComment
      , mockedSystemInterface
      , mockRcFile
      , newParseSpec
      , emptyCheckSpec
      , newPositionedComment
      , newComment
      , Fix(fixReplacements)
      , newFix
      , InsertionPoint(InsertBefore, InsertAfter)
      , Replacement(repStartPos, repEndPos, repString, repPrecedence, repInsertionPoint)
      , newReplacement
      , CheckDescription(cdName, cdDescription, cdPositive, cdNegative)
      , newCheckDescription
      ) where
  
  import ShellCheck.AST
  
  import Control.DeepSeq
  import Control.Monad.Identity
  import Data.List
  import Data.Monoid
  import Data.Ord
  import Data.Semigroup
  import GHC.Generics (Generic)
  import qualified Data.Map as Map
  
  
  data SystemInterface m = SystemInterface {
      -- | Given:
      --   What annotations say about including external files (if anything)
      --   A resolved filename from siFindSource
      --   Read the file or return an error
      siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
      -- | Given:
      --   the current script,
      --   what annotations say about including external files (if anything)
      --   a list of source-path annotations in effect,
      --   and a sourced file,
      --   find the sourced file
      siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
      -- | Get the configuration file (name, contents) for a filename
      siGetConfig :: String -> m (Maybe (FilePath, String))
  }
  
  -- ShellCheck input and output
  data CheckSpec = CheckSpec {
      csFilename :: String,
      csScript :: String,
      csCheckSourced :: Bool,
      csIgnoreRC :: Bool,
      csExcludedWarnings :: [Integer],
      csIncludedWarnings :: Maybe [Integer],
      csShellTypeOverride :: Maybe Shell,
      csMinSeverity :: Severity,
      csExtendedAnalysis :: Maybe Bool,
      csOptionalChecks :: [String]
  } deriving (Show, Eq)
  
  data CheckResult = CheckResult {
      crFilename :: String,
      crComments :: [PositionedComment]
  } deriving (Show, Eq)
  
  emptyCheckResult :: CheckResult
  emptyCheckResult = CheckResult {
      crFilename = "",
      crComments = []
  }
  
  emptyCheckSpec :: CheckSpec
  emptyCheckSpec = CheckSpec {
      csFilename = "",
      csScript = "",
      csCheckSourced = False,
      csIgnoreRC = False,
      csExcludedWarnings = [],
      csIncludedWarnings = Nothing,
      csShellTypeOverride = Nothing,
      csMinSeverity = StyleC,
      csExtendedAnalysis = Nothing,
      csOptionalChecks = []
  }
  
  newParseSpec :: ParseSpec
  newParseSpec = ParseSpec {
      psFilename = "",
      psScript = "",
      psCheckSourced = False,
      psIgnoreRC = False,
      psShellTypeOverride = Nothing
  }
  
  newSystemInterface :: Monad m => SystemInterface m
  newSystemInterface =
      SystemInterface {
          siReadFile = \_ _ -> return $ Left "Not implemented",
          siFindSource = \_ _ _ name -> return name,
          siGetConfig = \_ -> return Nothing
      }
  
  -- Parser input and output
  data ParseSpec = ParseSpec {
      psFilename :: String,
      psScript :: String,
      psCheckSourced :: Bool,
      psIgnoreRC :: Bool,
      psShellTypeOverride :: Maybe Shell
  } deriving (Show, Eq)
  
  data ParseResult = ParseResult {
      prComments :: [PositionedComment],
      prTokenPositions :: Map.Map Id (Position, Position),
      prRoot :: Maybe Token
  } deriving (Show, Eq)
  
  newParseResult :: ParseResult
  newParseResult = ParseResult {
      prComments = [],
      prTokenPositions = Map.empty,
      prRoot = Nothing
  }
  
  -- Analyzer input and output
  data AnalysisSpec = AnalysisSpec {
      asScript :: Token,
      asShellType :: Maybe Shell,
      asFallbackShell :: Maybe Shell,
      asExecutionMode :: ExecutionMode,
      asCheckSourced :: Bool,
      asOptionalChecks :: [String],
      asExtendedAnalysis :: Maybe Bool,
      asTokenPositions :: Map.Map Id (Position, Position)
  }
  
  newAnalysisSpec token = AnalysisSpec {
      asScript = token,
      asShellType = Nothing,
      asFallbackShell = Nothing,
      asExecutionMode = Executed,
      asCheckSourced = False,
      asOptionalChecks = [],
      asExtendedAnalysis = Nothing,
      asTokenPositions = Map.empty
  }
  
  newtype AnalysisResult = AnalysisResult {
      arComments :: [TokenComment]
  }
  
  newAnalysisResult = AnalysisResult {
      arComments = []
  }
  
  -- Formatter options
  data FormatterOptions = FormatterOptions {
      foColorOption :: ColorOption,
      foWikiLinkCount :: Integer
  }
  
  newFormatterOptions = FormatterOptions {
      foColorOption = ColorAuto,
      foWikiLinkCount = 3
  }
  
  data CheckDescription = CheckDescription {
      cdName :: String,
      cdDescription :: String,
      cdPositive :: String,
      cdNegative :: String
      }
  
  newCheckDescription = CheckDescription {
      cdName = "",
      cdDescription = "",
      cdPositive = "",
      cdNegative = ""
      }
  
  -- Supporting data types
  data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq)
  data ExecutionMode = Executed | Sourced deriving (Show, Eq)
  
  type ErrorMessage = String
  type Code = Integer
  
  data Severity = ErrorC | WarningC | InfoC | StyleC
      deriving (Show, Eq, Ord, Generic, NFData)
  data Position = Position {
      posFile :: String,    -- Filename
      posLine :: Integer,   -- 1 based source line
      posColumn :: Integer  -- 1 based source column, where tabs are 8
  } deriving (Show, Eq, Generic, NFData, Ord)
  
  newPosition :: Position
  newPosition = Position {
      posFile   = "",
      posLine   = 1,
      posColumn = 1
  }
  
  data Comment = Comment {
      cSeverity :: Severity,
      cCode     :: Code,
      cMessage  :: String
  } deriving (Show, Eq, Generic, NFData)
  
  newComment :: Comment
  newComment = Comment {
      cSeverity = StyleC,
      cCode     = 0,
      cMessage  = ""
  }
  
  -- only support single line for now
  data Replacement = Replacement {
      repStartPos :: Position,
      repEndPos :: Position,
      repString :: String,
      -- Order in which the replacements should happen: highest precedence first.
      repPrecedence :: Int,
      -- Whether to insert immediately before or immediately after the specified region.
      repInsertionPoint :: InsertionPoint
  } deriving (Show, Eq, Generic, NFData)
  
  data InsertionPoint = InsertBefore | InsertAfter
      deriving (Show, Eq, Generic, NFData)
  
  newReplacement = Replacement {
      repStartPos = newPosition,
      repEndPos = newPosition,
      repString = "",
      repPrecedence = 1,
      repInsertionPoint = InsertAfter
  }
  
  data Fix = Fix {
      fixReplacements :: [Replacement]
  } deriving (Show, Eq, Generic, NFData)
  
  newFix = Fix {
      fixReplacements = []
  }
  
  data PositionedComment = PositionedComment {
      pcStartPos :: Position,
      pcEndPos   :: Position,
      pcComment  :: Comment,
      pcFix      :: Maybe Fix
  } deriving (Show, Eq, Generic, NFData)
  
  newPositionedComment :: PositionedComment
  newPositionedComment = PositionedComment {
      pcStartPos = newPosition,
      pcEndPos   = newPosition,
      pcComment  = newComment,
      pcFix      = Nothing
  }
  
  data TokenComment = TokenComment {
      tcId :: Id,
      tcComment :: Comment,
      tcFix :: Maybe Fix
  } deriving (Show, Eq, Generic, NFData)
  
  newTokenComment = TokenComment {
      tcId = Id 0,
      tcComment = newComment,
      tcFix = Nothing
  }
  
  data ColorOption =
      ColorAuto
      | ColorAlways
      | ColorNever
    deriving (Ord, Eq, Show)
  
  -- For testing
  mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
  mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
      siReadFile = rf,
      siFindSource = fs,
      siGetConfig = const $ return Nothing
  }
    where
      rf _ file = return $
          case find ((== file) . fst) files of
              Nothing -> Left "File not included in mock."
              Just (_, contents) -> Right contents
      fs _ _ _ file = return file
  
  mockRcFile rcfile mock = mock {
      siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
  }
  