aihc-cabal-syntax-2.0.0.0: src/Aihc/Cabal/Internal/Version.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Versions and version ranges. "Aihc.Cabal" exports the public part. The
-- parsers and the specification version helpers are for the other internal
-- modules.
module Aihc.Cabal.Internal.Version
( -- * Public
Version, versionNumbers, mkVersion, parseVersion, renderVersion
, VersionRange (..), anyVersion, noVersion, thisVersion, withinVersion, withinRange, intersectRanges
, unionRanges, simplifyVersionRange, parseVersionRange, renderVersionRange
-- * Internal
, Parser, specVersion, latestSpec, versionParser, versionDigits, rangeParser
) where
import Control.Applicative ((<|>))
import Control.Monad (when)
import Data.Char (isAlphaNum, isDigit)
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Text (Text)
import qualified Data.Text as T
import Data.Void (Void)
import Text.Megaparsec (Parsec, between, eof, errorBundlePretty, many, runParser, satisfy, sepBy1, some, takeWhile1P)
import Text.Megaparsec.Char (char, space, string)
-- | A version: one or more numbers that are not negative. Versions compare
-- as lists, so @1.2@ is before @1.2.0@.
newtype Version = Version (NonEmpty Integer) deriving (Eq, Ord, Show)
-- | A version range as a Cabal file gives it. The constructors are exported
-- so that the caller can examine, simplify, or show a range. Equality is
-- structural: a @^>=@ bound and its expanded intersection are different
-- values. Use 'withinRange' to test membership.
data VersionRange
= AnyVersion
-- ^ @-any@, or an absent range. The same set of versions as @>= 0@.
| Equal Version
-- ^ @== v@
| Later Version
-- ^ @> v@
| Earlier Version
-- ^ @< v@
| AtLeast Version
-- ^ @>= v@
| AtMost Version
-- ^ @<= v@
| MajorBound Version
-- ^ @^>= v@
| Both VersionRange VersionRange
-- ^ @a && b@
| EitherRange VersionRange VersionRange
-- ^ @a || b@
deriving (Eq, Show)
type Parser = Parsec Void Text
-- | The numbers of a version.
versionNumbers :: Version -> NonEmpty Integer
versionNumbers (Version ns) = ns
-- | Make a version from its numbers. The result is 'Nothing' for an empty
-- list and for a negative number.
mkVersion :: [Integer] -> Maybe Version
mkVersion ns = case NE.nonEmpty ns of
Just xs | all (>= 0) xs -> Just (Version xs)
_ -> Nothing
-- | A known Cabal specification version, for example @specVersion [2, 2]@.
specVersion :: [Integer] -> Version
specVersion = Version . NE.fromList
-- | The newest Cabal specification version that the parser knows.
latestSpec :: Version
latestSpec = specVersion [3, 14]
-- | A version number part: at most nine digits and no leading zero.
versionDigits :: Parser Integer
versionDigits = do
ds <- some (satisfy isDigit)
case ds of
"0" -> pure 0
'0' : _ -> fail "Version digit with leading zero"
_ | length ds > 9 -> fail "At most 9 numbers are allowed per version number part"
| otherwise -> pure (read ds)
-- | A version with optional tags, for example @1.2.3-rc1@. The tags are not kept.
versionParser :: Parser Version
versionParser = Version . NE.fromList <$> versionDigits `sepBy1` char '.' <* tags
tags :: Parser ()
tags = () <$ many (char '-' *> some (satisfy isAlphaNum))
parseWith :: Parser a -> Text -> Either Text a
parseWith p input = case runParser (space *> p <* space <* eof) "" input of
Left err -> Left (T.pack (errorBundlePretty err))
Right value -> Right value
-- | Parse a version such as @9.12.2@. Spaces around the version are
-- permitted. Tags such as @-rc1@ are accepted and not kept.
parseVersion :: Text -> Either Text Version
parseVersion = parseWith versionParser
-- | Show a version with dots, as in @9.12.2@.
renderVersion :: Version -> Text
renderVersion (Version ns) = T.intercalate "." (map (T.pack . show) (NE.toList ns))
-- | The range of all versions.
anyVersion :: VersionRange
anyVersion = AnyVersion
-- | The empty range @-none@, as @< 0@.
noVersion :: VersionRange
noVersion = Earlier (Version (0 :| []))
-- | The range @== v@.
thisVersion :: Version -> VersionRange
thisVersion = Equal
-- | The range @a && b@ or @a || b@. The functions do not simplify. Use
-- 'simplifyVersionRange' for that.
intersectRanges, unionRanges :: VersionRange -> VersionRange -> VersionRange
intersectRanges = Both
unionRanges = EitherRange
-- | Test if a version is in a range.
withinRange :: Version -> VersionRange -> Bool
withinRange v range = case range of
AnyVersion -> True
Equal w -> v == w
Later w -> v > w
Earlier w -> v < w
AtLeast w -> v >= w
AtMost w -> v <= w
MajorBound w -> v >= w && v < majorUpperBound w
Both a b -> withinRange v a && withinRange v b
EitherRange a b -> withinRange v a || withinRange v b
-- | Parse a version range as Cabal-syntax does for the given specification version.
-- Unparenthesized @&&@ and @||@ associate to the right.
rangeParser :: Version -> Parser VersionRange
rangeParser spec = expr
where
expr = do
space
t <- term
space
(EitherRange t <$> (string "||" *> space *> expr)) <|> pure t
term = do
f <- factor
space
(Both f <$> (string "&&" *> space *> term)) <|> pure f
factor = parens <|> prim
parens = between (char '(' *> space) (char ')' *> space) (expr <* space)
prim = do
op <- takeWhile1P (Just "operator") isOpChar
case op of
"-" -> AnyVersion <$ string "any" <|> (string "none" *> none)
"==" -> space *> (wildOrVersion <|> (versionSet >>= set Equal))
"^>=" -> space *> (major <|> (versionSet >>= set MajorBound))
_ -> do
space
(wild, v) <- versionOrWild
when wild (fail ("wild-card version after non-== operator: " ++ T.unpack op))
case op of
">=" -> pure (AtLeast v)
"<" -> pure (Earlier v)
"<=" -> pure (AtMost v)
">" -> pure (Later v)
_ -> fail ("Unknown version operator " ++ T.unpack op)
isOpChar c = c `elem` ("<=>^" :: String) || (c == '-' && spec < specVersion [3, 4])
none
| spec >= specVersion [1, 22] = pure noVersion
| otherwise = fail "-none version range used"
wildOrVersion = do
(wild, v) <- versionOrWild
pure (if wild then withinVersion v else Equal v)
major = do
(wild, v) <- versionOrWild
when wild (fail "wild-card version after ^>= operator")
if spec >= specVersion [2, 0] then pure (MajorBound v)
else fail "major bounded version syntax (caret, ^>=) used"
set constructor vs
| spec >= specVersion [3, 0] = pure (foldr1 EitherRange (fmap constructor vs))
| otherwise = fail "version set syntax used"
versionSet = do
_ <- char '{' *> space
v <- plain <* space
vs <- many (char ',' *> space *> plain <* space)
_ <- char '}'
pure (v :| vs)
plain = Version . NE.fromList <$> versionDigits `sepBy1` char '.'
versionOrWild = versionDigits >>= loop . pure
loop acc = (char '.' *> ((versionDigits >>= loop . (: acc)) <|> ((True, done acc) <$ char '*')))
<|> ((False, done acc) <$ tags)
done = Version . NE.fromList . reverse
-- | The range @== v.*@.
withinVersion :: Version -> VersionRange
withinVersion v = Both (AtLeast v) (Earlier (wildcardUpperBound v))
wildcardUpperBound :: Version -> Version
wildcardUpperBound (Version ns) = Version (NE.fromList (NE.init ns ++ [NE.last ns + 1]))
majorUpperBound :: Version -> Version
majorUpperBound (Version ns) = Version $ case ns of
x :| [] -> x :| [1]
x :| (y:_) -> x :| [y + 1]
-- | Parse a version range. The parser accepts the syntax of all Cabal
-- format versions: @-any@, @-none@, @^>=@, and version sets. Operators
-- without parentheses associate to the right, as in Cabal-syntax.
parseVersionRange :: Text -> Either Text VersionRange
parseVersionRange = parseWith (rangeParser (specVersion [3, 0]))
-- | Show a range in Cabal syntax, for example @>=1.2 && <1.3 || ==2.0@.
-- The text has parentheses only where the structure needs them. A @||@
-- operand of @&&@ gets parentheses. A left operand with the same operator
-- gets parentheses, so that 'parseVersionRange' gives the same value.
renderVersionRange :: VersionRange -> Text
renderVersionRange r = case r of
AnyVersion -> "-any"
Equal v -> "==" <> renderVersion v
Later v -> ">" <> renderVersion v
Earlier v -> "<" <> renderVersion v
AtLeast v -> ">=" <> renderVersion v
AtMost v -> "<=" <> renderVersion v
MajorBound v -> "^>=" <> renderVersion v
Both a b -> parens (isBoth a || isEither a) a <> " && " <> parens (isEither b) b
EitherRange a b -> parens (isEither a) a <> " || " <> renderVersionRange b
where
parens needed x
| needed = "(" <> renderVersionRange x <> ")"
| otherwise = renderVersionRange x
isBoth Both {} = True
isBoth _ = False
isEither EitherRange {} = True
isEither _ = False
-- | The same set of versions as a union of separate intervals in increasing
-- order. An empty range gives 'noVersion'. A range of all versions gives
-- 'anyVersion'.
--
-- An interval becomes @==v@, a bound, or two bounds with @&&@. The bounds
-- keep their operators, so @>1@ stays @>1@. A @^>=@ bound and a @==v.*@ range
-- become two bounds. The unions associate to the right, as the parser reads
-- them.
--
-- Version @0@ is the smallest version, so a lower bound @>=0@ has no effect.
-- No version is between @v@ and @v.0@, so @>1 && <1.0@ is empty and
-- @>1 && <=1.0@ is @==1.0@.
simplifyVersionRange :: VersionRange -> VersionRange
simplifyVersionRange range = case map fromInterval (intervals range) of
[] -> noVersion
parts -> foldr1 EitherRange parts
where
fromInterval (Interval low high)
| Just high' <- high, upperKey high' == successor (lowerKey low) = Equal (lowerKey low)
| otherwise = case (lowerPart, upperPart) of
(Nothing, Nothing) -> AnyVersion
(Just a, Nothing) -> a
(Nothing, Just b) -> b
(Just a, Just b) -> Both a b
where
lowerPart
| lowerKey low == zero = Nothing
| otherwise = Just (let Bound v inclusive = low in if inclusive then AtLeast v else Later v)
upperPart = fmap (\(Bound v inclusive) -> if inclusive then AtMost v else Earlier v) high
-- | A bound version, and whether the version itself is in the interval.
data Bound = Bound Version Bool
-- | An interval from a lower bound to an upper bound. 'Nothing' is no upper
-- bound.
data Interval = Interval Bound (Maybe Bound)
zero :: Version
zero = Version (0 :| [])
-- | The next version: no version is between @v@ and @v.0@.
successor :: Version -> Version
successor (Version ns) = Version (ns <> (0 :| []))
-- | The smallest version in the interval above a lower bound.
lowerKey :: Bound -> Version
lowerKey (Bound v inclusive) = if inclusive then v else successor v
-- | The smallest version above the interval below an upper bound.
upperKey :: Bound -> Version
upperKey (Bound v inclusive) = if inclusive then successor v else v
-- | Separate, nonempty intervals in increasing order.
intervals :: VersionRange -> [Interval]
intervals range = case range of
AnyVersion -> [Interval (Bound zero True) Nothing]
Equal v -> [Interval (Bound v True) (Just (Bound v True))]
Later v -> [Interval (Bound v False) Nothing]
Earlier v -> normalize [Interval (Bound zero True) (Just (Bound v False))]
AtLeast v -> [Interval (Bound v True) Nothing]
AtMost v -> [Interval (Bound zero True) (Just (Bound v True))]
MajorBound v -> normalize [Interval (Bound v True) (Just (Bound (majorUpperBound v) False))]
EitherRange a b -> normalize (intervals a ++ intervals b)
Both a b -> normalize [intersect x y | x <- intervals a, y <- intervals b]
where
intersect (Interval low high) (Interval low' high') =
Interval (if lowerKey low' > lowerKey low then low' else low) (minUpper high high')
minUpper Nothing b = b
minUpper a Nothing = a
minUpper (Just a) (Just b) = Just (if upperKey b < upperKey a then b else a)
normalize :: [Interval] -> [Interval]
normalize = merge . sortOn (\(Interval low _) -> lowerKey low) . filter nonEmpty
where
nonEmpty (Interval _ Nothing) = True
nonEmpty (Interval low (Just high)) = lowerKey low < upperKey high
merge (Interval low high : Interval low' high' : rest)
| touches high low' = merge (Interval low (maxUpper high high') : rest)
merge (x : rest) = x : merge rest
merge [] = []
touches Nothing _ = True
touches (Just high) low' = lowerKey low' <= upperKey high
maxUpper Nothing _ = Nothing
maxUpper _ Nothing = Nothing
maxUpper (Just a) (Just b) = Just (if upperKey b > upperKey a then b else a)