packages feed

elm-package-0.2: src/Diff/Compare.hs

module Diff.Compare where

import Control.Applicative ((<$>), (<*>))
import Control.Monad (zipWithM)
import Data.Function (on)
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Set as Set

import qualified Catalog
import qualified Elm.Compiler.Module as Module
import qualified Elm.Compiler.Type as Type
import qualified Elm.Docs as Docs
import qualified Elm.Package.Name as N
import qualified Elm.Package.Version as V
import qualified Manager


computeChanges
    :: [Docs.Documentation]
    -> N.Name
    -> V.Version
    -> Manager.Manager PackageChanges
computeChanges newDocs name version =
    do  oldDocs <- Catalog.documentation name version
        return (diffPackages oldDocs newDocs)


-- CHANGE MAGNITUDE

data Magnitude
    = PATCH
    | MINOR
    | MAJOR
    deriving (Eq, Ord, Show)


bumpBy :: PackageChanges -> V.Version -> V.Version
bumpBy changes version =
    case packageChangeMagnitude changes of
      PATCH -> V.bumpPatch version
      MINOR -> V.bumpMinor version
      MAJOR -> V.bumpMajor version


packageChangeMagnitude :: PackageChanges -> Magnitude
packageChangeMagnitude pkgChanges =
    maximum (added : removed : map moduleChangeMagnitude moduleChanges)
  where
    moduleChanges =
        Map.elems (modulesChanged pkgChanges)

    removed =
        if null (modulesRemoved pkgChanges)
            then PATCH
            else MAJOR

    added =
        if null (modulesAdded pkgChanges)
            then PATCH
            else MINOR

moduleChangeMagnitude :: ModuleChanges -> Magnitude
moduleChangeMagnitude moduleChanges =
    maximum
        [ changeMagnitude (adtChanges moduleChanges)
        , changeMagnitude (aliasChanges moduleChanges)
        , changeMagnitude (valueChanges moduleChanges)
        ]

changeMagnitude :: Changes k v -> Magnitude
changeMagnitude (Changes added changed removed)
    | Map.size removed > 0 = MAJOR
    | Map.size changed > 0 = MAJOR
    | Map.size added   > 0 = MINOR
    | otherwise            = PATCH


-- DETECT CHANGES

data PackageChanges = PackageChanges
    { modulesAdded :: [String]
    , modulesChanged :: Map.Map String ModuleChanges
    , modulesRemoved :: [String]
    }

data ModuleChanges = ModuleChanges
    { adtChanges :: Changes String ([String], Map.Map String [Type.Type])
    , aliasChanges :: Changes String ([String], Type.Type)
    , valueChanges :: Changes String Type.Type
    }

data Changes k v = Changes
    { added :: Map.Map k v
    , changed :: Map.Map k (v,v)
    , removed :: Map.Map k v
    }


diffPackages :: [Docs.Documentation] -> [Docs.Documentation] -> PackageChanges
diffPackages oldDocs newDocs =
    PackageChanges
        (Map.keys added)
        (filterOutPatches (Map.map (uncurry diffModule) changed))
        (Map.keys removed)
  where
    filterOutPatches chngs =
        Map.filter (\chng -> moduleChangeMagnitude chng /= PATCH) chngs

    (Changes added changed removed) =
        getChanges
            (\_ _ -> False)
            (docsToModules oldDocs)
            (docsToModules newDocs)


data Module = Module
    { adts :: Map.Map String ([String], Map.Map String [Type.Type])
    , aliases :: Map.Map String ([String], Type.Type)
    , values :: Map.Map String Type.Type
    }


docsToModules :: [Docs.Documentation] -> Map.Map String Module
docsToModules docs =
    Map.fromList (map docToModule docs)


