Skip to content
Snippets Groups Projects
Select Git revision
  • 380af003cd225be83a83eeb45c0196ef6f2efbbb
  • master default protected
  • jwt_encode_inconsistencies
  • recovery-code-pwhash
  • incremental-sync
  • redis-rate-limits
  • typehints
  • v1.2.x
  • v1.x.x
  • v1.1.x
  • feature_invite_validuntil_minmax
  • Dockerfile
  • v1.0.x
  • roles-recursive-cte
  • v2.3.1
  • v2.3.0
  • v2.2.0
  • v2.1.0
  • v2.0.1
  • v2.0.0
  • v1.2.0
  • v1.1.2
  • v1.1.1
  • v1.0.2
  • v1.1.0
  • v1.0.1
  • v1.0.0
  • v0.3.0
  • v0.2.0
  • v0.1.5
  • v0.1.4
  • v0.1.2
32 results

models.py

Blame
  • Forked from uffd / uffd
    Source project has a limited visibility.
    CheckDir.hs 10.68 KiB
    {-# LANGUAGE BangPatterns         #-}
    {-# LANGUAGE DataKinds            #-}
    {-# LANGUAGE DeriveAnyClass       #-}
    {-# LANGUAGE DeriveGeneric        #-}
    {-# LANGUAGE FlexibleContexts     #-}
    {-# LANGUAGE FlexibleInstances    #-}
    {-# LANGUAGE LambdaCase           #-}
    {-# LANGUAGE OverloadedStrings    #-}
    {-# LANGUAGE TupleSections        #-}
    {-# LANGUAGE TypeFamilies         #-}
    {-# LANGUAGE UndecidableInstances #-}
    
    -- | Module that contains high-level checking for an entire directory
    module CheckDir ( maximumLintLevel
                    , recursiveCheckDir
                    , DirResult (..)
                    , MissingAsset(..)
                    , MissingDep(..)
                    , resultIsFatal
                    ,shrinkDirResult)  where
    
    import           Universum              hiding (Set)
    
    import           CheckMap               (MapResult (..), Optional,
                                             ResultKind (..), loadAndLintMap,
                                             shrinkMapResult)
    import           Control.Monad.Extra    (mapMaybeM)
    import           Data.Aeson             (ToJSON, (.=))
    import qualified Data.Aeson             as A
    import           Data.List              (partition)
    import qualified Data.Map               as M
    import           Data.Map.Strict        (mapKeys, mapWithKey, (\\))
    import           Data.Text              (isInfixOf)
    import qualified Data.Text              as T
    import           Data.Tiled             (Tiledmap)
    import           Dirgraph               (graphToDot, invertGraph, resultToGraph,
                                             takeSubGraph, unreachableFrom)
    import           GHC.Generics           (Generic)
    import           LintConfig             (LintConfig', configMaxLintLevel)
    import           Paths                  (normalise, normaliseWithFrag)
    import           System.Directory.Extra (doesFileExist)
    import           System.FilePath        (splitPath, (</>))
    import qualified System.FilePath        as FP
    import           System.FilePath.Posix  (takeDirectory)
    import           Text.Dot               (showDot)
    import           Types                  (Dep (Local, LocalMap), Hint (Hint),
                                             Level (..), hintLevel)
    import           Util                   (PrettyPrint (prettyprint), ellipsis)
    
    
    -- based on the startling observation that Data.Map has lower complexity
    -- for difference than Data.Set, but the same complexity for fromList
    type Set a = Map a ()
    setFromList :: Ord a => [a] -> Set a
    setFromList  = M.fromList . flip zip (repeat ())
    listFromSet :: Set a -> [a]
    listFromSet = map fst . M.toList
    
    -- | Result of linting an entire directory / repository
    data DirResult (complete :: ResultKind) = DirResult
      { dirresultMaps          :: Map FilePath (MapResult complete)
      -- ^ all maps of this respository, by (local) filepath
      , dirresultDeps          :: [MissingDep]
      -- ^ all dependencies to things outside this repository
      , dirresultMissingAssets :: [MissingAsset]
      -- ^ entrypoints of maps which are referred to but missing
      , dirresultGraph         :: Text
      } deriving (Generic)
    
    instance NFData (Optional a (Maybe Tiledmap)) => NFData (DirResult a)
    
    
    data MissingDep = MissingDep
      { depFatal   :: Maybe Bool
      , entrypoint :: Text
      , neededBy   :: [FilePath]
      } deriving (Generic, ToJSON, NFData)
    
    -- | Missing assets are the same thing as missing dependencies,
    -- but should not be confused (and also serialise differently
    -- to json)
    newtype MissingAsset = MissingAsset MissingDep
      deriving (Generic, NFData)
    
    
    -- | "shrink" the result by throwing the adjusted tiledmaps away
    shrinkDirResult :: DirResult Full -> DirResult Shrunk
    shrinkDirResult !res =
      res { dirresultMaps = fmap shrinkMapResult (dirresultMaps res) }
    
    -- | given this config, should the result be considered to have failed?
    resultIsFatal :: LintConfig' -> DirResult Full -> Bool
    resultIsFatal config res =
      not (null (dirresultMissingAssets res) || not (any (isJust . depFatal) (dirresultDeps res)))
      || maximumLintLevel res > configMaxLintLevel config
    
    -- | maximum lint level that was observed anywhere in any map.
    -- note that it really does go through all lints, so don't
    -- call it too often
    maximumLintLevel :: DirResult a -> Level
    maximumLintLevel res
      | not (null (dirresultMissingAssets res)) = Fatal
      | otherwise =
        (maybe Info maximum . nonEmpty)
        . map hintLevel
        . concatMap (\map -> keys (mapresultLayer map)
                      <> keys (mapresultTileset map)
                      <> mapresultGeneral map
                    )
        . elems
        . dirresultMaps
        $ res
    
    
    
    instance ToJSON (DirResult a) where
      toJSON res = A.object [
        "result" .=  A.object
          [ "missingDeps" .= dirresultDeps res
          , "missingAssets" .= dirresultMissingAssets res
          -- some repos have auto-generated maps which are basically all the
          -- same; aggregate those to reduce output size
          , "mapLints" .= (M.fromList
                           . fmap (first (ellipsis 6))
                           . foldr aggregateSameResults []
                           . M.toList
                           $ dirresultMaps res)
          , "exitGraph" .= dirresultGraph res
          ]
        , "severity" .= maximumLintLevel res
        , "mapInfo" .= fmap (\tm -> A.object [ "badges" .= mapresultBadges tm ])
                            (dirresultMaps res)
        ]
        where
          aggregateSameResults (path,res) acc =
            case partition (\(_,res') -> res == res') acc of
              ([],_)             -> ([toText path], res):acc
              ((paths,_):_,acc') -> (toText path:paths, res) : acc'
    
    instance ToJSON MissingAsset where
      toJSON (MissingAsset md) = A.object
        [ "asset" .= entrypoint md
        , "neededBy" .= neededBy md
        ]
    
    
    instance PrettyPrint (Level, DirResult a) where
      prettyprint (level, res) = prettyMapLints <> prettyMissingDeps
        where
          prettyMissingDeps = if not (null (dirresultDeps res))
            then "\nDependency Errors:\n" <> foldMap prettyprint (dirresultDeps res)
            else ""
          prettyMapLints = T.concat
            (map prettyLint $ M.toList $ dirresultMaps res)
          prettyLint :: (FilePath, MapResult a) -> Text
          prettyLint (p, lint) =
            "\nin " <> toText p <> ":\n" <> prettyprint (level, lint)
    
    instance PrettyPrint MissingDep where
      prettyprint (MissingDep _ f n) =
        "  - " <> f <> " does not exist, but is required by "
        <> prettyDependents <> "\n"
        where
          prettyDependents =
            T.intercalate "," $ map toText n
    
    
    -- | check an entire repository
    recursiveCheckDir
      :: LintConfig'
      -> FilePath
      -- ^ the repository's prefix (i.e. path to its directory)
      -> FilePath
      -- ^ the repository's entrypoint (filename of a map, from the repo's root)
      -> IO (DirResult Full)
    recursiveCheckDir config prefix root = do
      maps <- recursiveCheckDir' config prefix [root] mempty
    
      let exitGraph = resultToGraph maps
      -- maps that don't have (local) ways back to the main entrypoint
      let nowayback =
            unreachableFrom root
            . invertGraph
            $ exitGraph
    
      -- inject warnings for maps that have no way back to the entrypoint
      let maps' = flip mapWithKey maps $ \path res ->
            if path `elem` nowayback
            then res { mapresultGeneral =
                       Hint Warning ("Cannot go back to " <> toText root <> " from this map.")
                       : mapresultGeneral res
                     }
            else res
    
      mAssets <- missingAssets prefix maps'
      pure $ DirResult { dirresultDeps = missingDeps root maps'
                       , dirresultMissingAssets = mAssets
                       , dirresultMaps = maps'
                       , dirresultGraph =
                         toText
                         . showDot
                         . graphToDot
                         . takeSubGraph 7 root
                         $ exitGraph
                       }
    
    
    -- | Given a (partially) completed DirResult, check which local
    -- maps are referenced but do not actually exist.
    missingDeps :: FilePath -> Map FilePath (MapResult a) -> [MissingDep]
    missingDeps entrypoint maps =
      let simple = M.insert (toText entrypoint) [] used \\ M.union defined trivial
      in M.foldMapWithKey (\f n -> [MissingDep (Just $ not ("#" `isInfixOf` f)) f n]) simple
      where
        -- which maps are linked somewhere?
        used :: Map Text [FilePath]
        used = M.fromList
          $ M.foldMapWithKey
          (\path v -> map (, [path]) . mapMaybe (extractLocalDeps path) . mapresultDepends $ v)
          maps
          where extractLocalDeps prefix = \case
                  LocalMap name -> Just $ toText $ normaliseWithFrag prefix name
                  _             -> Nothing
        -- which are defined using startLayer?
        defined :: Set Text
        defined = setFromList
          $ M.foldMapWithKey
          (\k v -> map ((toText k <> "#") <>) . mapresultProvides $ v)
          maps
        -- each map file is an entrypoint by itself
        trivial = mapKeys toText $ void maps
    
    -- | Checks if all assets referenced in the result actually exist as files
    missingAssets :: FilePath -> Map FilePath (MapResult a) -> IO [MissingAsset]
    missingAssets prefix maps =
      mapM (fmap (fmap (fmap MissingAsset)) missingOfMap) (M.toList maps) <&> fold
      where missingOfMap (path, mapres) = mapMaybeM
              (\case Local relpath ->
                       let asset = normalise (takeDirectory path) relpath
                       in doesFileExist (prefix </> asset) <&>
                         \case True  -> Nothing
                               False -> Just $ MissingDep Nothing (toText asset) [path]
                     _ -> pure Nothing)
              (mapresultDepends mapres)
    
    
    -- | recursive checking of all maps in a repository
    recursiveCheckDir'
      :: LintConfig'
      -> FilePath
      -- ^ the repo's directory
      -> [FilePath]
      -- ^ paths of maps yet to check
      -> Map FilePath (MapResult Full)
      -- ^ accumulator for map results
      -> IO (Map FilePath (MapResult Full))
    recursiveCheckDir' config prefix paths !acc = do
    
      -- lint all maps in paths. The double fmap skips maps which cause IO errors
      -- (in which case loadAndLintMap returns Nothing); appropriate warnings will
      -- show up later during dependency checks
      lints <-
        let lintPath p = fmap (fmap (p,)) (loadAndLintMap config (prefix </> p) depth)
              where depth = length (splitPath p) - 1
        in mapMaybeM lintPath paths >>= evaluateNF
    
    
      let mapdeps = setFromList (concatMap extractDeps lints)
           where extractDeps (mappath, lintresult) =
                   fmap (FP.normalise . normalise (takeDirectory mappath))
                   . mapMaybe onlyLocalMaps
                   $ mapresultDepends lintresult
                 onlyLocalMaps = \case
                   LocalMap p -> Just p
                   _          -> Nothing
    
      let acc' = acc <> M.fromList lints
    
      -- newly found maps that still need to be checked
      let unknowns = listFromSet $ M.difference mapdeps acc
    
      -- no further maps? return acc'. Otherwise, recurse
      case unknowns of
        [] -> pure acc'
        _  -> recursiveCheckDir' config prefix unknowns acc'