packages feed

arch-hs-0.15: rdepcheck/RDepCheck.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}

module RDepCheck (FailureCounts (..), check, checkReverseDep, checkReverseDepRevisions, printRdepcheckResult) where

import Control.Monad (forM)
import Data.List (partition)
import qualified Data.Map.Strict as Map
import Distribution.ArchHs.Exception
import Distribution.ArchHs.ExtraDB (versionInExtra)
import Distribution.ArchHs.Hackage (RawHackageDB)
import Distribution.ArchHs.Internal.Prelude
import Distribution.ArchHs.PP
import Distribution.ArchHs.RDepCheck
import Distribution.ArchHs.Types
import Distribution.Version (asVersionIntervals)
import System.Exit (exitFailure)

data FailureCounts = FailureCounts
  { newFailures :: Int,
    oldFailures :: Int
  }
  deriving stock (Eq, Show)

check ::
  Members
    [ ExtraEnv,
      RawHackageEnv,
      KnownGHCVersion,
      FlagAssignmentsEnv,
      Trace,
      DependencyRecord,
      WithMyErr,
      Embed IO
    ]
    r =>
  RawHackageDB ->
  Maybe Version ->
  PackageName ->
  Sem r FailureCounts
check revision0 mVersion target = do
  latest <- indexResults <$> reverseDependencyRangesWithSkips target
  original <- indexResults <$> local @RawHackageDB (const revision0) (reverseDependencyRangesWithSkips target)
  versions <- forM mVersion $ \candidate -> do
    rawVersion <- versionInExtra target
    case simpleParsec rawVersion of
      Just current -> pure (current, candidate)
      Nothing -> throw $ VersionNoParse rawVersion
  failures <- forM (Map.toList latest) $ \(name, latestResult) -> do
    let originalResult = Map.findWithDefault (Left $ PkgNotFound name) name original
        (doc, counts) = checkReverseDepRevisions versions name latestResult originalResult
    embed $ putDoc $ doc <> line
    pure counts
  pure $ FailureCounts (sum $ newFailures <$> failures) (sum $ oldFailures <$> failures)

indexResults :: ([ReverseDep], [SkippedReverseDep]) -> Map.Map ArchLinuxName (Either MyException ReverseDep)
indexResults (checked, skipped) =
  Map.fromList $
    [(reverseDepName dep, Right dep) | dep <- checked]
      <> [(skippedReverseDepName dep, Left $ skippedReverseDepError dep) | dep <- skipped]

checkReverseDep :: Maybe (Version, Version) -> ReverseDep -> (Doc AnsiStyle, FailureCounts)
checkReverseDep versions ReverseDep {..} =
  let (docs, counts) = checkRanges versions reverseDepRanges
   in (vsep $ reverseDepHeader reverseDepName : docs, counts)

checkReverseDepRevisions ::
  Maybe (Version, Version) ->
  ArchLinuxName ->
  Either MyException ReverseDep ->
  Either MyException ReverseDep ->
  (Doc AnsiStyle, FailureCounts)
checkReverseDepRevisions versions name latest original
  | sameResult latest original =
      case latest of
        Right dep -> checkReverseDep versions dep
        Left err -> (annYellow $ "Skip" <+> pretty (unArchLinuxName name) <> colon <+> viaShow err, FailureCounts 0 0)
  | otherwise =
      ( vsep $
          reverseDepHeader name
            : revisionDocs annCyan "latest revision" latest
              <> revisionDocs annBlue "revision 0" original,
        snd $ resultDetails latest
      )
  where
    sameResult (Right a) (Right b) =
      [(src, asVersionIntervals range) | (src, range) <- reverseDepRanges a]
        == [(src, asVersionIntervals range) | (src, range) <- reverseDepRanges b]
    sameResult (Left a) (Left b) = show a == show b
    sameResult _ _ = False

    resultDetails (Right dep) = checkRanges versions $ reverseDepRanges dep
    resultDetails (Left err) = ([indent 2 $ annYellow $ "unchecked:" <+> viaShow err], FailureCounts 0 0)

    revisionDocs style label result =
      let (docs, counts) = resultDetails result
          status = case (versions, result) of
            (Just _, Right _) -> space <> parens (prettyFailureCounts counts)
            _ -> mempty
       in indent 2 (style $ annBold label <> status <> colon) : fmap (indent 2) docs

reverseDepHeader :: ArchLinuxName -> Doc AnsiStyle
reverseDepHeader name = annMagneta $ "Reverse dependency" <> colon <+> annBold (pretty $ unArchLinuxName name)

checkRanges :: Maybe (Version, Version) -> [(DepSrc, VersionRange)] -> ([Doc AnsiStyle], FailureCounts)
checkRanges versions ranges =
  ( rangeDocs versions ranges <> errors,
    FailureCounts (length newRanges) (length oldRanges)
  )
  where
    (newRanges, oldRanges) =
      case versions of
        Nothing -> ([], [])
        Just (current, candidate) ->
          partition (withinRange current . snd) $ versionFailures (Just candidate) ranges
    errors =
      case versions of
        Nothing -> []
        Just (_, candidate) ->
          versionErrors annRed "rdep:" candidate newRanges
            <> versionErrors annYellow "rdep-old:" candidate oldRanges

printRdepcheckResult :: IO (Either MyException FailureCounts) -> IO ()
printRdepcheckResult io = do
  result <- io
  case result of
    Left err -> do
      printError $ "Runtime Exception" <> colon <+> viaShow err
      exitFailure
    Right counts@FailureCounts {..}
      | newFailures > 0 -> do
          printError $ "Reverse dependency range check(s) failed:" <+> prettyFailureCounts counts
          exitFailure
      | oldFailures > 0 ->
          printWarn $ "Existing reverse dependency range failure(s):" <+> prettyFailureCounts counts
      | otherwise -> printSuccess "Success!"

prettyFailureCounts :: FailureCounts -> Doc AnsiStyle
prettyFailureCounts FailureCounts {..} =
  (if newFailures == 0 then annGreen else annRed) ("rdep=" <> pretty newFailures)
    <> comma
      <+> (if oldFailures == 0 then annGreen else annYellow) ("rdep-old=" <> pretty oldFailures)

rangeDocs :: Maybe (Version, Version) -> [(DepSrc, VersionRange)] -> [Doc AnsiStyle]
rangeDocs versions result =
  [ indent 2 $ pretty s <> colon <+> rangeColor r (viaPretty r)
    | (s, r) <- result
  ]
  where
    rangeColor range =
      case versions of
        Nothing -> annBlue
        Just (current, candidate)
          | withinRange candidate range -> annGreen
          | withinRange current range -> annRed
          | otherwise -> annYellow

versionErrors :: (Doc AnsiStyle -> Doc AnsiStyle) -> Doc AnsiStyle -> Version -> [(DepSrc, VersionRange)] -> [Doc AnsiStyle]
versionErrors style label version result =
  [ indent 2 $ style $
      label
        <+> annBold (viaPretty version)
        <+> "is outside"
        <+> pretty src
        <+> "range"
        <+> parens (viaPretty range)
    | (src, range) <- result
  ]