packages feed

module-munging-0.1.0.0: library/ModuleMunging.hs

module ModuleMunging
  ( Module(..)
  , ModuleName(..)
  , buildModule
  , displayModule
  , ModuleFragment(..)
  , ModuleExport(..)
  , ModuleImport(..)
  , ModuleImportStyle(..)
  , ModuleDeclaration(..)
  , DeclName(..)
  , DeclBody(..)
  ) where

import Prelude

import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Kind (Type)
import Data.String (IsString)
import Text.Printf (printf)

import Data.Char qualified as Char
import Data.List qualified as List
import Data.Maybe qualified as Maybe

type Module :: Type
data Module = Module
  { moduleName :: String
  , moduleExports :: [String]
  , moduleImports :: [ModuleImport]
  , moduleDeclarations :: [ModuleDeclaration]
  } deriving stock (Eq, Show)

type ModuleName :: Type
data ModuleName
  = ModuleNameExact String
  | ModuleNameFromFilePath FilePath

moduleNameFromFilePath :: FilePath -> Maybe String
moduleNameFromFilePath fp = do
  fp' <- List.reverse <$> List.stripPrefix "sh." (List.reverse fp)
  n : _ <- pure $ dropWhile (not . Char.isUpper . head) $ List.tails fp'
  pure $ n & map \case
    c | Char.isAlphaNum c || c == '_' -> c
      | otherwise -> '.'

buildModule :: ModuleName -> ModuleFragment -> Module
buildModule name modFragment =
  Module
    { moduleName =
        case name of
          ModuleNameExact n -> n
          ModuleNameFromFilePath fp
            | Just n <- moduleNameFromFilePath fp -> n
            | otherwise -> error $ printf "buildModule: Failed to convert filepath \"%s\" to module" fp
    , moduleExports = toExports $ moduleFragmentDeclarations modFragment
    , moduleImports =
        flattenModuleImportGroups
          $ List.groupBy groupModuleImports
          $ List.sort
          $ moduleFragmentImports modFragment
    , moduleDeclarations = moduleFragmentDeclarations modFragment
    }
  where
  toExports :: [ModuleDeclaration] -> [String]
  toExports = filter (not . null) . fmap \case
    ModuleDeclaration shouldExport (DeclName funName) _
      | shouldExport -> funName
      | otherwise -> []

  flattenModuleImportGroups :: [[ModuleImport]] -> [ModuleImport]
  flattenModuleImportGroups = foldMap \case
    [] -> []
    x : xs ->
      case moduleImportStyle x of
        ModuleImportStyleOpen -> pure x
        ModuleImportStyleExplicit ids ->
          pure x
            { moduleImportStyle =
                ModuleImportStyleExplicit
                  $ List.sort
                  $ List.nub
                  $ ids <> idsFromModuleImportGroup (moduleImportStyle <$> xs)
            }
        ModuleImportStyleQualified {} -> pure x

  idsFromModuleImportGroup :: [ModuleImportStyle] -> [String]
  idsFromModuleImportGroup = foldMap \case
    ModuleImportStyleExplicit ids -> ids
    _ -> []

  groupModuleImports :: ModuleImport -> ModuleImport -> Bool
  groupModuleImports x y =
    moduleImportName x == moduleImportName y &&
      case (moduleImportStyle x, moduleImportStyle y) of
        (ModuleImportStyleOpen, ModuleImportStyleOpen) -> True
        (ModuleImportStyleExplicit {}, ModuleImportStyleExplicit {}) -> True
        (ModuleImportStyleQualified qx, ModuleImportStyleQualified qy) -> qx == qy
        (_, _) -> False

