packages feed

update-nix-fetchgit-0.2.8: src/Update/Nix/FetchGit.hs

{-# LANGUAGE QuasiQuotes #-}

module Update.Nix.FetchGit
  ( processFile
  , processText
  , updatesFromText
  ) where

import           Control.Monad                  ( when )
import           Control.Monad.Reader           ( MonadReader(ask) )
import           Control.Monad.Validate         ( MonadValidate(tolerate) )
import           Data.Fix
import           Data.Foldable
import           Data.Functor
import           Data.Maybe
import           Data.Text                      ( Text
                                                , pack
                                                )
import qualified Data.Text                     as T
import qualified Data.Text.IO
import           Data.Time                      ( Day )
import qualified Data.Vector                   as V
import           Nix.Comments
import           Nix.Expr
import           Nix.Match.Typed
import           System.Exit
import           Text.Regex.TDFA
import           Update.Nix.FetchGit.Types
import           Update.Nix.FetchGit.Utils
import           Update.Nix.Updater
import           Update.Span

--------------------------------------------------------------------------------
-- Tying it all together
--------------------------------------------------------------------------------

-- | Provided FilePath, update Nix file in-place
processFile :: Env -> FilePath -> IO ()
processFile env filename = do
  t  <- Data.Text.IO.readFile filename
  t' <- processText env t
  -- If updates are needed, write to the file.
  when (t /= t') $ Data.Text.IO.writeFile filename t'

processText :: Env -> Text -> IO Text
processText env t = do
  (es, t') <- runM env (updatesFromText t <&> (`updateSpans` t))
  traverse_ (sayLog env Normal . formatWarning) es
  maybe exitFailure pure $ case dryness env of
    Wet -> t'
    Dry -> Just t

-- | Given the path to a Nix file, returns the SpanUpdates
-- all the parts of the file we want to update.
updatesFromText :: Text -> M [SpanUpdate]
updatesFromText t = do
  let nixLines = V.fromList (T.lines t)
      getComment sourceLines =
        annotation . getCompose . unFix . annotateWithComments sourceLines
  tree <- do
    expr <- fromEither $ ourParseNixText t
    findUpdates (getComment nixLines) expr
  us <- evalUpdates =<< filterUpdates tree
  case us of
    []  -> logVerbose "Made no updates"
    [_] -> logVerbose "Made 1 update"
    _   -> logVerbose ("Made " <> T.pack (show (length us)) <> " updates")
  pure us

----------------------------------------------------------------
-- Finding updates
----------------------------------------------------------------

findUpdates :: (NExprLoc -> Maybe Comment) -> NExprLoc -> M FetchTree
findUpdates getComment e = do
  Env {..} <- ask
  -- First of all, if this expression doesn't enclose the requested position,
  -- return an empty tree
  -- Then check against all the updaters, if they match we have a leaf
  if not (null updateLocations || any (containsPosition e) updateLocations)
    then pure $ Node Nothing []
    else
      let
        updaters     = ($ e) <$> fetchers onlyCommented getComment
        bindingTrees = \case
          NamedVar p e' _ | Just t <- pathText p ->
            (: []) . (Just t, ) <$> findUpdates getComment e'
          b ->
            traverse (fmap (Nothing, ) . findUpdates getComment) . toList $ b
      in
        case asum updaters of
          Just u  -> UpdaterNode <$> u
          Nothing -> case e of
            [matchNixLoc|{ _version = ^version; }|] | NSet_ _ _ bs <- unFix e ->
              Node version . concat <$> traverse bindingTrees bs
            [matchNixLoc|let _version = ^version; in ^x|]
              | NLet_ _ bs _ <- unFix e -> do
                bs' <- concat <$> traverse bindingTrees bs
                x'  <- findUpdates getComment x
                pure $ Node version ((Nothing, x') : bs')
            _ -> Node Nothing <$> traverse
              (fmap (Nothing, ) . findUpdates getComment)
              (toList (unFix e))

filterUpdates :: FetchTree -> M FetchTree
filterUpdates t = do
  Env {..} <- ask
  let matches s = any (`match` s) attrPatterns
  -- If we're in a branch, include any bindings which match unconditionally,
  -- otherwise recurse
  -- If we reach a leaf, return empty because it hasn't been included by a
  -- binding yet
  let go = \case
        Node v cs     -> Node
          v
          [ (n, c')
          | (n, c) <- cs
          , let c' = if maybe False matches n then c else go c
          ]
        UpdaterNode _ -> Node Nothing []
  -- If there are no patterns, don't do any filtering
  pure $ if null attrPatterns then t else go t


evalUpdates :: FetchTree -> M [SpanUpdate]
evalUpdates = fmap snd . go
 where
  go :: FetchTree -> M (Maybe Day, [SpanUpdate])
  go = \case
    UpdaterNode (Updater u) -> u
    Node versionExpr cs     -> do
      -- Run over all children
      (ds, ss) <- unzip . catMaybes <$> traverse (tolerate . go . snd) cs
      -- Update version string with the maximum of versions in the children
      let latestDate = maximumMay (catMaybes ds)
      pure
        ( latestDate
        , [ SpanUpdate (exprSpan v)
                       (quoteString . ("unstable-" <>) . pack . show $ d)
          | Just d <- pure latestDate
          , Just v <- pure versionExpr
          ]
        <> concat ss
        )

----------------------------------------------------------------
-- Utils
----------------------------------------------------------------

maximumMay :: Ord a => [a] -> Maybe a
maximumMay = \case
  [] -> Nothing
  xs -> Just (maximum xs)