From ca8552c4eb69b2b8267fe0046320bccf9f547b52 Mon Sep 17 00:00:00 2001
From: stuebinm
Date: Thu, 30 Sep 2021 02:02:29 +0200
Subject: simple map-map link dependency checking

This is purely based on a set difference, i.e. it won't catch stupid
things like a map linking to itself, a map link going only one-way, etc.

Also, it only handles map links; it doesn't check if all ressource files
referenced by a map actually exist.
---
 lib/CheckDir.hs   | 79 +++++++++++++++++++++++++++++++++++++++++++++++--------
 lib/CheckMap.hs   | 11 +++++---
 lib/LintWriter.hs | 12 +++++++++
 lib/Paths.hs      |  7 ++++-
 lib/Properties.hs |  5 +++-
 lib/Types.hs      | 13 ++++++---
 6 files changed, 106 insertions(+), 21 deletions(-)

diff --git a/lib/CheckDir.hs b/lib/CheckDir.hs
index ab231b9..cd8f5ab 100644
--- a/lib/CheckDir.hs
+++ b/lib/CheckDir.hs
@@ -3,27 +3,31 @@
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE LambdaCase        #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TupleSections     #-}
 
 -- | Module that contains high-level checking for an entire directory
+{-# LANGUAGE TupleSections     #-}
 module CheckDir (recursiveCheckDir)  where
 
-import           CheckMap              (MapResult, loadAndLintMap,
-                                        mapresultDepends)
-import           Data.Aeson            (ToJSON)
+import           CheckMap              (MapResult (mapresultProvides),
+                                        loadAndLintMap, mapresultDepends)
+import           Control.Monad         (void)
+import           Data.Aeson            (ToJSON, (.=))
+import qualified Data.Aeson            as A
+import           Data.Bifunctor        (bimap)
+import           Data.Foldable         (fold)
 import           Data.Functor          ((<&>))
 import           Data.Map              (Map)
 import qualified Data.Map              as M
+import           Data.Map.Strict       (mapKeys, (\\))
 import           Data.Maybe            (mapMaybe)
 import           Data.Text             (Text)
 import qualified Data.Text             as T
 import           GHC.Generics          (Generic)
-import           Paths                 (normalise)
+import           Paths                 (normalise, normaliseWithFrag)
 import           System.FilePath       (splitPath, (</>))
 import qualified System.FilePath       as FP
 import           System.FilePath.Posix (takeDirectory)
-import           Types                 (Dep (LocalMap), Hint (hintLevel),
-                                        Level (Info))
+import           Types                 (Dep (LocalMap), Level)
 import           Util                  (PrettyPrint (prettyprint))
 
 -- based on the startling observation that Data.Map has lower complexity
@@ -37,17 +41,41 @@ listFromSet = map fst . M.toList
 -- | Result of linting an entire directory / repository
 data DirResult = DirResult
   { dirresultMaps :: Map FilePath MapResult
-  , dirresultDeps :: [Text]
+  , dirresultDeps :: [MissingDep]
+  } deriving (Generic)
+
+data MissingDep = MissingDep
+  { entrypoint :: Text
+  , neededBy   :: [FilePath]
   } deriving (Generic, ToJSON)
 
+instance ToJSON DirResult where
+  toJSON res = A.object
+    [ "missingDeps" .= dirresultDeps res
+    , "mapLints" .= dirresultMaps res
+    ]
+
 instance PrettyPrint (Level, DirResult) where
-  prettyprint (level, res) = T.concat
-    (map prettyLint $ M.toList $ dirresultMaps res)
+  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) -> Text
       prettyLint (p, lint) =
         "\nin " <> T.pack 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 T.pack n
