packages feed

update-nix-fetchgit-0.2.11: 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; }|]
                | NSetAnnF _ _ bs <- unFix e ->
                    Node version . concat <$> traverse bindingTrees bs
              [matchNixLoc|let _version = ^version; in ^x|]
                | NLetAnnF _ 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)