packages feed

prune-juice (empty) → 0.1

raw patch · 10 files changed

+514/−0 lines, 10 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, containers, directory, file-path-th, filepath, hpack, hspec, megaparsec, mtl, optparse-applicative, prune-juice, text, turtle, yaml

Files

+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2021 Dan Fithian++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ app/main.hs view
@@ -0,0 +1,61 @@+import Prelude++import Control.Applicative (many)+import Control.Monad (unless)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.State (execStateT, put)+import Data.Foldable (for_, traverse_)+import Data.Text (Text, pack, unpack)+import Data.Traversable (for)+import System.Exit (ExitCode(ExitFailure, ExitSuccess), exitWith)+import qualified Data.Set as Set+import qualified Options.Applicative as Opt++import Data.Prune.Dependency (getDependencyByModule)+import Data.Prune.ImportParser (getCompilableUsedDependencies)+import Data.Prune.Package (parseStackYaml)+import qualified Data.Prune.Types as T++data Opts = Opts+  { optsStackYamlFile :: FilePath+  , optsPackages :: [Text]+  }++parseArgs :: IO Opts+parseArgs = Opt.execParser (Opt.info (Opt.helper <*> parser) $ Opt.progDesc "Prune a Stack project's dependencies")+  where+    parser = Opts+      <$> Opt.strOption (+        Opt.long "stack-yaml-file"+          <> Opt.metavar "STACK_YAML_FILE"+          <> Opt.help "Location of stack.yaml"+          <> Opt.value "stack.yaml"+          <> Opt.showDefault )+      <*> many ( pack <$> Opt.strOption (+        Opt.long "package"+          <> Opt.metavar "PACKAGE"+          <> Opt.help "Package name(s)" ) )+++main :: IO ()+main = do+  Opts {..} <- parseArgs+  packages <- parseStackYaml optsStackYamlFile optsPackages++  dependencyByModule <- liftIO $ getDependencyByModule optsStackYamlFile packages+  code <- flip execStateT ExitSuccess $ for_ packages $ \T.Package {..} -> do+    baseUsedDependencies <- fmap mconcat . for packageCompilables $ \compilable@T.Compilable {..} -> do+      usedDependencies <- liftIO $ getCompilableUsedDependencies dependencyByModule compilable+      let (baseUsedDependencies, otherUsedDependencies) = Set.partition (flip Set.member packageBaseDependencies) usedDependencies+          otherUnusedDependencies = Set.difference compilableDependencies otherUsedDependencies+      unless (Set.null otherUnusedDependencies) $ do+        liftIO . putStrLn . unpack $ "Some unused dependencies for " <> pack (show compilableType) <> " " <> T.unCompilableName compilableName <> " in package " <> packageName+        traverse_ (liftIO . putStrLn . unpack . ("  " <>) . T.unDependencyName) $ Set.toList otherUnusedDependencies+        put $ ExitFailure 1+      pure baseUsedDependencies+    let baseUnusedDependencies = Set.difference packageBaseDependencies baseUsedDependencies+    unless (Set.null baseUnusedDependencies) $ do+      liftIO . putStrLn . unpack $ "Some unused base dependencies for package " <> packageName+      liftIO . traverse_ (putStrLn . unpack . ("  " <>) . T.unDependencyName) $ Set.toList baseUnusedDependencies+      put $ ExitFailure 1+  exitWith code
+ prune-juice.cabal view
@@ -0,0 +1,99 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: c82d607d1298918a330352ff35cee89ea7edd8f5b4321866f33d55b7558eb1c7++name:           prune-juice+version:        0.1+synopsis:       Prune unused Haskell dependencies+description:    Prune unused Haskell dependencies from a Stack/Hpack project+category:       Development+maintainer:     Dan Fithian+copyright:      2020 Dan Fithian+license:        MIT+license-file:   LICENSE+build-type:     Simple++library+  exposed-modules:+      Data.Prune.Dependency+      Data.Prune.ImportParser+      Data.Prune.Package+      Data.Prune.Types+  other-modules:+      Paths_prune_juice+  hs-source-dirs:+      src+  default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric DerivingStrategies DerivingVia EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings QuasiQuotes RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators ViewPatterns+  ghc-options: -Wall -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog+  build-depends:+      aeson+    , base <5.0+    , bytestring+    , containers+    , directory+    , filepath+    , hpack+    , megaparsec+    , mtl+    , text+    , turtle+    , yaml+  default-language: Haskell2010++executable prune-juice+  main-is: main.hs+  other-modules:+      Paths_prune_juice+  hs-source-dirs:+      app+  default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric DerivingStrategies DerivingVia EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings QuasiQuotes RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators ViewPatterns+  ghc-options: -Wall -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog+  build-depends:+      aeson+    , base <5.0+    , bytestring+    , containers+    , directory+    , filepath+    , hpack+    , megaparsec+    , mtl+    , optparse-applicative+    , prune-juice+    , text+    , turtle+    , yaml+  default-language: Haskell2010++test-suite test+  type: exitcode-stdio-1.0+  main-is: main.hs+  other-modules:+      Data.Prune.ImportParserSpec+      Data.Prune.PackageSpec+      Paths_prune_juice+  hs-source-dirs:+      test+  default-extensions: ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric DerivingStrategies DerivingVia EmptyDataDecls FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings QuasiQuotes RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeOperators ViewPatterns+  ghc-options: -Wall -fwarn-tabs -fwarn-redundant-constraints -Wincomplete-uni-patterns -eventlog+  build-depends:+      aeson+    , base <5.0+    , bytestring+    , containers+    , directory+    , file-path-th+    , filepath+    , hpack+    , hspec+    , megaparsec+    , mtl+    , prune-juice+    , text+    , turtle+    , yaml+  default-language: Haskell2010
+ src/Data/Prune/Dependency.hs view
@@ -0,0 +1,49 @@+module Data.Prune.Dependency where++import Prelude hiding (words)++import Data.Foldable (find)+import Data.Map (Map)+import Data.Maybe (mapMaybe)+import Data.Text (Text, isPrefixOf, pack, strip, unpack, words)+import Data.Traversable (for)+import System.Exit (ExitCode(ExitFailure, ExitSuccess))+import Turtle (shellStrict)+import qualified Data.Map as Map+import qualified Data.Set as Set++import Data.Prune.ImportParser (parseExposedModules)+import qualified Data.Prune.Types as T++runOrFail :: Text -> IO Text+runOrFail cmd = shellStrict cmd mempty >>= \case+  (ExitSuccess, out) -> pure out+  (ExitFailure _, out) -> fail . unpack $ "Failed to \"" <> cmd <> "\" due to " <> out++getDependencyByModule :: FilePath -> [T.Package] -> IO (Map T.ModuleName T.DependencyName)+getDependencyByModule stackYamlFile packages = do+  let allDependencies = foldMap T.packageBaseDependencies packages <> foldMap T.compilableDependencies (foldMap T.packageCompilables packages)+      tupleDependency x = (, x) <$> find (\d -> isPrefixOf (T.unDependencyName d) x) allDependencies+  compilerBin <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --compiler-bin")+  snapshotPkgDb <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --snapshot-pkg-db")+  globalPkgDb <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --global-pkg-db")+  localPkgDb <- strip <$> runOrFail ("stack --stack-yaml " <> pack stackYamlFile <> " path --local-pkg-db")+  let snapshotGhcPkg = compilerBin <> "/ghc-pkg --package-db " <> snapshotPkgDb+      globalGhcPkg = compilerBin <> "/ghc-pkg --package-db " <> globalPkgDb+      localGhcPkg = compilerBin <> "/ghc-pkg --package-db " <> localPkgDb+  snapshotPkgs <- mapMaybe tupleDependency . words . strip+    <$> runOrFail (snapshotGhcPkg <> " list --simple-output")+  globalPkgs <- mapMaybe tupleDependency . words . strip+    <$> runOrFail (globalGhcPkg <> " list --simple-output")+  localPkgs <- mapMaybe tupleDependency . words . strip+    <$> runOrFail (localGhcPkg <> " list --simple-output")+  snapshotDependencyByModule <- fmap mconcat . for snapshotPkgs $ \(dependencyName, pkg) -> do+    moduleNames <- parseExposedModules . unpack . strip =<< runOrFail (snapshotGhcPkg <> " field " <> pkg <> " exposed-modules")+    pure . Map.fromList . map (, dependencyName) . Set.toList $ moduleNames+  globalDependencyByModule <- fmap mconcat . for globalPkgs $ \(dependencyName, pkg) -> do+    moduleNames <- parseExposedModules . unpack . strip =<< runOrFail (globalGhcPkg <> " field " <> pkg <> " exposed-modules")+    pure . Map.fromList . map (, dependencyName) . Set.toList $ moduleNames+  localDependencyByModule <- fmap mconcat . for localPkgs $ \(dependencyName, pkg) -> do+    moduleNames <- parseExposedModules . unpack . strip =<< runOrFail (localGhcPkg <> " field " <> pkg <> " exposed-modules")+    pure . Map.fromList . map (, dependencyName) . Set.toList $ moduleNames+  pure $ snapshotDependencyByModule <> globalDependencyByModule <> localDependencyByModule
+ src/Data/Prune/ImportParser.hs view
@@ -0,0 +1,72 @@+module Data.Prune.ImportParser where++import Prelude++import Control.Applicative ((<|>), optional, some)+import Control.Monad (void)+import Data.List (isPrefixOf)+import Data.Map (Map)+import Data.Set (Set)+import Data.Text (pack)+import Data.Traversable (for)+import Data.Void (Void)+import Text.Megaparsec (Parsec, between, oneOf, parse)+import Text.Megaparsec.Char (alphaNumChar, char, space, string, symbolChar)+import qualified Data.Map as Map+import qualified Data.Set as Set++import qualified Data.Prune.Types as T++type Parser = Parsec Void String++padded :: Parser a -> Parser a+padded = between space space++quoted :: Parser a -> Parser a+quoted = between (ptoken "\"") (ptoken "\"")++ptoken :: String -> Parser String+ptoken = padded . string++operator :: Parser String+operator = concat <$> sequence [ptoken "(", symbolChars, ptoken ")"]++symbolChars :: Parser String+symbolChars = some (oneOf ("!#$%&*+./<=>?@^|-~:\\" :: String)) <|> some symbolChar++symbol :: Parser String+symbol = padded $ operator <|> some (alphaNumChar <|> oneOf ("._'" :: String))++pkgName :: Parser String+pkgName = some (alphaNumChar <|> char '-')++oneImport :: Parser T.ModuleName+oneImport = void (string "import") *> space+  *> optional (void (string "qualified") *> space)+  *> optional (void (padded (quoted pkgName)) *> space)+  *> (T.ModuleName . pack <$> (symbol <* space))++exposedModules :: Parser (Set T.ModuleName)+exposedModules = void (string "exposed-modules:") *> space+  *> (Set.fromList <$> some (T.ModuleName . pack <$> symbol))++parseFileImports :: FilePath -> IO (Set T.ModuleName)+parseFileImports fp = do+  either (fail . ("Failed to parse imports due to " <>) . show) (pure . Set.fromList) . traverse (parse oneImport fp) . filter (isPrefixOf "import ") . lines+    =<< readFile fp++parseExposedModules :: String -> IO (Set T.ModuleName)+parseExposedModules input =+  if null input+    then pure mempty+    else either (\e -> fail $ "Failed to parse exposed modules due to " <> show e <> " original input " <> input) pure $ parse exposedModules "" input++getUsedDependencies :: Map T.ModuleName T.DependencyName -> Set T.ModuleName -> Set T.DependencyName+getUsedDependencies dependencyByModule = foldr go mempty . Set.toList+  where+    go next acc = acc <> maybe mempty Set.singleton (Map.lookup next dependencyByModule)++getCompilableUsedDependencies :: Map T.ModuleName T.DependencyName -> T.Compilable -> IO (Set T.DependencyName)+getCompilableUsedDependencies dependencyByModule T.Compilable {..} = fmap mconcat . for (Set.toList compilableFiles) $ \fp -> do+  moduleNames <- parseFileImports fp+  pure $ getUsedDependencies dependencyByModule moduleNames
+ src/Data/Prune/Package.hs view
@@ -0,0 +1,73 @@+module Data.Prune.Package where++import Prelude++import Data.Map (Map)+import Data.Set (Set)+import Data.Text (Text, pack)+import Data.Traversable (for)+import Hpack.Config+  ( Section, decodeOptionsTarget, decodeResultPackage, defaultDecodeOptions, packageBenchmarks+  , packageConfig, packageExecutables, packageInternalLibraries, packageLibrary, packageName+  , packageTests, readPackageConfig, sectionDependencies, sectionSourceDirs, unDependencies+  )+import System.Directory (doesDirectoryExist, listDirectory, pathIsSymbolicLink)+import System.FilePath.Posix ((</>), isExtensionOf)+import qualified Data.ByteString as BS+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Data.Yaml as Yaml++import qualified Data.Prune.Types as T++listFilesRecursive :: FilePath -> IO (Set FilePath)+listFilesRecursive dir = do+  dirs <- listDirectory dir+  fmap mconcat . for dirs $ \case+    -- don't include "hidden" directories, i.e. those that start with a '.'+    '.' : _ -> pure mempty+    fn -> do+      let+        path = if dir == "." then fn else dir </> fn+      isDir <- doesDirectoryExist path+      isSymlink <- pathIsSymbolicLink path+      case (isSymlink, isDir) of+        (True, _) -> pure mempty+        (_, True) -> listFilesRecursive path+        _ -> pure $ Set.singleton path++getSectionDependencyNames :: Section a -> Set T.DependencyName+getSectionDependencyNames = Set.fromList . map (T.DependencyName . pack) . Map.keys . unDependencies . sectionDependencies++getSectionFiles :: FilePath -> Section a -> IO (Set FilePath)+getSectionFiles fp section = fmap mconcat . for (sectionSourceDirs section) $ \dir -> do+  allFiles <- listFilesRecursive $ fp </> dir+  pure $ Set.filter (isExtensionOf "hs") allFiles++getSectionCompilables :: FilePath -> T.CompilableType -> Set T.DependencyName -> Map String (Section a) -> IO [T.Compilable]+getSectionCompilables fp typ baseDependencies sections = for (Map.toList sections) $ \(name, section) -> do+  sourceFiles <- getSectionFiles fp section+  pure $ T.Compilable (T.CompilableName (pack name)) typ (Set.difference (getSectionDependencyNames section) baseDependencies) sourceFiles++parsePackageYaml :: FilePath -> IO T.Package+parsePackageYaml fp = do+  package <- either fail (pure . decodeResultPackage) =<< readPackageConfig (defaultDecodeOptions { decodeOptionsTarget = fp </> packageConfig })+  let baseDependencies = maybe mempty getSectionDependencyNames $ packageLibrary package+  libraries         <- getSectionCompilables fp T.CompilableTypeLibrary    baseDependencies $ maybe mempty (Map.singleton (packageName package)) $ packageLibrary package+  internalLibraries <- getSectionCompilables fp T.CompilableTypeLibrary    baseDependencies $ packageInternalLibraries package+  executables       <- getSectionCompilables fp T.CompilableTypeExecutable baseDependencies $ packageExecutables package+  tests             <- getSectionCompilables fp T.CompilableTypeTest       baseDependencies $ packageTests package+  benchmarks        <- getSectionCompilables fp T.CompilableTypeBenchmark  baseDependencies $ packageBenchmarks package+  pure T.Package+    { packageName = pack $ packageName package+    , packageBaseDependencies = baseDependencies+    , packageCompilables = libraries <> internalLibraries <> executables <> tests <> benchmarks+    }++parseStackYaml :: FilePath -> [Text] -> IO [T.Package]+parseStackYaml stackYamlFile packages = do+  T.StackYaml {..} <- either (fail . ("Couldn't parse stack.yaml due to " <>) . show) pure . Yaml.decodeEither' =<< BS.readFile stackYamlFile+  rawPackages <- traverse parsePackageYaml stackYamlPackages+  if null packages+    then pure rawPackages+    else pure $ filter (flip elem packages . T.packageName) rawPackages
+ src/Data/Prune/Types.hs view
@@ -0,0 +1,56 @@+module Data.Prune.Types where++import Prelude++import Data.Aeson ((.:), FromJSON, parseJSON, withObject)+import Data.Set (Set)+import Data.Text (Text)++data CompilableType+  = CompilableTypeLibrary+  | CompilableTypeExecutable+  | CompilableTypeTest+  | CompilableTypeBenchmark+  deriving (Eq, Ord)++instance Show CompilableType where+  show = \case+    CompilableTypeLibrary -> "library"+    CompilableTypeExecutable -> "executable"+    CompilableTypeTest -> "test"+    CompilableTypeBenchmark -> "benchmark"++newtype CompilableName = CompilableName { unCompilableName :: Text }+  deriving (Eq, Ord, Show)++data DependencyName = DependencyName { unDependencyName :: Text }+  deriving (Eq, Ord, Show)++data ModuleName = ModuleName { unModuleName :: Text }+  deriving (Eq, Ord, Show)++data Compilable = Compilable+  { compilableName :: CompilableName+  , compilableType :: CompilableType+  , compilableDependencies :: Set DependencyName+  , compilableFiles :: Set FilePath+  }+  deriving (Eq, Ord, Show)++data Package = Package+  { packageName :: Text+  , packageBaseDependencies :: Set DependencyName+  , packageCompilables :: [Compilable]+  }+  deriving (Eq, Ord, Show)++data StackYaml = StackYaml+  { stackYamlPackages :: [FilePath]+  -- ^ FIXME not every package is this way.+  }+  deriving (Eq, Ord, Show)++instance FromJSON StackYaml where+  parseJSON = withObject "StackYaml" $ \obj ->+    StackYaml+      <$> obj .: "packages"
+ test/Data/Prune/ImportParserSpec.hs view
@@ -0,0 +1,46 @@+module Data.Prune.ImportParserSpec where++import Prelude++import Test.Hspec (Spec, describe, it, shouldBe)+import Text.Megaparsec (parse)+import qualified Data.Set as Set++import qualified Data.Prune.Types as T++-- the module being tested+import Data.Prune.ImportParser++spec :: Spec+spec = describe "Data.Prune.ImportParser" $ do+  it "should parse wildcards" $ do+    parse oneImport "" "import Foo.Bar" `shouldBe` Right (T.ModuleName "Foo.Bar")++  it "should parse imports" $ do+    parse oneImport "" "import Foo.Bar (foo, bar)" `shouldBe` Right (T.ModuleName "Foo.Bar")++  it "should parse qualified" $ do+    parse oneImport "" "import qualified Foo.Bar as Baz" `shouldBe` Right (T.ModuleName "Foo.Bar")++  it "should parse package imports" $ do+    parse oneImport "" "import \"foobar\" Foo.Bar ()" `shouldBe` Right (T.ModuleName "Foo.Bar")++  it "should parse something complicated" $ do+    parse oneImport "" "import   qualified   \"foobar\"   Foo.Bar   as   Baz (foo, bar) " `shouldBe` Right (T.ModuleName "Foo.Bar")++  it "should parse exposed modules - single line" $ do+    parse exposedModules "" "exposed-modules: Hpack Hpack.Config Hpack.Render Hpack.Yaml"+      `shouldBe` Right (Set.fromList [T.ModuleName "Hpack", T.ModuleName "Hpack.Config", T.ModuleName "Hpack.Render", T.ModuleName "Hpack.Yaml"])++  it "should parse exposed modules - multiline" $ do+    parse exposedModules "" "exposed-modules: Text.Megaparsec Text.Megaparsec.Byte\n\+                                              \Text.Megaparsec.Byte.Lexer Text.Megaparsec.Char\n\+                                              \Text.Megaparsec.Char.Lexer Text.Megaparsec.Debug\n\+                                              \Text.Megaparsec.Error Text.Megaparsec.Error.Builder\n\+                                              \Text.Megaparsec.Internal Text.Megaparsec.Pos Text.Megaparsec.Stream"+      `shouldBe` Right ( Set.fromList+        [ T.ModuleName "Text.Megaparsec", T.ModuleName "Text.Megaparsec.Byte"+        , T.ModuleName "Text.Megaparsec.Byte.Lexer", T.ModuleName "Text.Megaparsec.Char"+        , T.ModuleName "Text.Megaparsec.Char.Lexer", T.ModuleName "Text.Megaparsec.Debug"+        , T.ModuleName "Text.Megaparsec.Error", T.ModuleName "Text.Megaparsec.Error.Builder"+        , T.ModuleName "Text.Megaparsec.Internal", T.ModuleName "Text.Megaparsec.Pos", T.ModuleName "Text.Megaparsec.Stream" ] )
+ test/Data/Prune/PackageSpec.hs view
@@ -0,0 +1,26 @@+module Data.Prune.PackageSpec where++import Prelude++import Data.Foldable (find)+import Data.List (isSuffixOf)+import System.FilePath.TH (fileRelativeToAbsolute)+import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)+import qualified Data.Set as Set++import qualified Data.Prune.Types as T++-- the module being tested+import Data.Prune.Package++spec :: Spec+spec = describe "Data.Prune.Package" $ do+  let stackYamlFile = $(fileRelativeToAbsolute "../../../stack.yaml")+  it "parses stack.yaml" $ do+    [T.Package {..}] <- parseStackYaml stackYamlFile []+    packageName `shouldBe` "prune-juice"+    packageBaseDependencies `shouldSatisfy` Set.member (T.DependencyName "base")+    T.Compilable {..} <- maybe (fail "No compilable with the name \"test\"") pure $ find ((==) (T.CompilableName "test") . T.compilableName) packageCompilables+    compilableType `shouldBe` T.CompilableTypeTest+    compilableDependencies `shouldSatisfy` not . Set.null+    compilableFiles `shouldSatisfy` not . Set.null . Set.filter (isSuffixOf "PackageSpec.hs")
+ test/main.hs view
@@ -0,0 +1,11 @@+import Prelude++import Test.Hspec (hspec)++import qualified Data.Prune.ImportParserSpec+import qualified Data.Prune.PackageSpec++main :: IO ()+main = hspec $ do+  Data.Prune.ImportParserSpec.spec+  Data.Prune.PackageSpec.spec