docToModule :: Docs.Documentation -> (String, Module)
docToModule (Docs.Documentation name _ aliases' unions' values') =
    (,) (Module.nameToString name) $ Module
    { adts =
        Map.fromList $ flip map unions' $ \union ->
            ( Docs.unionName union
            , (Docs.unionArgs union, Map.fromList (Docs.unionCases union))
            )

    , aliases =
        Map.fromList $ flip map aliases' $ \alias ->
            (Docs.aliasName alias, (Docs.aliasArgs alias, Docs.aliasType alias))

    , values =
        Map.fromList $ flip map values' $ \value ->
            (Docs.valueName value, Docs.valueType value)
    }


diffModule :: Module -> Module -> ModuleChanges
diffModule (Module adts aliases values) (Module adts' aliases' values') =
    ModuleChanges
        (getChanges isEquivalentAdt adts adts')
        (getChanges isEquivalentType aliases aliases')
        (getChanges (\t t' -> isEquivalentType ([],t) ([],t')) values values')


getChanges :: (Ord k) => (v -> v -> Bool) -> Map.Map k v -> Map.Map k v -> Changes k v
getChanges isEquivalent old new =
    Changes
    { added =
        Map.difference new old
    , changed =
        Map.filter
            (not . uncurry isEquivalent)
            (Map.intersectionWith (,) old new)
    , removed =
        Map.difference old new
    }


isEquivalentAdt
    :: ([String], Map.Map String [Type.Type])
    -> ([String], Map.Map String [Type.Type])
    -> Bool
isEquivalentAdt (oldVars, oldCtors) (newVars, newCtors) =
    Map.size oldCtors == Map.size newCtors
    && and (zipWith (==) (Map.keys oldCtors) (Map.keys newCtors))
    && and (Map.elems (Map.intersectionWith equiv oldCtors newCtors))
  where
    equiv :: [Type.Type] -> [Type.Type] -> Bool
    equiv oldTypes newTypes =
        let allEquivalent =
                zipWith
                  isEquivalentType
                  (map ((,) oldVars) oldTypes)
                  (map ((,) newVars) newTypes)
        in
            length oldTypes == length newTypes
            && and allEquivalent


isEquivalentType :: ([String], Type.Type) -> ([String], Type.Type) -> Bool
isEquivalentType (oldVars, oldType) (newVars, newType) =
    case diffType oldType newType of
      Nothing -> False
      Just renamings ->
          length oldVars == length newVars
          && isEquivalentRenaming (zip oldVars newVars ++ renamings)


-- TYPES

diffType :: Type.Type -> Type.Type -> Maybe [(String,String)]
diffType oldType newType =
    case (oldType, newType) of
      (Type.Var oldName, Type.Var newName) ->
          Just [(oldName, newName)]

      (Type.Type oldName, Type.Type newName) ->
          if oldName == newName
              then Just []
              else Nothing

      (Type.Lambda a b, Type.Lambda a' b') ->
          (++)
              <$> diffType a a'
              <*> diffType b b'

      (Type.App t ts, Type.App t' ts') ->
          if length ts /= length ts'
            then Nothing
            else
                (++)
                    <$> diffType t t'
                    <*> (concat <$> zipWithM diffType ts ts')

      (Type.Record fields maybeExt, Type.Record fields' maybeExt') ->
          case (maybeExt, maybeExt') of
            (Nothing, Just _) -> Nothing
            (Just _, Nothing) -> Nothing
            (Nothing, Nothing) -> diffFields fields fields'
            (Just ext, Just ext') ->
                (++)
                    <$> diffType ext ext'
                    <*> diffFields fields fields'

      (_, _) ->
          Nothing


diffFields :: [(String, Type.Type)] -> [(String, Type.Type)] -> Maybe [(String,String)]
diffFields rawFields rawFields'
    | length rawFields /= length rawFields' = Nothing
    | or (zipWith ((/=) `on` fst) fields fields') = Nothing
    | otherwise =
        concat <$> zipWithM (diffType `on` snd) fields fields'
    where
      fields  = sort rawFields
      fields' = sort rawFields'

      sort =
          List.sortBy (compare `on` fst)


-- TYPE VARIABLES

isEquivalentRenaming :: [(String,String)] -> Bool
isEquivalentRenaming varPairs =
    case mapM verify renamings of
      Nothing -> False
      Just verifiedRenamings ->
          allUnique (map snd verifiedRenamings)
  where
    renamings =
        Map.toList (foldr insert Map.empty varPairs)

    insert (old,new) dict =
        Map.insertWith (++) old [new] dict

    verify (old, news) =
        case news of
          [] -> Nothing
          new : rest ->
              if all (new ==) rest
                  then Just (old, new)
                  else Nothing

    allUnique list =
        length list == Set.size (Set.fromList list)


compatableVars :: String -> String -> Bool
compatableVars old new =
    case (categorizeVar old, categorizeVar new) of
      (Comparable, Comparable) -> True
      (Appendable, Appendable) -> True
      (Number    , Number    ) -> True

      (Comparable, Appendable) -> True
      (Number    , Comparable) -> True

      (_, Var) -> True

      (_, _) -> False


data TypeVarCategory
    = Comparable
    | Appendable
    | Number
    | Var


categorizeVar :: String -> TypeVarCategory
categorizeVar varName
    | any (/= '\'') primes = Var
    | name == "comparable" = Comparable
    | name == "appendable" = Appendable
    | name == "number"     = Number
    | otherwise            = Var
    where
      (name, primes) =
          break (=='\'') varName