packages feed

glean-hs-0.1.0: haskell/app/Main.hs

-- | glean-hs command line tool.
--
-- Index Haskell projects and query the resulting fact database.
--
-- Usage:
--   glean-hs index --hie-dir .hie --db /tmp/mydb
--   glean-hs query --db /tmp/mydb "validateCDTCode"
--   glean-hs query --db /tmp/mydb "ref:validateCDTCode"
--   glean-hs query --db /tmp/mydb "mod:Glean.Storage"

module Main (main) where

import Control.Exception (catch, SomeException, displayException)
import Data.Text (Text)
import qualified Data.Text as Text
import Options.Applicative
import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)

import Glean.RocksDB (RocksDB)
import Glean.Storage
import Glean.Indexer.HIE
import Glean.Indexer.Types
import Glean.Query

-- ── CLI options ───────────────────────────────────────────────────────────────

data Command
  = Index IndexOptions
  | Query QueryOptions
  | Stats StatsOptions
  deriving (Show)

data IndexOptions = IndexOptions
  { idxHieDir   :: FilePath
  , idxDbPath   :: FilePath
  , idxVerbose  :: Bool
  , idxMaxFiles :: Maybe Int
  } deriving (Show)

data QueryOptions = QueryOptions
  { qryDbPath :: FilePath
  , qryQuery  :: Text
  } deriving (Show)

data StatsOptions = StatsOptions
  { stDbPath :: FilePath
  } deriving (Show)

-- ── Parsers ───────────────────────────────────────────────────────────────────

indexOptions :: Parser IndexOptions
indexOptions = IndexOptions
  <$> strOption
        ( long "hie-dir"
       <> metavar "DIR"
       <> value ".hie"
       <> showDefault
       <> help "Directory containing .hie files" )
  <*> strOption
        ( long "db"
       <> metavar "PATH"
       <> help "Path to the glean-hs database" )
  <*> switch
        ( long "verbose"
       <> short 'v'
       <> help "Print progress information" )
  <*> optional (option auto
        ( long "max-files"
       <> metavar "N"
       <> help "Maximum number of HIE files to index" ))

queryOptions :: Parser QueryOptions
queryOptions = QueryOptions
  <$> strOption
        ( long "db"
       <> metavar "PATH"
       <> help "Path to the glean-hs database" )
  <*> ( Text.pack <$> argument str
          ( metavar "QUERY"
         <> help "Query string. Prefix with ref: for references, mod: for modules" ))

statsOptions :: Parser StatsOptions
statsOptions = StatsOptions
  <$> strOption
        ( long "db"
       <> metavar "PATH"
       <> help "Path to the glean-hs database" )

commandParser :: Parser Command
commandParser = subparser
  ( command "index"
      ( info (Index <$> indexOptions)
             (progDesc "Index a Haskell project from HIE files") )
 <> command "query"
      ( info (Query <$> queryOptions)
             (progDesc "Query the fact database") )
 <> command "stats"
      ( info (Stats <$> statsOptions)
             (progDesc "Show database statistics") )
  )

opts :: ParserInfo Command
opts = info (commandParser <**> helper)
  ( fullDesc
 <> progDesc "glean-hs: Docker-free Haskell code indexing"
 <> header "glean-hs - XF-Interchange LLC" )

-- ── Command handlers ──────────────────────────────────────────────────────────

runIndex :: IndexOptions -> IO ()
runIndex options = do
  let config = defaultDbConfig (idxDbPath options)
  let idxCfg = defaultIndexConfig
        { cfgHieDir   = idxHieDir   options
        , cfgVerbose  = idxVerbose  options
        , cfgMaxFiles = idxMaxFiles options
        }
  withStorage config $ \(db :: RocksDB) -> do
    stats <- indexProject db idxCfg
    putStrLn $ "Indexed " ++ show (statsFilesIndexed stats) ++ " files"
    putStrLn $ "  " ++ show (statsDefsFound    stats) ++ " definitions"
    putStrLn $ "  " ++ show (statsRefsFound    stats) ++ " references"
    putStrLn $ "  " ++ show (statsModulesFound stats) ++ " modules"
    when (statsErrors stats > 0) $
      putStrLn $ "  " ++ show (statsErrors stats) ++ " errors"
  where
    when True  action = action
    when False _      = return ()

runQuery :: QueryOptions -> IO ()
runQuery options = do
  let config = (defaultDbConfig (qryDbPath options))
        { dbReadOnly = True
        , dbCreate   = False
        }
  withStorage config $ \(db :: RocksDB) -> do
    let q = qryQuery options
    putStrLn $ "Query: " ++ Text.unpack q
    if Text.pack "ref:" `Text.isPrefixOf` q
      then do
        refs <- findReferences db (Text.drop 4 q)
        if null refs
          then putStrLn "No references found."
          else do
            putStrLn $ "Found " ++ show (length refs) ++ " reference(s):"
            mapM_ printRef refs
      else if Text.pack "mod:" `Text.isPrefixOf` q
      then do
        facts <- findByModule db (Text.drop 4 q)
        if null facts
          then putStrLn "No facts found for module."
          else putStrLn $ "Found " ++ show (length facts) ++ " fact(s) in module."
      else do
        defs <- findDefinitions db q
        if null defs
          then putStrLn "No definitions found."
          else do
            putStrLn $ "Found " ++ show (length defs) ++ " definition(s):"
            mapM_ printDef defs

runStats :: StatsOptions -> IO ()
runStats options = do
  let config = (defaultDbConfig (stDbPath options))
        { dbReadOnly = True
        , dbCreate   = False
        }
  withStorage config $ \(db :: RocksDB) -> do
    props <- properties db
    stats <- predicateStats db
    putStrLn $ "Database: " ++ stDbPath options
    putStrLn $ "  Version:    " ++ show (propVersion     props)
    putStrLn $ "  First ID:   " ++ show (propFirstId     props)
    putStrLn $ "  Next ID:    " ++ show (propFirstFreeId props)
    putStrLn $ "  Facts:      " ++ show (propFactCount   props)
    putStrLn $ "  Predicates: " ++ show (length stats)

-- ── Display helpers ───────────────────────────────────────────────────────────

printDef :: DefinitionFact -> IO ()
printDef d = putStrLn $
  "  " ++ Text.unpack (defName d) ++
  " [" ++ Text.unpack (defModule d) ++ "]" ++
  " line " ++ show (posLine (spanStart (defSpan d)))

printRef :: ReferenceFact -> IO ()
printRef r = putStrLn $
  "  " ++ Text.unpack (refName r) ++
  " [" ++ Text.unpack (refModule r) ++ "]" ++
  " line " ++ show (posLine (spanStart (refSpan r))) ++
  maybe "" (\t -> " -> " ++ Text.unpack t) (refTarget r)

-- ── Main ──────────────────────────────────────────────────────────────────────

main :: IO ()
main = do
  cmd <- execParser opts
  result <- catch (run cmd >> return True)
    (\e -> do
      hPutStrLn stderr $ "Error: " ++ displayException (e :: SomeException)
      return False)
  if result
    then return ()
    else exitFailure

run :: Command -> IO ()
run (Index options) = runIndex options
run (Query options) = runQuery options
run (Stats options) = runStats options