packages feed

now-haskell-0.1.0.0: src/CabalScanner.hs

module CabalScanner where

import Data.Aeson
import qualified Data.Text as T

-- Cabal things
import Distribution.Verbosity (normal)
import Distribution.PackageDescription
import Distribution.PackageDescription.Configuration (flattenPackageDescription) -- TODO replace with finalizePD
import Distribution.PackageDescription.Parsec
import Distribution.Types.UnqualComponentName
import Distribution.ModuleName (ModuleName, fromComponents)
import Distribution.Simple.Utils

import Data.List (inits)
import System.Directory
import System.Environment
import System.FilePath
import qualified ModuleScanner as Mod

data Output
  = PackageUsedOutput PackageInfo
  | ModuleUsedOutput ModuleInfo

instance ToJSON Output where
  toJSON o = case o of
    PackageUsedOutput p -> object
      [ "type" .= ("package" :: T.Text)
      , "data" .= p
      ]
    ModuleUsedOutput m -> object
      [ "type" .= ("module" :: T.Text)
      , "data" .= m
      ]

data ModulePackage
  = Dangling
  | InPackageLibrary FilePath
  | InPackageExecutable
    String -- ^ Executable name
    FilePath
  | InOtherPackageStanza FilePath

data ModuleInfo = ModuleInfo
  { moduleInfoPackage :: ModulePackage
  , moduleInfoAnalysis :: Mod.Analyze
  }

instance ToJSON ModuleInfo where
  toJSON (ModuleInfo p a) = object
    [ "package" .= case p of
        Dangling -> Null
        InPackageLibrary fp -> object ["library" .= (takeBaseName fp), "path" .= fp]
        InPackageExecutable e fp -> object ["executable" .= e, "path" .= fp]
        InOtherPackageStanza fp -> object ["path" .= fp]
    , "analysis" .= a
    ]

data PackageInfo = PackageInfo
  { packageInfoExecutables :: [String]
  }

instance ToJSON PackageInfo where
  toJSON (PackageInfo exes) = object
    [ "executables" .= exes
    {-
      From cabal:
        executables[].modulePath :: FilePath
        resolve libraries[].modules with buildInfo
        dataFiles :: [FilePath]
        dataDir :: FilePath
        extraSrcFiles :: [FilePath]
        extraTmpFiles :: [FilePath]
        extraDocFiles :: [FilePath]
    -}
    , "files" .= object
      [ "executableModules" .= ([] :: [FilePath])
      ]
    ]

dispatchFileScan :: FilePath -> IO (Either ModuleInfo PackageInfo)
dispatchFileScan fp = case takeFileName fp of
  "package.yml" -> error "hpack package.yml"
  "package.yaml" -> error "hpack package.yaml"
  other -> case takeExtension other of
    ".hs" -> Left <$> performHaskellModuleScan fp
    ".lhs" -> Left <$> performHaskellModuleScan fp
    -- TODO
    -- "chs" -> _
    ".cabal" -> Right <$> (produceFlatPackage fp >>= performCabalPackageScan)
    unsupported -> error ("." ++ unsupported ++ " file extension is not a supported Haskell builder type.")

performHaskellModuleScan :: FilePath -> IO ModuleInfo
performHaskellModuleScan relFp = do
  fp <- canonicalizePath relFp
  mCabalFile <- findClosestAncestorCabalFile fp
  analysis <- Mod.readDesiredModule relFp

  case mCabalFile of
    Nothing -> do
      pure $ ModuleInfo Dangling analysis
    Just cf -> do
      flatPack <- produceFlatPackage cf
      case library flatPack of
        Nothing -> do
          pure $ ModuleInfo (InOtherPackageStanza cf) analysis
        Just l -> do
          let relativeModulePath = makeRelative (takeDirectory cf) fp
              desiredModule = fileToModule (hsSourceDirs $ libBuildInfo l) relativeModulePath
          if desiredModule `isExposedFromLibrary` l
            then do
              pure $ ModuleInfo (InPackageLibrary cf) analysis
            else error "TODO: not sure what to do here..."

getCabalPackage :: FilePath -> IO PackageDescription
getCabalPackage p = do
  efp <- findPackageDesc p
  let fp = either error id efp
  produceFlatPackage fp

produceFlatPackage :: FilePath -> IO PackageDescription
produceFlatPackage fp = do
  gpd <- readGenericPackageDescription normal fp
  pure $ flattenPackageDescription gpd

performCabalPackageScan :: PackageDescription -> IO PackageInfo
performCabalPackageScan gpd = do
  let exes = map (unUnqualComponentName . exeName) $ executables gpd
  pure $ PackageInfo exes

-- Takes a path and searches for a cabal file up the directory hierarchy. returns a cabal file if found
findClosestAncestorCabalFile :: FilePath -> IO (Maybe FilePath)
findClosestAncestorCabalFile fp = do
  isDir <- doesDirectoryExist fp
  let startingDir = if isDir then fp else takeDirectory fp
  dir <- canonicalizePath startingDir
  let chunkedDir = splitPath dir
      allPaths = map joinPath $ reverse $ tail $ inits chunkedDir
  -- print allPaths
  go allPaths
  where
    go [] = pure Nothing
    go (path:ps) = do
      res <- findPackageDesc path
      case res of
        Left _ -> go ps
        Right p -> pure $ Just p


fileToModule
  :: [FilePath] -- Search paths
  -- -> [FilePath] -- Extensions
  -> FilePath -- Haskell File
  -> ModuleName
fileToModule searchPs startP = go searchPs
  where
    extensionless = dropExtension startP
    go [] = fromComponents $ splitDirectories extensionless
    go (p:ps) = let rel = makeRelative p extensionless in
      if rel == extensionless
      then go ps
      else fromComponents $ splitDirectories rel

isExposedFromLibrary :: ModuleName -> Library -> Bool
isExposedFromLibrary m l = m `elem` exposedModules l