packages feed

henforcer-1.0.0.0: src/Henforcer/CodeStructure/ModuleTree.hs

{- |
Module      : Henforcer.CodeStructure.ModuleTree
Description : Definition of 'ModuleTree', parsing, and related functionality.
Copyright   : (c) Flipstone Technology Partners, 2023-2026
License     : MIT
Maintainer  : maintainers@flipstone.com
-}
module Henforcer.CodeStructure.ModuleTree
  ( ModuleTree
  , parse
  , isSuperTreeOf
  , treeContainsModule
  , treeStrictlyContainsModule
  , moduleTreeCodec
  , moduleTreeListCodec
  ) where

import qualified Data.List.NonEmpty as NEL
import qualified Data.Text as T
import qualified Toml

import qualified CompatGHC

{- | Conceptually a "module tree" refers to a root module (e.g. `Data.Text`) and all the modules are
prefixed by it (e.g. `Data.Text.Encoding` and `Data.Text.Lazy`). A 'ModuleTree' is a
representation of a 'CompatGHC.ModuleName'
@since 1.0.0.0
-}
data ModuleTree
  = ModuleTree !String !(Maybe ModuleTree)
  deriving (Eq, Show)

-- | @since 1.0.0.0
instance CompatGHC.Outputable ModuleTree where
  ppr =
    CompatGHC.text . format

{- | Invert 'parse', using "." as the separator between parts of the 'ModuleTree'.

@since 1.0.0.0
-}
format :: ModuleTree -> String
format (ModuleTree name maybeRest) =
  let
    joinName rest =
      name <> ('.' : format rest)
   in
    maybe name joinName maybeRest

{- | Parse a 'String' into a 'ModuleTree', the input will not be validated.

@since 1.0.0.0
-}
parse :: String -> ModuleTree
parse str =
  let
    addPart newPart subModuleTree =
      Just (ModuleTree newPart subModuleTree)

    (x NEL.:| xs) = moduleNameParts str
   in
    ModuleTree x $ foldr addPart Nothing xs

{- | Is a 'ModuleTree' contained within another 'ModuleTree'.

@since 1.0.0.0
-}
isSuperTreeOf :: ModuleTree -> ModuleTree -> Bool
isSuperTreeOf (ModuleTree parent mbParentRest) (ModuleTree child mbChildRest) =
  (parent == child)
    && case (mbParentRest, mbChildRest) of
      (Nothing, Just _) ->
        True
      (Just _, Nothing) ->
        False
      (Nothing, Nothing) ->
        True
      (Just parentRest, Just childRest) ->
        isSuperTreeOf parentRest childRest

{- | Is a 'CompatGHC.ModuleName' contained within a 'ModuleTree'.

@since 1.0.0.0
-}
treeContainsModule :: ModuleTree -> CompatGHC.ModuleName -> Bool
treeContainsModule moduleTree =
  isSuperTreeOf moduleTree . moduleTreeOfModule

{- | Is a 'CompatGHC.ModuleName' contained within a 'ModuleTree' without being exactly the
'ModuleTree'.

@since 1.0.0.0
-}
treeStrictlyContainsModule :: ModuleTree -> CompatGHC.ModuleName -> Bool
treeStrictlyContainsModule moduleTree modName =
  let moduleTreeForName = moduleTreeOfModule modName
   in (moduleTree /= moduleTreeForName)
        && isSuperTreeOf moduleTree moduleTreeForName

{- | Parse a 'CompatGHC.ModuleName' into a 'ModuleTree'.

@since 1.0.0.0
-}
moduleTreeOfModule :: CompatGHC.ModuleName -> ModuleTree
moduleTreeOfModule =
  parse . CompatGHC.moduleNameString

moduleNameParts :: String -> NEL.NonEmpty String
moduleNameParts modName =
  case break (== '.') modName of
    (firstPart, '.' : rest) ->
      NEL.cons firstPart $ moduleNameParts rest
    (onlyPart, _) ->
      pure onlyPart

{- | A codec for a single 'ModuleTree'

@since 1.0.0.0
-}
moduleTreeCodec :: Toml.Key -> Toml.TomlCodec ModuleTree
moduleTreeCodec =
  Toml.dimap format parse . Toml.string

{- | A codec for a list of 'ModuleTree' that is required and if missing will throw as opposed to
being an empty list.

@since 1.0.0.0
-}
moduleTreeListCodec :: Toml.Key -> Toml.TomlCodec [ModuleTree]
moduleTreeListCodec =
  Toml.arrayOf (Toml._TextBy (T.pack . format) (pure . parse . T.unpack))