packages feed

glualint-1.26.0: app/GLuaFixer/Effects/Files.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module GLuaFixer.Effects.Files where

import Control.DeepSeq (deepseq, force)
import Control.Monad.IO.Class (liftIO)
import Data.List (foldl', stripPrefix)
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, IOE, (:>))
import Effectful.Dispatch.Dynamic (interpose, interpret, send)
import Effectful.Dispatch.Static (HasCallStack)
import GLuaFixer.Effects.Logging (Logging, putStrLnStdError)
import qualified System.Directory as Dir
import System.FilePath (takeDirectory, (</>))
import System.FilePath.Find (FindClause, always, fileName, filePath, find, (&&?), (/~?), (~~?))
import System.FilePath.GlobPattern (GlobPattern)
import System.IO (IOMode (..), hGetContents, hPutStrLn, hSetEncoding, utf8_bom, withFile)
import Prelude hiding (readFile, writeFile)

newtype Directory = Directory {dir :: FilePath}
newtype FileNames = FileNames {names :: [String]}
newtype IgnoreFiles = IgnoreFiles {ignore :: [GlobPattern]}

-- | Interacting with the file system
data Files :: Effect where
  -- | Get current working directory alias
  GetCurrentDirectory :: Files m FilePath
  -- | Read stdin
  ReadStdIn :: Files m String
  -- | Read a file
  ReadFile :: FilePath -> Files m String
  -- | Write to a file
  WriteFile :: FilePath -> String -> Files m ()
  -- | Returns whether a file exists
  FileExists :: FilePath -> Files m Bool
  -- | Returns whether a file exists and is a directory
  IsDirectory :: FilePath -> Files m Bool
  -- | Will search upwards in directories for a file, returning it if it exists
  SearchUpwardsForFile :: Directory -> FileNames -> Files m (Maybe FilePath)
  -- | Returns the file path of the first path that exists
  FirstExists :: [FilePath] -> Files m (Maybe FilePath)
  -- | Search for Lua files
  FindLuaFiles :: IgnoreFiles -> FilePath -> Files m [FilePath]
  -- | Get Home directory
  GetHomeDirectory :: Files m FilePath
  -- | Make a path absolute
  MakeAbsolute :: FilePath -> Files m FilePath

type instance DispatchOf Files = Dynamic

makeAbsolute :: Files :> es => FilePath -> Eff es FilePath
makeAbsolute filepath = send $ MakeAbsolute filepath

getHomeDirectory :: Files :> es => Eff es FilePath
getHomeDirectory = send GetHomeDirectory

findLuaFiles :: Files :> es => IgnoreFiles -> FilePath -> Eff es [FilePath]
findLuaFiles ignoreFiles filepath =
  send $ FindLuaFiles ignoreFiles filepath

firstExists :: Files :> es => [FilePath] -> Eff es (Maybe FilePath)
firstExists files = send $ FirstExists files

searchUpwardsForFile :: Files :> es => Directory -> FileNames -> Eff es (Maybe FilePath)
searchUpwardsForFile directory files = send $ SearchUpwardsForFile directory files

isDirectory :: Files :> es => FilePath -> Eff es Bool
isDirectory filepath = send $ IsDirectory filepath

fileExists :: Files :> es => FilePath -> Eff es Bool
fileExists filepath = send $ FileExists filepath

writeFile :: Files :> es => FilePath -> String -> Eff es ()
writeFile filepath contents = send $ WriteFile filepath contents

readFile :: Files :> es => FilePath -> Eff es String
readFile filepath = send $ ReadFile filepath

readStdIn :: Files :> es => Eff es String
readStdIn = send ReadStdIn

getCurrentDirectory
  :: forall (es_a9miX :: [Effect])
   . (HasCallStack, Files :> es_a9miX)
  => Eff es_a9miX FilePath
getCurrentDirectory = send (GetCurrentDirectory @(Eff es_a9miX))

