summaryrefslogtreecommitdiff
path: root/walint/CheckMap.hs
diff options
context:
space:
mode:
Diffstat (limited to 'walint/CheckMap.hs')
-rw-r--r--walint/CheckMap.hs227
1 files changed, 227 insertions, 0 deletions
diff --git a/walint/CheckMap.hs b/walint/CheckMap.hs
new file mode 100644
index 0000000..ef80a7f
--- /dev/null
+++ b/walint/CheckMap.hs
@@ -0,0 +1,227 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+-- | Module that contains the high-level checking functions
+module CheckMap (loadAndLintMap, MapResult(..), ResultKind(..), Optional,shrinkMapResult) where
+
+import Universum
+
+import Data.Aeson (ToJSON (toJSON))
+import qualified Data.Aeson as A
+import Data.Aeson.Types ((.=))
+import qualified Data.Map as M
+import qualified Data.Text as T
+import qualified Data.Vector as V
+
+
+import Badges (Badge)
+import Data.Tiled (Layer (layerLayers, layerName),
+ Tiledmap (tiledmapLayers, tiledmapTilesets),
+ loadTiledmap)
+import LintConfig (LintConfig (..), LintConfig')
+import LintWriter (LintResult, invertLintResult,
+ resultToAdjusted, resultToBadges,
+ resultToCWs, resultToDeps, resultToJitsis,
+ resultToLints, resultToOffers, runLintWriter)
+import Properties (checkLayer, checkMap, checkTileset)
+import System.FilePath (takeFileName)
+import Types (Dep (MapLink),
+ Hint (Hint, hintLevel, hintMsg), Level (..),
+ lintsToHints)
+import Util (PrettyPrint (prettyprint), prettyprint)
+
+
+data ResultKind = Full | Shrunk
+
+type family Optional (a :: ResultKind) (b :: Type) where
+ Optional Full b = b
+ Optional Shrunk b = ()
+
+-- | What this linter produces: lints for a single map
+data MapResult (kind :: ResultKind) = MapResult
+ { mapresultLayer :: Map Hint [Text]
+ -- ^ lints that occurred in one or more layers
+ , mapresultTileset :: Map Hint [Text]
+ -- ^ lints that occurred in one or more tilesets
+ , mapresultDepends :: [Dep]
+ -- ^ (external and local) dependencies of this map
+ , mapresultProvides :: [Text]
+ -- ^ entrypoints provided by this map (needed for dependency checking)
+ , mapresultAdjusted :: Optional kind (Maybe Tiledmap)
+ -- ^ the loaded map, with adjustments by the linter
+ , mapresultBadges :: [Badge]
+ -- ^ badges that can be found on this map
+ , mapresultCWs :: [Text]
+ -- ^ collected CWs that apply to this map
+ , mapresultJitsis :: [Text]
+ -- ^ all jitsi room slugs mentioned in this map
+ , mapresultGeneral :: [Hint]
+ -- ^ general-purpose lints that didn't fit anywhere else
+ } deriving (Generic)
+
+instance NFData (Optional a (Maybe Tiledmap)) => NFData (MapResult a)
+
+
+instance Eq (MapResult a) where
+ a == b =
+ mapresultLayer a == mapresultLayer b &&
+ mapresultTileset a == mapresultTileset b &&
+ -- mapresultBadges a == mapresultBadges b &&
+ mapresultGeneral a == mapresultGeneral b
+
+
+instance ToJSON (MapResult a) where
+ toJSON res = A.object
+ [ "layer" .= CollectedLints (mapresultLayer res)
+ , "tileset" .= CollectedLints (mapresultTileset res)
+ , "general" .= mapresultGeneral res
+ ]
+
+newtype CollectedLints = CollectedLints (Map Hint [Text])
+
+instance ToJSON CollectedLints where
+ toJSON (CollectedLints col) = toJSON
+ . M.mapKeys hintMsg
+ $ M.mapWithKey (\h cs -> A.object [ "level" .= hintLevel h, "in" .= truncated cs ]) col
+ where truncated cs = if length cs > 10
+ then take 9 cs <> [ "..." ]
+ else cs
+
+
+shrinkMapResult :: MapResult Full -> MapResult Shrunk
+shrinkMapResult !res = res { mapresultAdjusted = () }
+
+-- | this module's raison d'être
+-- Lints the map at `path`, and limits local links to at most `depth`
+-- layers upwards in the file hierarchy
+loadAndLintMap :: LintConfig' -> FilePath -> Int -> IO (Maybe (MapResult Full))
+loadAndLintMap config path depth = loadTiledmap path <&> \case
+ Left err -> Just (MapResult mempty mempty mempty mempty Nothing mempty mempty mempty
+ [ Hint Fatal . toText $ "Fatal: " <> err
+ ])
+ Right waMap ->
+ Just (runLinter (takeFileName path == "main.json") config waMap depth)
+
+-- | lint a loaded map
+runLinter :: Bool -> LintConfig' -> Tiledmap -> Int -> MapResult Full
+runLinter isMain config@LintConfig{..} tiledmap depth = MapResult
+ { mapresultLayer = invertThing layer
+ , mapresultTileset = invertThing tileset
+ , mapresultGeneral =
+ [Hint Warning "main.json should link back to the lobby"
+ | isMain && not (any linksLobby layerDeps)]
+ <> lintsToHints (resultToLints generalResult)
+ , mapresultDepends = resultToDeps generalResult
+ <> layerDeps
+ <> concatMap resultToDeps tileset
+ , mapresultProvides = concatMap resultToOffers layer
+ , mapresultAdjusted = Just adjustedMap
+ , mapresultCWs = resultToCWs generalResult
+ , mapresultJitsis = concatMap resultToJitsis tileset
+ <> concatMap resultToJitsis layer
+ , mapresultBadges = concatMap resultToBadges layer
+ <> resultToBadges generalResult
+ }
+ where
+ linksLobby = \case
+ MapLink link ->
+ ("/@/"<>configEventSlug<>"/lobby") `T.isPrefixOf` link
+ || configAssemblyTag == "lobby"
+ _ -> False
+ layerDeps = concatMap resultToDeps layer
+ layer = checkLayerRec config depth (V.toList $ tiledmapLayers tiledmap)
+ tileset = checkThing tiledmapTilesets checkTileset
+ generalResult = runLintWriter config tiledmap depth checkMap
+
+ checkThing getter checker = V.toList . V.map runCheck $ getter tiledmap
+ where runCheck thing = runLintWriter config thing depth checker
+
+ -- | "inverts" a LintResult, i.e. groups it by lints instead of
+ -- layers / maps
+ invertThing thing = M.unionsWith (<>) $ fmap invertLintResult thing
+
+ adjustedMap = (resultToAdjusted generalResult)
+ { tiledmapLayers = V.fromList
+ . fmap resultToAdjusted
+ $ take (length (tiledmapLayers tiledmap)) layer
+ , tiledmapTilesets = V.fromList
+ . fmap resultToAdjusted
+ $ tileset
+ }
+
+-- | Recursively checks a layer.
+--
+-- This is apparently necessary because someone thought it would be a good
+-- idea to have group layers, even if their entire semantics appear to be
+-- "they're group layers"; they don't seem to /do/ anything …
+--
+-- Note that this will flatten the layer structure and give them all back
+-- in a single list, but the ones that were passed in will always be at
+-- the head of the list.
+checkLayerRec :: LintConfig' -> Int -> [Layer] -> [LintResult Layer]
+checkLayerRec config depth layers =
+ -- reordering to get the correct ones back up front
+ (\rs -> fmap fst rs <> concatMap snd rs)
+ -- map over all input layers
+ $ flip fmap layers $ \parent ->
+ case layerLayers parent of
+ -- not a group layer; just lint this one
+ Nothing ->
+ (runLintWriter config parent depth checkLayer,[])
+ -- this is a group layer. Fun!
+ Just sublayers ->
+ (parentResult, subresults)
+ where
+ -- Lintresults for sublayers with adjusted names
+ subresults :: [LintResult Layer]
+ subresults =
+ take (length sublayers)
+ . fmap (fmap (\l -> l { layerName = layerName parent <> "/" <> layerName l } ))
+ $ subresults'
+
+ -- Lintresults for sublayers and subsublayers etc.
+ subresults' =
+ checkLayerRec config depth sublayers
+
+ -- lintresult for the parent layer
+ parentResult = runLintWriter config parentAdjusted depth checkLayer
+
+ -- the parent layer with adjusted sublayers
+ parentAdjusted =
+ parent { layerLayers = Just (fmap resultToAdjusted subresults') }
+
+
+
+-- human-readable lint output, e.g. for consoles
+instance PrettyPrint (Level, MapResult a) where
+ prettyprint (_, mapResult) = if complete == ""
+ then " all good!\n" else complete
+ where
+ complete = T.concat $ prettyGeneral
+ <> prettyLints mapresultLayer
+ <> prettyLints mapresultTileset
+
+ -- | pretty-prints a collection of Hints, printing each
+ -- Hint only once, then a list of its occurences line-wrapped
+ -- to fit onto a decent-sized terminal
+ prettyLints :: (MapResult a -> Map Hint [Text]) -> [Text]
+ prettyLints getter = fmap
+ (\(h, cs) -> prettyprint h
+ <> "\n (in "
+ <> snd (foldl (\(l,a) c -> case l of
+ 0 -> (T.length c, c)
+ _ | l < 70 -> (l+2+T.length c, a <> ", " <> c)
+ _ -> (6+T.length c, a <> ",\n " <> c)
+ )
+ (0, "") cs)
+ <> ")\n")
+ (M.toList . getter $ mapResult)
+
+ prettyGeneral :: [Text]
+ prettyGeneral = map
+ ((<> "\n") . prettyprint)
+ $ mapresultGeneral mapResult