{-# LANGUAGE NoImplicitPrelude #-}
{-|
Module : Yarn.Lock.Helpers
Description : Helpers for modifying Lockfiles
Maintainer : Profpatsch
Stability : experimental

Freshly parsed 'Lockfile's are often not directly usable
e.g. because they still can contain cycles. This module
provides helpers for modifying them.
-}
module Yarn.Lock.Helpers
( decycle
) where

import Protolude
import qualified Data.List as L
import GHC.Stack (HasCallStack)

import qualified Data.MultiKeyedMap as MKM

import Yarn.Lock.Types


-- | Takes a 'Lockfile' and removes dependency cycles.
--
-- Node packages often contain those and the yarn lockfile
-- does not yet eliminate them, which may lead to infinite
-- recursions.
--
-- Invariant: Every dependency entry in each package in the
-- 'Lockfile' *must* point to an existing key, otherwise
-- the function crashes.
decycle :: HasCallStack => Lockfile -> Lockfile
decycle :: Lockfile -> Lockfile
decycle Lockfile
lf = [PackageKey] -> Lockfile -> [PackageKey] -> Lockfile
forall (t :: * -> *).
Foldable t =>
[PackageKey] -> Lockfile -> t PackageKey -> Lockfile
goFold [] Lockfile
lf (Lockfile -> [PackageKey]
forall k v. Ord k => MKMap k v -> [k]
MKM.keys Lockfile
lf)
  -- TODO: probably rewrite with State
  where
    -- | fold over all package keys, passing the lockfile
    goFold :: [PackageKey] -> Lockfile -> t PackageKey -> Lockfile
goFold [PackageKey]
seen Lockfile
lf' t PackageKey
pkeys =
      (Lockfile -> PackageKey -> Lockfile)
-> Lockfile -> t PackageKey -> Lockfile
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' (\Lockfile
lf'' PackageKey
pkey -> [PackageKey] -> Lockfile -> Lockfile
go (PackageKey
pkeyPackageKey -> [PackageKey] -> [PackageKey]
forall a. a -> [a] -> [a]
:[PackageKey]
seen) Lockfile
lf'') Lockfile
lf' t PackageKey
pkeys
    -- | We get a stack of already seen packages
    -- and filter out any dependencies we already saw.
    go :: [PackageKey] -> Lockfile -> Lockfile
    go :: [PackageKey] -> Lockfile -> Lockfile
go seen :: [PackageKey]
seen@(PackageKey
we:[PackageKey]
_) Lockfile
lf' =
      let ourPkg :: Package
ourPkg = Lockfile
lf' Lockfile -> PackageKey -> Package
forall k v. (HasCallStack, Ord k) => MKMap k v -> k -> v
MKM.! PackageKey
we
          -- old deps minus the already seen ones
          -- TODO make handling of opt pkgs less of a duplication
          newDeps :: [PackageKey]
newDeps = Package -> [PackageKey]
dependencies Package
ourPkg [PackageKey] -> [PackageKey] -> [PackageKey]
forall a. Eq a => [a] -> [a] -> [a]
L.\\ [PackageKey]
seen
          newOptDeps :: [PackageKey]
newOptDeps = Package -> [PackageKey]
optionalDependencies Package
ourPkg [PackageKey] -> [PackageKey] -> [PackageKey]
forall a. Eq a => [a] -> [a] -> [a]
L.\\ [PackageKey]
seen
          -- we update the pkg with the cleaned dependencies
          lf'' :: Lockfile
lf'' = PackageKey -> Package -> Lockfile -> Lockfile
forall k v. Ord k => k -> v -> MKMap k v -> MKMap k v
MKM.insert PackageKey
we (Package
ourPkg { dependencies :: [PackageKey]
dependencies = [PackageKey]
newDeps
                               , optionalDependencies :: [PackageKey]
optionalDependencies = [PackageKey]
newOptDeps }) Lockfile
lf'
      -- finally we do the same for all remaining deps
      in [PackageKey] -> Lockfile -> [PackageKey] -> Lockfile
forall (t :: * -> *).
Foldable t =>
[PackageKey] -> Lockfile -> t PackageKey -> Lockfile
goFold [PackageKey]
seen Lockfile
lf'' ([PackageKey] -> Lockfile) -> [PackageKey] -> Lockfile
forall a b. (a -> b) -> a -> b
$ [PackageKey]
newDeps [PackageKey] -> [PackageKey] -> [PackageKey]
forall a. [a] -> [a] -> [a]
++ [PackageKey]
newOptDeps
    go [] Lockfile
_ = Text -> Lockfile
forall a. HasCallStack => Text -> a
panic (Text -> Lockfile) -> Text -> Lockfile
forall a b. (a -> b) -> a -> b
$ [Char] -> Text
forall a b. ConvertText a b => a -> b
toS [Char]
"should not happen!"