hadolint-2.5.0: src/Hadolint/Formatter/Checkstyle.hs
module Hadolint.Formatter.Checkstyle
( printResults,
formatResult,
)
where
import qualified Control.Foldl as Foldl
import qualified Data.ByteString.Builder as Builder
import qualified Data.ByteString.Lazy.Char8 as B
import Data.Char (isAsciiLower, isAsciiUpper, isDigit, ord)
import qualified Data.Text as Text
import Data.Text.Encoding (encodeUtf8Builder)
import Hadolint.Formatter.Format
( Result (..),
errorBundlePretty,
errorPosition,
severityText,
)
import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))
import System.IO (stdout)
import Text.Megaparsec (TraversableStream)
import Text.Megaparsec.Error
( ParseErrorBundle,
ShowErrorComponent,
)
import Text.Megaparsec.Pos (sourceColumn, sourceLine, unPos)
import Text.Megaparsec.Stream (VisualStream)
data CheckStyle = CheckStyle
{ line :: Int,
column :: Int,
impact :: Text.Text,
msg :: Text.Text,
source :: Text.Text
}
errorToCheckStyle :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> CheckStyle
errorToCheckStyle err =
CheckStyle
{ line = unPos (sourceLine pos),
column = unPos (sourceColumn pos),
impact = severityText DLErrorC,
msg = Text.pack (errorBundlePretty err),
source = "DL1000"
}
where
pos = errorPosition err
ruleToCheckStyle :: CheckFailure -> CheckStyle
ruleToCheckStyle CheckFailure {..} =
CheckStyle
{ line = line,
column = 1,
impact = severityText severity,
msg = message,
source = unRuleCode code
}
toXml :: CheckStyle -> Builder.Builder
toXml CheckStyle {..} =
"<error "
<> attr "line" (Builder.intDec line)
<> attr "column" (Builder.intDec column)
<> attr "severity" (encode impact)
<> attr "message" (encode msg)
<> attr "source" (encode source)
<> "/>"
encode :: Text.Text -> Builder.Builder
encode = encodeUtf8Builder . escape
attr :: Text.Text -> Builder.Builder -> Builder.Builder
attr name value = encodeUtf8Builder name <> "='" <> value <> "' "
escape :: Text.Text -> Text.Text
escape = Text.concatMap doEscape
where
doEscape c =
if isOk c
then Text.singleton c
else "&#" <> Text.pack (show (ord c)) <> ";"
isOk x = any (\check -> check x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` [' ', '.', '/'])]
formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Maybe FilePath -> Builder.Builder
formatResult (Result filename errors checks) filePathInReport = header <> xmlBody <> footer
where
xmlBody = Foldl.fold (Foldl.premap toXml Foldl.mconcat) issues
issues = checkstyleErrors <> checkstyleChecks
checkstyleErrors = fmap errorToCheckStyle errors
checkstyleChecks = fmap ruleToCheckStyle checks
isEmpty = null checks && null errors
name = if null filePathInReport then filename else getFilePath filePathInReport
header =
if isEmpty
then ""
else "<file " <> attr "name" (encode name) <> ">"
footer = if isEmpty then "" else "</file>"
printResults ::
(Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
f (Result s e) -> Maybe FilePath ->
IO ()
printResults results filePathInReport = do
B.putStr header
mapM_ put results
B.putStr footer
where
header = "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>"
footer = "</checkstyle>"
put result = Builder.hPutBuilder stdout (formatResult result filePathInReport)
getFilePath :: Maybe FilePath -> Text.Text
getFilePath Nothing = ""
getFilePath (Just filePath) = toText [filePath]
toText :: [FilePath] -> Text.Text
toText = foldMap Text.pack