displayModule :: Module -> String
displayModule m =
  unlines
    $ "-- Auto-generated - do not manually modify!"
    : "{-# LANGUAGE ImportQualifiedPost #-}"
    : "module " <> moduleName
    : "  ( " <> List.intercalate "\n  , " moduleExports
    : "  ) where"
    : mconcat
        [ spacingIfNotNull openImportLines
        , spacingIfNotNull explicitImportLines
        , spacingIfNotNull qualifiedImportLines
        , spacingIfNotNull moduleDeclarationLines
        ]
  where
  spacingIfNotNull :: [String] -> [String]
  spacingIfNotNull xs
    | null xs = []
    | otherwise = [List.intercalate "\n" $ "" : xs]

  openImportLines :: [String]
  openImportLines =
    openImports <&> \n -> "import " <> n

  openImports :: [String]
  openImports =
    moduleImports & Maybe.mapMaybe \case
      ModuleImport { moduleImportName = n, moduleImportStyle = s }
        | ModuleImportStyleOpen <- s -> Just n
      _ -> Nothing

  explicitImportLines :: [String]
  explicitImportLines =
    explicitImports
      & fmap \(n, xs) -> "import " <> n <> " (" <> List.intercalate ", " xs <> ")"

  explicitImports :: [(String, [String])]
  explicitImports =
    moduleImports & Maybe.mapMaybe \case
      ModuleImport { moduleImportName = n, moduleImportStyle = s }
        | ModuleImportStyleExplicit xs <- s -> Just (n, xs)
      _ -> Nothing

  qualifiedImportLines :: [String]
  qualifiedImportLines =
    qualifiedImports
      & fmap \case
          (n, Just q) ->  "import " <> n <> " qualified as " <> q
          (n, Nothing) ->  "import " <> n <> " qualified"

  qualifiedImports :: [(String, Maybe String)]
  qualifiedImports =
    moduleImports & Maybe.mapMaybe \case
      ModuleImport { moduleImportName = n, moduleImportStyle = s }
        | ModuleImportStyleQualified q <- s -> Just (n, q)
      _ -> Nothing

  moduleDeclarationLines :: [String]
  moduleDeclarationLines =
    zip [0 :: Int ..] moduleDeclarations
      & fmap \case
          (n, ModuleDeclaration _ (DeclName {}) (DeclBody body))
            | n < 1 -> body
            | otherwise -> "\n" <> body

  Module
    { moduleName
    , moduleExports
    , moduleImports
    , moduleDeclarations
    } = m

type ModuleFragment :: Type
data ModuleFragment = ModuleFragment
  { moduleFragmentImports :: [ModuleImport]
  , moduleFragmentDeclarations :: [ModuleDeclaration]
  } deriving stock (Eq, Show)

instance Semigroup ModuleFragment where
  (<>) :: ModuleFragment -> ModuleFragment -> ModuleFragment
  mf1 <> mf2 =
    ModuleFragment
      { moduleFragmentImports = moduleFragmentImports mf1 <> moduleFragmentImports mf2
      , moduleFragmentDeclarations = moduleFragmentDeclarations mf1 <> moduleFragmentDeclarations mf2
      }

instance Monoid ModuleFragment where
  mempty :: ModuleFragment
  mempty =
    ModuleFragment
      { moduleFragmentImports = []
      , moduleFragmentDeclarations = []
      }

type ModuleExport :: Type
newtype ModuleExport = ModuleExport String
  deriving stock (Eq, Show)
  deriving newtype (IsString)

type ModuleImport :: Type
data ModuleImport = ModuleImport
  { moduleImportName :: String
  , moduleImportStyle :: ModuleImportStyle
  } deriving stock (Eq, Ord, Show)

type ModuleImportStyle :: Type
data ModuleImportStyle
  = ModuleImportStyleOpen
  | ModuleImportStyleExplicit [String]
  | ModuleImportStyleQualified (Maybe String)
  deriving stock (Eq, Ord, Show)

type ModuleDeclaration :: Type
data ModuleDeclaration =
  ModuleDeclaration
    Bool -- ^ 'True' to export, 'False' to not export
    DeclName
    DeclBody
  deriving stock (Eq, Show)

type DeclName :: Type
newtype DeclName = DeclName String
  deriving stock (Eq, Show)
  deriving newtype (IsString)

type DeclBody :: Type
newtype DeclBody = DeclBody String
  deriving stock (Eq, Show)
  deriving newtype (IsString)