+
+
 instance Semigroup DirResult where
   a <> b = DirResult
     { dirresultMaps = dirresultMaps a <> dirresultMaps b
@@ -66,7 +94,36 @@ instance Monoid DirResult where
 -- a root (i.e. the name of the file containing the entrypoint
 -- map within that file)
 recursiveCheckDir :: FilePath -> FilePath -> IO DirResult
-recursiveCheckDir prefix root = recursiveCheckDir' prefix [root] mempty mempty
+recursiveCheckDir prefix root = do
+  linted <- recursiveCheckDir' prefix [root] mempty mempty
+  pure $ linted <> mempty { dirresultDeps = missingDeps linted }
+
+
+-- | Given a (partially) completed DirResult, check which local
+-- maps are referenced but do not actually exist.
+missingDeps :: DirResult -> [MissingDep]
+missingDeps res =
+  let simple = used \\ M.union defined trivial
+  in M.foldMapWithKey (\f n -> [MissingDep 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)
+      (dirresultMaps res)
+      where extractLocalDeps prefix = \case
+              LocalMap name -> Just $ T.pack $ normaliseWithFrag prefix name
+              _             -> Nothing
+    -- which are defined using startLayer?
+    defined :: Set Text
+    defined = setFromList
+      $ M.foldMapWithKey
+      (\k v -> map ((T.pack k <> "#") <>) . mapresultProvides $ v)
+      (dirresultMaps res)
+    -- each map file is an entrypoint by itself
+    trivial = mapKeys T.pack $ void (dirresultMaps res)
+
 
 -- | The less-nice function for checking an entire repository.
 --
diff --git a/lib/CheckMap.hs b/lib/CheckMap.hs
index eaeac55..176e3d5 100644
--- a/lib/CheckMap.hs
+++ b/lib/CheckMap.hs
@@ -19,7 +19,7 @@ import           GHC.Generics (Generic)
 
 import           LintWriter   (LintResult (..), LintWriter, askContext,
                                filterLintLevel, lintToDep, resultToDeps,
-                               resultToLints, runLintWriter)
+                               resultToLints, resultToOffers, runLintWriter)
 import           Properties   (checkLayerProperty, checkMap)
 import           Tiled2       (Layer (layerName, layerProperties),
                                Tiledmap (tiledmapLayers), loadTiledmap)
@@ -30,9 +30,10 @@ import           Util         (PrettyPrint (prettyprint), prettyprint)
 
 -- | What this linter produces: lints for a single map
 data MapResult = MapResult
-  { mapresultLayer   :: Maybe (Map Text (LintResult Layer))
-  , mapresultGeneral :: [Lint]
-  , mapresultDepends :: [Dep]
+  { mapresultLayer    :: Maybe (Map Text (LintResult Layer))
+  , mapresultGeneral  :: [Lint]
+  , mapresultDepends  :: [Dep]
+  , mapresultProvides :: [Text]
   } deriving (Generic, ToJSON)
 
 
@@ -45,6 +46,7 @@ loadAndLintMap path depth = loadTiledmap path >>= pure . \case
     Left err -> MapResult
       { mapresultLayer = Nothing
       , mapresultDepends = []
+      , mapresultProvides = []
       , mapresultGeneral =
         [ hint Fatal . T.pack $
           path <> ": Fatal: " <> err
@@ -60,6 +62,7 @@ runLinter tiledmap depth = MapResult
   , mapresultGeneral = generalLints  -- no general lints for now
   , mapresultDepends = concatMap (resultToDeps . snd) layer
     <> mapMaybe lintToDep generalLints
+  , mapresultProvides = concatMap (resultToOffers . snd) layer
   }
   where
     layerMap :: Map Text (LintResult Layer)
diff --git a/lib/LintWriter.hs b/lib/LintWriter.hs
index e704a3c..cdec972 100644
--- a/lib/LintWriter.hs
+++ b/lib/LintWriter.hs
@@ -53,6 +53,11 @@ lintToDep = \case
   Depends dep -> Just dep
   _           -> Nothing
 
+lintToOffer :: Lint -> Maybe Text
+lintToOffer = \case
+  Offers frag -> Just frag
+  _           -> Nothing
+
 filterLintLevel :: Level -> [Lint] -> [Lint]
 filterLintLevel level = mapMaybe $ \l -> if level <= lintLevel l
   then Just l
@@ -61,6 +66,9 @@ filterLintLevel level = mapMaybe $ \l -> if level <= lintLevel l
 resultToDeps :: LintResult a -> [Dep]
 resultToDeps (LintResult a) = mapMaybe lintToDep $ snd a
 
+resultToOffers :: LintResult a -> [Text]
+resultToOffers (LintResult a) = mapMaybe lintToOffer $ snd a
+
 -- | convert a lint result into a flat list of lints
 -- (throwing away information on if a single error was fatal)
 resultToLints :: LintResult a -> [Lint]
@@ -78,6 +86,10 @@ lint level = tell . (: []) . hint level
 dependsOn :: Dep -> LintWriter a
 dependsOn dep = tell . (: []) $ Depends dep
 
+offersEntrypoint :: Text -> LintWriter a
+offersEntrypoint = tell . (: []) . Offers
+
+
 
 info = lint Info
 suggest = lint Suggestion
diff --git a/lib/Paths.hs b/lib/Paths.hs
index 5044673..af66e77 100644
--- a/lib/Paths.hs
+++ b/lib/Paths.hs
@@ -35,8 +35,9 @@ parsePath text =
       else Nothing
 
 instance PrettyPrint RelPath where
-  prettyprint (Path up rest _) = ups <> rest
+  prettyprint (Path up rest frag) = ups <> rest <> fragment
     where ups = T.concat $ replicate up "../"
+          fragment = maybe mempty ("#" <>) frag
 
 -- | Normalises a path.
 --
@@ -48,3 +49,7 @@ normalise prefix (Path 0 path _) = prefix </> T.unpack path
 normalise prefix (Path i path _) =
   concat (take (length dirs - i) dirs) </> T.unpack path
   where dirs = splitPath prefix
+
+normaliseWithFrag :: FilePath -> RelPath -> FilePath
+normaliseWithFrag prefix (Path i path frag) =
+  normalise prefix (Path (i+1) path frag) <> T.unpack (maybe mempty ("#" <>) frag)
diff --git a/lib/Properties.hs b/lib/Properties.hs
index 86acda9..877afe1 100644
--- a/lib/Properties.hs
+++ b/lib/Properties.hs
@@ -13,7 +13,8 @@ import           Tiled2        (Layer (..), Property (..), PropertyValue (..),
 import           Util          (layerIsEmpty, prettyprint)
 
 import           LintWriter    (LintWriter, askContext, askFileDepth, complain,
-                                dependsOn, forbid, suggest, warn)
+                                dependsOn, forbid, offersEntrypoint, suggest,
+                                warn)
 import           Paths         (RelPath (..), parsePath)
 import           Types         (Dep (Link, Local, LocalMap, MapLink))
 
@@ -142,6 +143,8 @@ checkLayerProperty p@(Property name _value) = case name of
         else unwrapPath link (dependsOn . LocalMap)
     "startLayer" -> do
       forbidEmptyLayer
+      layer <- askContext
+      offersEntrypoint $ layerName layer
       unwrapBool p $ \case
         True  -> pure ()
         False -> complain "startLayer must be set to true"
diff --git a/lib/Types.hs b/lib/Types.hs
index b609012..fd2bd20 100644
--- a/lib/Types.hs
+++ b/lib/Types.hs
@@ -45,11 +45,11 @@ instance HasArguments Level where
 
 -- | a hint comes with an explanation (and a level), or is a dependency
 -- (in which case it'll be otherwise treated as an info hint)
-data Lint = Depends Dep | Lint Hint
+data Lint = Depends Dep | Offers Text | Lint Hint
 
 -- | TODO: add a reasonable representation of possible urls
 data Dep = Local RelPath | Link Text | MapLink Text | LocalMap RelPath
-  deriving (Generic)
+  deriving (Generic, Ord, Eq)
 
 data Hint = Hint
   { hintLevel :: Level
@@ -63,20 +63,25 @@ hint level msg = Lint Hint { hintLevel = level, hintMsg = msg }
 
 -- | dependencies just have level Info
 lintLevel :: Lint -> Level
-lintLevel (Lint h)    = hintLevel h
-lintLevel (Depends _) = Info
+lintLevel (Lint h) = hintLevel h
+lintLevel _        = Info
 
 instance PrettyPrint Lint where
   prettyprint (Lint  Hint { hintMsg, hintLevel } ) =
     "  " <> showText hintLevel <> ": " <> hintMsg
   prettyprint (Depends dep) =
     "  Info: found dependency: " <> prettyprint dep
+  prettyprint (Offers dep) =
+    "  Info: map offers entrypoint " <> prettyprint dep
 
 instance ToJSON Lint where
   toJSON (Lint l) = toJSON l
   toJSON (Depends dep) = A.object
     [ "hintMsg" .= prettyprint dep
     , "hintLevel" .= A.String "Dependency Info" ]
+  toJSON (Offers l) = A.object
+    [ "hintMsg" .= prettyprint l
+    , "hintLevel" .= A.String "Entrypoint Info" ]
 
 instance ToJSON Dep where
   toJSON  = \case
-- 
cgit v1.2.3