packages feed

aihc-parser-1.0.0.2: common/StackageProgress/Summary.hs

{-# LANGUAGE BangPatterns #-}

module StackageProgress.Summary
  ( FailedPackage (..),
    PackageResult (..),
    PackageSpec (..),
    PromptCandidate (..),
    RunSummary,
    SummaryOptions (..),
    addPackageResults,
    emptySummary,
    finalizeSummary,
    forceString,
    formatPackage,
    packageParserFailed,
    promptCandidateFromResult,
    renderPrompt,
    selectPromptCandidate,
    summaryFailedPackages,
    summaryGhcErrors,
    summarySuccessGhcN,
    summarySuccessHseN,
    summarySuccessOursN,
    summarySucceededPackages,
  )
where

import Aihc.Hackage.Types (PackageSpec (..), formatPackage)
import Data.Char (isSpace)
import Data.List qualified as List

data PackageResult = PackageResult
  { package :: !PackageSpec,
    packageOursOk :: !Bool,
    packageHseOk :: !Bool,
    packageGhcOk :: !Bool,
    packageReason :: !String,
    packageGhcError :: !(Maybe String),
    packageSourceSize :: !Integer,
    packageFileErrors :: ![(String, String)] -- [(filePath, errorMessage)]
  }

data FailedPackage = FailedPackage
  { failedPackageName :: !String,
    failedPackageSourceSize :: !Integer,
    failedPackageErrors :: ![(String, String)] -- [(filePath, errorMessage)]
  }
  deriving (Eq, Show)

data PromptCandidate = PromptCandidate
  { promptPackageName :: !String,
    promptErrorMessage :: !String
  }
  deriving (Eq, Show)

data SummaryOptions = SummaryOptions
  { summaryKeepSucceeded :: !Bool,
    summaryKeepFailedPackages :: !Bool,
    summaryGhcErrorLimit :: !Int
  }

data RunSummary = RunSummary
  { summarySuccessOursN :: !Int,
    summarySuccessHseN :: !Int,
    summarySuccessGhcN :: !Int,
    summarySucceededPackagesAcc :: ![String],
    summaryFailedPackagesAcc :: ![FailedPackage],
    summaryGhcErrorsAcc :: ![(String, String)],
    summaryGhcErrorsStored :: !Int
  }

emptySummary :: RunSummary
emptySummary =
  RunSummary
    { summarySuccessOursN = 0,
      summarySuccessHseN = 0,
      summarySuccessGhcN = 0,
      summarySucceededPackagesAcc = [],
      summaryFailedPackagesAcc = [],
      summaryGhcErrorsAcc = [],
      summaryGhcErrorsStored = 0
    }

addPackageResults :: SummaryOptions -> [PackageResult] -> RunSummary -> RunSummary
addPackageResults opts results summary0 = List.foldl' (addPackageResult opts) summary0 results
  where
    addPackageResult :: SummaryOptions -> RunSummary -> PackageResult -> RunSummary
    addPackageResult summaryOpts summary result =
      let !oursN = summarySuccessOursN summary + boolToInt (packageOursOk result)
          !hseN = summarySuccessHseN summary + boolToInt (packageHseOk result)
          !ghcN = summarySuccessGhcN summary + boolToInt (packageGhcOk result)
          !pkgLabel = forceString (formatPackage (package result))
          succeededRev =
            if summaryKeepSucceeded summaryOpts && packageOursOk result
              then pkgLabel : summarySucceededPackagesAcc summary
              else summarySucceededPackagesAcc summary
          failedRev =
            if summaryKeepFailedPackages summaryOpts && packageParserFailed result
              then FailedPackage pkgLabel (packageSourceSize result) (packageFileErrors result) : summaryFailedPackagesAcc summary
              else summaryFailedPackagesAcc summary
          (!ghcStored, ghcErrorsRev) = addGhcErrorIfNeeded summaryOpts summary result pkgLabel
       in RunSummary
            { summarySuccessOursN = oursN,
              summarySuccessHseN = hseN,
              summarySuccessGhcN = ghcN,
              summarySucceededPackagesAcc = succeededRev,
              summaryFailedPackagesAcc = failedRev,
              summaryGhcErrorsAcc = ghcErrorsRev,
              summaryGhcErrorsStored = ghcStored
            }

    addGhcErrorIfNeeded :: SummaryOptions -> RunSummary -> PackageResult -> String -> (Int, [(String, String)])
    addGhcErrorIfNeeded summaryOpts summary result pkgLabel
      | packageGhcOk result = (summaryGhcErrorsStored summary, summaryGhcErrorsAcc summary)
      | summaryGhcErrorsStored summary >= summaryGhcErrorLimit summaryOpts = (summaryGhcErrorsStored summary, summaryGhcErrorsAcc summary)
      | otherwise =
          let !message = forceString (ghcFailureMessage result)
           in (summaryGhcErrorsStored summary + 1, (pkgLabel, message) : summaryGhcErrorsAcc summary)

finalizeSummary :: RunSummary -> RunSummary
finalizeSummary summary =
  summary
    { summarySucceededPackagesAcc = reverse (summarySucceededPackagesAcc summary),
      summaryFailedPackagesAcc = reverse (summaryFailedPackagesAcc summary),
      summaryGhcErrorsAcc = reverse (summaryGhcErrorsAcc summary)
    }

summarySucceededPackages :: RunSummary -> [String]
summarySucceededPackages = summarySucceededPackagesAcc

summaryFailedPackages :: RunSummary -> [FailedPackage]
summaryFailedPackages = summaryFailedPackagesAcc

summaryGhcErrors :: RunSummary -> [(String, String)]
summaryGhcErrors = summaryGhcErrorsAcc

packageParserFailed :: PackageResult -> Bool
packageParserFailed result = not (packageOursOk result) && packageSourceSize result > 0

promptCandidateFromResult :: PackageResult -> Maybe PromptCandidate
promptCandidateFromResult result
  | packageOursOk result = Nothing
  | otherwise =
      Just
        PromptCandidate
          { promptPackageName = pkgName (package result),
            promptErrorMessage = packageReason result
          }

renderPrompt :: String -> PromptCandidate -> String
renderPrompt template candidate =
  replaceAll "{{ERROR_MESSAGES}}" (promptErrorMessage candidate) $ replaceAll "{{PACKAGE_NAME}}" (promptPackageName candidate) template

selectPromptCandidate :: Integer -> [PromptCandidate] -> Maybe PromptCandidate
selectPromptCandidate _ [] = Nothing
selectPromptCandidate seed candidates =
  let n = length candidates
      idx = fromInteger (seed `mod` toInteger n)
   in Just (candidates !! idx)

ghcFailureMessage :: PackageResult -> String
ghcFailureMessage result =
  case packageGhcError result of
    Just err -> forceString err
    Nothing ->
      let reason = trim (packageReason result)
       in if null reason
            then "GHC check failed without diagnostic details"
            else "No direct GHC diagnostic; package failed before/around GHC check: " ++ forceString reason

trim :: String -> String
trim = List.dropWhileEnd isSpace . dropWhile isSpace

boolToInt :: Bool -> Int
boolToInt True = 1
boolToInt False = 0

forceString :: String -> String
forceString value = length value `seq` value

replaceAll :: String -> String -> String -> String
replaceAll needle replacement = go
  where
    go haystack
      | null needle = haystack
      | otherwise =
          case List.stripPrefix needle haystack of
            Just rest -> replacement ++ go rest
            Nothing ->
              case haystack of
                [] -> []
                x : xs -> x : go xs