packages feed

hadolint-2.15.0: src/Hadolint/Formatter/JUnit.hs

module Hadolint.Formatter.JUnit ( hWrite ) where


import qualified Data.ByteString.Lazy.Char8 as B
import Data.Foldable
import Data.Maybe
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified Data.Time as Time
import Hadolint.Formatter.Format
  ( Result (..),
    errorBundlePretty,
    errorPosition,
    severityText,
  )
import Hadolint.Rule (CheckFailure (..), RuleCode (..))
import Hadolint.Meta (getShortVersion)
import System.IO (Handle)
import Text.Megaparsec (TraversableStream)
import Text.Megaparsec.Error
  ( ParseErrorBundle,
    ShowErrorComponent,
  )
import Text.Megaparsec.Pos (sourceColumn, sourceLine, unPos)
import Text.Megaparsec.Stream (VisualStream)
import qualified Text.XML as XML


providerName :: Text.Text
providerName = "Hadolint " <> Text.pack getShortVersion

providerID :: Text.Text
providerID = "hadolint"


hWrite ::
  (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
  Handle -> f (Result s e) -> Maybe FilePath -> IO ()
hWrite handle results maybeFilepath = do
  time <- Time.getCurrentTime
  B.hPutStr handle $ XML.renderLBS settings $ document time

  where
    settings = XML.def

    document :: Time.UTCTime -> XML.Document
    document t =
      XML.Document
        { documentPrologue = XML.Prologue [] Nothing [],
          documentRoot = root t,
          documentEpilogue = []

        }

    root :: Time.UTCTime -> XML.Element
    root t =
      XML.Element
        { elementName = "testsuites",
          elementAttributes =
            Map.fromList
              [ ("id", runID t),
                ("name", runName t),
                ("time", "0.001")
              ],
          elementNodes = fmap ( `renderResult` maybeFilepath ) ( toList results )
        }

    runID :: Time.UTCTime -> Text.Text
    runID t =
      Text.pack $ Time.formatTime Time.defaultTimeLocale "%Y%m%d_%H%M%S" t

    runName :: Time.UTCTime -> Text.Text
    runName t =
      Text.pack $ Time.formatTime Time.defaultTimeLocale "Hadolint run at %Y-%m-%d %H:%M:%S" t

renderResult ::
  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
  Result s e -> Maybe FilePath -> XML.Node
renderResult (Result filename errors checks) maybeFilepath =
  XML.NodeElement XML.Element
    { elementName = "testsuite",
      elementAttributes =
        Map.fromList
          [ ("id", providerID),
            ("name", providerName),
            ("time", "0.001"),
            ("failures", Text.pack $ show $ length findings),
            ("errors", Text.pack $ show parseErrors)
          ],
      elementNodes = findings
    }
  where
    findings = toList ( errorNodes <> checkNodes )
    parseErrors = length errorNodes
    errorNodes = fmap ( `errorToNode` file ) errors
    checkNodes = fmap ( `checkToNode` file ) checks
    file = if null maybeFilepath then filename else Text.pack $ fromMaybe "" maybeFilepath

errorToNode ::
  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
  ParseErrorBundle s e -> Text.Text -> XML.Node
errorToNode err filename =
  XML.NodeElement XML.Element
    { elementName = "testcase",
      elementAttributes =
        Map.fromList
          [ ("id", providerID <> ".error"),
            ("name", "DL1000"),
            ("time", "0.001")
          ],
      elementNodes =
        [ XML.NodeElement XML.Element
            { elementName = "failure",
              elementAttributes =
                Map.fromList
                  [ ("type", "error"),
                    ("message", "parse error at line " <> line <> ", column " <> column)
                  ],
              elementNodes =
                [ XML.NodeContent $
                    Text.unlines
                      [ "Parse Error in " <> filename,
                        "Line: " <> line,
                        "Column: " <> column,
                        "Parse Error: " <> Text.pack ( errorBundlePretty err )
                      ]
                ]
            }
        ]
    }
  where
    line = Text.pack ( show $ unPos $ sourceLine $ errorPosition err )
    column = Text.pack ( show $ unPos $ sourceColumn $ errorPosition err )

checkToNode :: CheckFailure -> Text.Text -> XML.Node
checkToNode CheckFailure {..} filename =
  XML.NodeElement XML.Element
    { elementName = "testcase",
      elementAttributes =
        Map.fromList
          [ ("id", providerID <> ".rule." <> unRuleCode code),
            ("time", "0.001")
          ],
      elementNodes =
        [ XML.NodeElement XML.Element
            { elementName = "failure",
              elementAttributes =
                Map.fromList
                  [ ("type", severityText severity),
                    ("message", message),
                    ("id", unRuleCode code)
                  ],
              elementNodes =
                [ XML.NodeContent $
                    Text.unlines
                      [ "File: " <> filename,
                        "Line: " <> Text.pack ( show line ),
                        "Category: Hadolint - Dockerfile Static Analysis",
                        severityText severity <> ": " <> message
                      ]
                ]
            }
        ]
    }