runFilesIO :: forall es a. IOE :> es => Eff (Files : es) a -> Eff es a
runFilesIO = interpret $ \_ -> \case
  GetCurrentDirectory -> liftIO Dir.getCurrentDirectory
  ReadStdIn -> liftIO getContents
  ReadFile file -> liftIO $ do
    withFile file ReadMode $ \handle -> do
      hSetEncoding handle utf8_bom
      !contents <- force <$> hGetContents handle
      pure contents
  WriteFile file contents -> deepseq contents $
    liftIO $
      withFile file WriteMode $ \handle -> do
        hSetEncoding handle utf8_bom
        hPutStrLn handle contents
  FileExists filepath -> liftIO $ Dir.doesFileExist filepath
  IsDirectory filepath -> liftIO $ Dir.doesDirectoryExist filepath
  SearchUpwardsForFile (Directory directory) (FileNames filename) ->
    let
      go !subdir = do
        let
          filepaths = fmap (subdir </>) filename
        mbFoundFile <- runFilesIO $ firstExists filepaths
        case mbFoundFile of
          Just file -> pure $ Just file
          Nothing -> do
            let
              up = takeDirectory subdir
            dirExists <- runFilesIO $ isDirectory up
            if up == subdir || not dirExists
              then pure Nothing
              else go up
    in
      go directory
  FirstExists filepath -> case filepath of
    (file : files) -> do
      exists <- runFilesIO $ fileExists file
      if exists
        then pure $ Just file
        else runFilesIO $ firstExists files
    [] -> pure Nothing
  FindLuaFiles (IgnoreFiles ignoreFiles) path ->
    let
      ignoredGlobs = foldl' (&&?) always $ map excludeFiles ignoreFiles

      -- Exclude a path when either its relative path or its full path match the ignore globs.
      excludeFiles :: GlobPattern -> FindClause Bool
      excludeFiles glob = relativeFilePath /~? glob &&? filePath /~? glob

      relativeFilePath :: FindClause FilePath
      relativeFilePath = fmap stripFromPath filePath

      -- Turn absolute path into relative path by stripping prefix
      stripFromPath :: FilePath -> FilePath
      stripFromPath fp =
        fromMaybe fp $ listToMaybe $ mapMaybe (`stripPrefix` fp) pathsToStrip

      -- Prefixes to attempt to strip, in order
      pathsToStrip =
        [ "./"
        , path ++ "/"
        , path
        ]
    in
      liftIO $ find always (fileName ~~? "*.lua" &&? ignoredGlobs) path
  GetHomeDirectory -> liftIO Dir.getHomeDirectory
  MakeAbsolute filepath -> liftIO $ Dir.makeAbsolute filepath

-- | Trace actions on files, useful for debugging
traceFiles :: (Files :> es, Logging :> es) => Eff es a -> Eff es a
traceFiles = interpose $ \_ -> \case
  GetCurrentDirectory -> do
    putStrLnStdError "GetCurrentDirectory"
    send GetCurrentDirectory
  ReadStdIn -> do
    putStrLnStdError "ReadStdIn"
    send ReadStdIn
  ReadFile filepath -> do
    putStrLnStdError $ "ReadFile " <> filepath
    send $ ReadFile filepath
  WriteFile filepath contents -> do
    putStrLnStdError $ "WriteFile " <> filepath
    send $ WriteFile filepath contents
  FileExists filepath -> do
    putStrLnStdError "FileExists"
    send $ FileExists filepath
  IsDirectory filepath -> do
    putStrLnStdError $ "IsDirectory " <> filepath
    send $ IsDirectory filepath
  SearchUpwardsForFile directory filenames -> do
    putStrLnStdError $ "SearchUpwardsForFile " <> directory.dir <> " " <> show filenames.names
    send $ SearchUpwardsForFile directory filenames
  FirstExists filepath -> do
    putStrLnStdError $ "FirstExists " <> show filepath
    send $ FirstExists filepath
  FindLuaFiles ignores filepath -> do
    putStrLnStdError $ "FindLuaFiles " <> show ignores.ignore <> " " <> filepath
    send $ FindLuaFiles ignores filepath
  GetHomeDirectory -> do
    putStrLnStdError "GetHomeDirectory"
    send GetHomeDirectory
  MakeAbsolute filepath -> do
    putStrLnStdError $ "MakeAbsolute " <> filepath
    send $ MakeAbsolute filepath

traceFilesIfEnabled :: (Files :> es, Logging :> es) => Bool -> Eff es a -> Eff es a
traceFilesIfEnabled enabled = if enabled then traceFiles else id