packages feed

hcount-0.0.2: app/hcount.hs

{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -Wno-x-partial #-}

import Control.Monad
import Data.Bifunctor
import Data.ByteString qualified as BS
import Data.Char qualified as Char
import Data.List qualified as List
import Data.Map.Strict qualified as Map
import Data.Ord
import Data.String.Interpolate
import FlatParse.Basic qualified as FP
import GHC.Data.FastString
import GHC.Iface.Ext.Binary
import GHC.Iface.Ext.Types
import GHC.Types.Name
import GHC.Types.Name.Cache
import GHC.Unit.Module.Name
import GHC.Unit.Types
import NumHask.Prelude
import Optics.Core
import Options.Applicative as OA
import System.Directory
import System.Directory.Recursive
import System.FilePath
import System.Process

-- import Language.Haskell.Syntax.Module.Name

data Options = Options
  { arg :: ArgType,
    run :: RunType,
    stem :: String,
    directory :: String,
    recursive :: Bool,
    exclude :: [String],
    topN :: Int
  }
  deriving (Eq, Show, Generic)

defaultOptions :: Options
defaultOptions = Options ArgReport RunAll ".hie" "." True [] 10

options :: Parser Options
options =
  Options
    <$> parseCommand
    <*> parseRun
    <*> option str (value (stem defaultOptions) <> short 's' <> long "stem" <> help "hie files directory")
    <*> option str (value (directory defaultOptions) <> long "directory" <> short 'd' <> help "base directory")
    <*> switch (long "recursize" <> short 'r' <> help "search recursively")
    <*> many (option str (short 'e' <> long "exclude" <> help "libraries to exclude"))
    <*> option auto (value (topN defaultOptions) <> short 'n' <> long "topn" <> help "report top N results")

data RunType = RunAll | RunUpper | RunLocal | RunOperators | RunLower deriving (Eq, Show, Generic)

data ArgType = ArgReport | ArgAddCpl | ArgBuild | ArgRebuild deriving (Eq, Show, Generic)

parseRun :: Parser RunType
parseRun =
  flag' RunAll (long "all" <> help "all names")
    <|> flag' RunOperators (long "operators" <> help "operators")
    <|> flag' RunLower (long "lower" <> help "lower-case functions")
    <|> flag' RunUpper (long "upper" <> help "upper-case constructors")
    <|> flag' RunLocal (long "local" <> help "local variable names")
    <|> pure RunAll

parseCommand :: Parser ArgType
parseCommand =
  subparser $
    command "report" (info (pure ArgReport) (progDesc "report counts."))
      <> command "build" (info (pure ArgBuild) (progDesc "build projects"))
      <> command "rebuild" (info (pure ArgRebuild) (progDesc "rebuild projects."))
      <> command "addcpl" (info (pure ArgAddCpl) (progDesc "Add a CPL file."))

infoOptions :: ParserInfo Options
infoOptions =
  info
    (options <**> helper)
    (fullDesc <> progDesc "hcount" <> header "count Haskell artifacts")

main :: IO ()
main = do
  o <- execParser infoOptions
  case view #arg o of
    ArgReport -> report o
    ArgRebuild -> do
      n <- rebuild o
      putStrLn $ "rebuilt " <> show n <> " packages"
    ArgBuild -> do
      n <- build o
      putStrLn $ "built " <> show n <> " packages"
    ArgAddCpl -> do
      n <- addcpl o
      putStrLn $ "added " <> show n <> " cpl's"

-- | report to stdout
-- >>> report defaultOptions
-- Number of repos: 1
-- Number of files: 1
-- CCon  ~                 71
-- CCon  Monad             58
-- CCon  Functor           56
-- CCon  Category          54
-- CCon  Semigroup         50
-- CCon  Eq                49
-- CVars x                 37
-- CCon  Ord               34
-- CCon  FromInteger       29
-- CCon  Show              28
report :: Options -> IO ()
report o = do
  fs <- getFiles o
  let ns = fs & getNodes & fmap astFlatten & mconcat & getNames & getSimplifiedNames
  let n = view #topN o
  putStrLn $ "Number of repos: " <> show (length fs)
  putStrLn $ "Number of files: " <> show (length $ mconcat fs)
  case view #run o of
    RunAll -> reportTopX n (const True) printName ns
    RunOperators -> reportTopX n (not . FP.isLatinLetter . head . snd) printName ns
    RunLower -> reportTopX n (\x -> ((CExt ==) . fst) x && (Char.isLower . head . snd) x) printName ns
    RunUpper -> reportTopX n (Char.isUpper . head . snd) printName ns
    RunLocal -> reportTopX n (\x -> ((CVars ==) . fst) x && (Char.isLower . head . snd) x) printName ns

reportTopX :: (Ord a) => Int -> (a -> Bool) -> (a -> String) -> [a] -> IO ()
reportTopX n p pr xs = mapM_ putStrLn $ formatCount 20 . first pr <$> Main.top n (filter p xs)

printName :: (NameCats, String) -> String
printName (c, name) = padr 6 (show c) <> name

-- | Get the hie files, possibly recursively.
getFiles :: Options -> IO [[HieFile]]
getFiles o = do
  dirs <- case view #recursive o of
    True -> getSubdirsRecursive (view #directory o)
    False -> getDirectoryContents (view #directory o) >>= filterM doesDirectoryExist
  let hs = filter (List.isSuffixOf (stem o)) dirs
  mapM readHieFiles hs

-- | Extract the Hie AST
getNodes :: [[HieFile]] -> [HieAST TypeIndex]
getNodes fss =
  mconcat fss
    & fmap (hie_asts >>> getAsts >>> Map.toList)
    & mconcat
    & fmap snd

-- | Extract NodeInfo, throwing structural elements (and everything else) away
astFlatten :: HieAST a -> [NodeInfo a]
astFlatten h = Map.elems (getSourcedNodeInfo $ sourcedNodeInfo h) <> mconcat (fmap astFlatten (nodeChildren h))

-- | Extract the 'Name's from the 'NodeInfo'
getNames :: [NodeInfo a] -> [Name]
getNames ns = [x | (Right x) <- idents]
  where
    idents = fmap (Map.toList . nodeIdentifiers) ns & mconcat & fmap fst

-- | Simplify 'Name' to the hcount categories and a string.
getSimplifiedNames :: [Name] -> [(NameCats, String)]
getSimplifiedNames ids = allNames
  where
    xs = ids & fmap toNameX
    lxs = view #name <$> filter (\x -> module' x == "") xs
    exs = filter (\x -> module' x /= "") xs
    allNames =
      (second FP.utf8ToStr . rp deconstructLocalName <$> lxs)
        <> ((CExt,) . view #name <$> exs)

-- | A simplified categorization of name types
data NameCats
  = -- | Operators ("$_in$$c" prefix)
    COps
  | -- | Contructors ("$_in$$d" prefix)
    CCon
  | -- | Variables ("$_in$" prefix)
    CVars
  | COther
  | CError
  | -- | external project name
    CExt
  deriving (Generic, Eq, Ord, Show)

deconstructLocalName :: FP.Parser () (NameCats, BS.ByteString)
deconstructLocalName =
  $( FP.switch
       [|
         case _ of
           "$_in$$d" -> (CCon,) <$> FP.takeRest
           "$_in$$c" -> (COps,) <$> FP.takeRest
           "$_in$$t" -> (COther,) <$> FP.takeRest
           "$_in$$maxtag_" -> (COther,) <$> FP.takeRest
           "$_in$$tag2con_" -> (COther,) <$> FP.takeRest
           "$_in$$" -> (CError,) <$> FP.takeRest
           "$_in$" -> (CVars,) <$> FP.takeRest
           _ -> (CError,) <$> FP.takeRest
         |]
   )

package' :: FP.Parser () String
package' = reverse . (\x -> bool id (drop 1) (head x == '-') x) . reverse <$> FP.some (FP.satisfy (not . FP.isDigit))

-- | The main data of interest in a 'Name'
data NameX = NameX {name :: String, module' :: String, package :: String} deriving (Generic, Eq, Show, Ord)

-- | xtrct snsbl nms
toNameX :: Name -> NameX
toNameX n =
  bool
    (NameX (nameStableString n) "" "")
    (NameX (occNameString $ nameOccName n) (moduleNameString $ GHC.Unit.Types.moduleName $ nameModule n) (GHC.Data.FastString.unpackFS $ unitIdFS $ GHC.Unit.Types.moduleUnitId $ nameModule n))
    (isExternalName n)

-- | count occurrences
count :: (Ord a) => [a] -> Map.Map a Int
count = Map.fromListWith (+) . fmap (,1)

-- | top n occurrences
top :: (Ord k) => Int -> [k] -> [(k, Int)]
top t xs = take t $ List.sortOn (Down . snd) . Map.toList $ count xs

-- | run an FP.Parser; leftovers is an error
rp :: FP.Parser () a -> String -> a
rp p s = case FP.runParserUtf8 p s of
  FP.OK r "" -> r
  _ -> error "parser error"

-- | run an FP.Parser; leftovers are thrown away
rp' :: FP.Parser () a -> String -> a
rp' p s = case FP.runParserUtf8 p s of
  FP.OK r _ -> r
  _ -> error "parser error"

padl :: Int -> String -> String
padl n t = replicate (n - length t) ' ' <> t

padr :: Int -> String -> String
padr n t = t <> replicate (n - length t) ' '

formatCount :: Int -> (String, Int) -> String
formatCount n (s, x) = padr n s <> padl 6 (show x)

-- | get contents of all @.hie@ files recursively in the given directory
readHieFiles :: FilePath -> IO [HieFile]
readHieFiles hieDir = do
  nameCache <- initNameCache 'h' []
  hieContent <- getDirRecursive hieDir
  let isHieFile f = (&&) (takeExtension f == ".hie") <$> doesFileExist f
  hiePaths <- filterM isHieFile hieContent

  forM hiePaths $ \hiePath -> do
    hieFileResult <- readHieFile nameCache hiePath
    pure $ hie_file_result hieFileResult

cpl :: String
cpl =
  [i|program-options
  ghc-options:
    -fwrite-ide-info
    -hiedir=.hie|]

hasCabalFile :: FilePath -> IO Bool
hasCabalFile d = d & listDirectory & fmap (filter ((== ".cabal") . takeExtension) >>> null >>> not)

hasCPL :: FilePath -> IO Bool
hasCPL d = doesFileExist $ d </> "cabal.project.local"

getSubdirs :: FilePath -> IO [FilePath]
getSubdirs d = d & (listDirectory >=> filterM (doesDirectoryExist . (d </>)))

addCPL :: FilePath -> IO Bool
addCPL d = do
  b <- (\p c -> not p && c) <$> hasCPL d <*> hasCabalFile d
  when b (writeFile (d </> "cabal.project.local") cpl)
  return b

subdirAction :: Options -> (FilePath -> IO a) -> IO [a]
subdirAction o act = do
  xs <- getSubdirs (view #directory o)
  let xs' = filter (not . (`elem` view #exclude o)) xs
  mapM act ((view #directory o </>) <$> xs')

dirAction :: Options -> (FilePath -> IO a) -> IO [a]
dirAction o act = fmap (: []) (act (view #directory o))

recursiveAction :: (FilePath -> IO Bool) -> Options -> IO Int
recursiveAction act o =
  sum . fmap (bool 0 1) <$> bool dirAction subdirAction (view #recursive o) o act

addcpl :: Options -> IO Int
addcpl o = recursiveAction addCPL o

rebuild :: Options -> IO Int
rebuild o = recursiveAction doRebuild o

build :: Options -> IO Int
build o = recursiveAction doBuild o

doRebuild :: FilePath -> IO Bool
doRebuild fp = do
  b <- hasCabalFile fp
  when b $ do
    putStrLn ("rebuilding " <> fp)
    withCurrentDirectory
      fp
      (callCommand "cabal clean && cabal build all")
  pure b

doBuild :: FilePath -> IO Bool
doBuild fp = do
  b <- hasCabalFile fp
  when b $ do
    putStrLn ("rebuilding " <> fp)
    withCurrentDirectory
      fp
      (callCommand "cabal build all")
  pure b