diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,4 @@
+Changelog for Weeder
+
+0.1
+    Initial version
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Neil Mitchell 2017.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Neil Mitchell nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+# Weeder [![Hackage version](https://img.shields.io/hackage/v/weeder.svg?label=Hackage)](https://hackage.haskell.org/package/weeder) [![Stackage version](https://www.stackage.org/package/weeder/badge/lts?label=Stackage)](https://www.stackage.org/package/weeder) [![Linux Build Status](https://img.shields.io/travis/ndmitchell/weeder.svg?label=Linux%20build)](https://travis-ci.org/ndmitchell/weeder) [![Windows Build Status](https://img.shields.io/appveyor/ci/ndmitchell/weeder.svg?label=Windows%20build)](https://ci.appveyor.com/project/ndmitchell/weeder)
+
+The principle is to delete dead code (pulling up the weeds). To do that, run:
+
+* GHC with `-fwarn-unused-binds -fwarn-unused-imports`, which finds unused definitions and unused imports.
+* [HLint](https://github.com/ndmitchell/hlint#readme), looking for "Redundant extension" hints, which finds unused extensions.
+* This tool, `weeder .` which detects redundant `build-depends` in the `.cabal` and functions that are exported internally but not available outside this library.
+
+To use `weeder` your code must be building with `stack`, as it piggy-backs off some files `stack` generates. If you don't normally build with `stack` a simple `stack init && weeder . --build` is likely to be sufficient.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Cabal.hs b/src/Cabal.hs
new file mode 100644
--- /dev/null
+++ b/src/Cabal.hs
@@ -0,0 +1,125 @@
+{-# LANGUAGE ViewPatterns, RecordWildCards #-}
+
+module Cabal(
+    Cabal(..), CabalSection(..), CabalSectionType,
+    parseCabal,
+    selectCabalFile,
+    selectHiFiles
+    ) where
+
+import System.IO.Extra
+import System.Directory.Extra
+import System.FilePath
+import qualified Data.HashMap.Strict as Map
+import Util
+import Data.Maybe
+import Data.List.Extra
+import Data.Tuple.Extra
+import Data.Either.Extra
+import Data.Monoid
+import Prelude
+
+
+selectCabalFile :: FilePath -> IO FilePath
+selectCabalFile dir = do
+    xs <- listFiles dir
+    case filter ((==) ".cabal" . takeExtension) xs of
+        [x] -> return x
+        _ -> fail $ "Didn't find exactly 1 cabal file in " ++ dir
+
+-- | Return the (exposed Hi files, internal Hi files)
+selectHiFiles :: Map.HashMap FilePath a -> CabalSection -> ([a], [a], [ModuleName])
+selectHiFiles his sect@CabalSection{..} = (external, internal, bad1++bad2)
+    where
+        (bad1, external) = partitionEithers $
+            [findHi his sect $ Left cabalMainIs | cabalMainIs /= ""] ++
+            [findHi his sect $ Right x | x <- cabalExposedModules]
+        (bad2, internal) = partitionEithers
+            [findHi his sect $ Right x | x <- filter (not . isPathsModule) cabalOtherModules]
+
+        findHi :: Map.HashMap FilePath a -> CabalSection -> Either FilePath ModuleName -> Either ModuleName a
+        findHi his cabal@CabalSection{..} name = maybe (Left mname) Right $ firstJust (`Map.lookup` his) poss
+            where
+                mname = either takeFileName id name
+                poss = [ normalise $ joinPath (root : x : either (return . dropExtension) (splitOn ".") name) <.> "dump-hi"
+                    | root <- ["build" </> x </> (x ++ "-tmp") | Just x <- [cabalSectionTypeName cabalSectionType]] ++ ["build"]
+                    , x <- if null cabalSourceDirs then ["."] else cabalSourceDirs]
+
+
+data Cabal = Cabal
+    {cabalName :: PackageName
+    ,cabalSections :: [CabalSection]
+    } deriving Show
+
+instance Monoid Cabal where
+    mempty = Cabal "" []
+    mappend (Cabal x1 x2) (Cabal y1 y2) = Cabal (x1?:y1) (x2++y2)
+
+data CabalSectionType = Library | Executable String | TestSuite String | Benchmark String
+    deriving (Eq,Ord)
+
+cabalSectionTypeName :: CabalSectionType -> Maybe String
+cabalSectionTypeName Library = Nothing
+cabalSectionTypeName (Executable x) = Just x
+cabalSectionTypeName (TestSuite x) = Just x
+cabalSectionTypeName (Benchmark x) = Just x
+
+instance Show CabalSectionType where
+    show Library = "library"
+    show (Executable x) = "exe:" ++ x
+    show (TestSuite x) = "test:" ++ x
+    show (Benchmark x) = "bench:" ++ x
+
+instance Read CabalSectionType where
+    readsPrec _ "library" = [(Library,"")]
+    readsPrec _ x
+        | Just x <- stripPrefix "exe:" x = [(Executable x, "")]
+        | Just x <- stripPrefix "test:" x = [(TestSuite x, "")]
+        | Just x <- stripPrefix "bench:" x = [(Benchmark x, "")]
+    readsPrec _ _ = []
+
+data CabalSection = CabalSection
+    {cabalSectionType :: CabalSectionType
+    ,cabalMainIs :: FilePath
+    ,cabalExposedModules :: [ModuleName]
+    ,cabalOtherModules :: [ModuleName]
+    ,cabalSourceDirs :: [FilePath]
+    ,cabalPackages :: [PackageName]
+    } deriving Show
+
+instance Monoid CabalSection where
+    mempty = CabalSection Library "" [] [] [] []
+    mappend (CabalSection x1 x2 x3 x4 x5 x6) (CabalSection y1 y2 y3 y4 y5 y6) =
+        CabalSection x1 (x2?:y2) (x3<>y3) (x4<>y4) (x5<>y5) (x6<>y6)
+
+
+parseCabal :: FilePath -> IO Cabal
+parseCabal = fmap parseTop . readFile'
+
+parseTop = mconcat . map f . parseHanging . filter (not . isComment) . lines
+    where
+        isComment = isPrefixOf "--" . trimStart
+        keyName = (lower *** fst . word1) . word1
+
+        f (keyName -> (key, name), xs) = case key of
+            "name:" -> mempty{cabalName=name}
+            "library" -> mempty{cabalSections=[parseSection Library xs]}
+            "executable" -> mempty{cabalSections=[parseSection (Executable name) xs]}
+            "test-suite" -> mempty{cabalSections=[parseSection (TestSuite name) xs]}
+            "benchmark" -> mempty{cabalSections=[parseSection (Benchmark name) xs]}
+            _ -> mempty
+
+parseSection typ xs =
+    mempty{cabalSectionType=typ} <>
+    mconcat (map f $ parseHanging xs)
+    where
+        keyValues (x,xs) = let (x1,x2) = word1 x in (lower x1, filter (not . null) $ map trim $ x2:xs)
+        listModules = concatMap (wordsBy (`elem` " ,"))
+
+        f (keyValues -> (k,vs)) = case k of
+            "build-depends:" -> mempty{cabalPackages = map (trim . takeWhile (`notElem` "=><")) . splitOn "," $ unwords vs}
+            "hs-source-dirs:" -> mempty{cabalSourceDirs=vs}
+            "exposed-modules:" -> mempty{cabalExposedModules=listModules vs}
+            "other-modules:" -> mempty{cabalOtherModules=listModules vs}
+            "main-is:" -> mempty{cabalMainIs=head $ vs ++ [""]}
+            _ -> mempty
diff --git a/src/Check.hs b/src/Check.hs
new file mode 100644
--- /dev/null
+++ b/src/Check.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE RecordWildCards, NamedFieldPuns, ScopedTypeVariables #-}
+
+module Check(check) where
+
+import Hi
+import Cabal
+import Util
+import Data.List.Extra
+import Data.Tuple.Extra
+import qualified Data.HashSet as Set
+import qualified Data.HashMap.Strict as Map
+import Warning
+
+
+data S = S
+    {pkg :: PackageName
+    ,hi :: HiKey -> Hi
+    ,sections :: [(CabalSection, ([HiKey], [HiKey]))]
+    }
+
+check :: (HiKey -> Hi) -> PackageName -> [(CabalSection, ([HiKey], [HiKey], [ModuleName]))] -> [Warning]
+check hi pkg sections2 = map (\x -> x{warningSections = sort $ warningSections x}) $
+    warnReusedModuleBetweenSections s ++
+    warnRedundantPackageDependency s ++
+    warnIncorrectOtherModules s ++
+    warnUnusedExport s ++
+    warnNotCompiled pkg sections2
+    where
+        s = S{..}
+        sections = map (second $ \(a,b,c) -> let aa = nubOrd a in (aa,nubOrd b \\ aa)) sections2
+
+
+warnNotCompiled :: PackageName -> [(CabalSection, ([HiKey], [HiKey], [ModuleName]))] -> [Warning]
+warnNotCompiled pkg xs =
+    [ Warning pkg [cabalSectionType s] "Module not compiled" Nothing (Just m) Nothing
+    | (s, (_, _, missing)) <- xs, m <- missing]
+
+
+warnReusedModuleBetweenSections :: S -> [Warning]
+warnReusedModuleBetweenSections S{..} =
+    [ Warning pkg ss "Module reused between components" Nothing (Just $ hiModuleName $ hi m) Nothing
+    | (m, ss) <- groupSort [(x, cabalSectionType c) | (c, (x1,x2)) <- sections, x <- x1++x2]
+    , length ss > 1]
+
+
+warnRedundantPackageDependency :: S -> [Warning]
+warnRedundantPackageDependency S{..} =
+    [ Warning pkg [cabalSectionType] "Redundant build-depends entry" (Just p) Nothing Nothing
+    | (CabalSection{..}, (x1,x2)) <- sections
+    , let usedPackages = Set.unions $ map (hiImportPackage . hi) $ x1 ++ x2
+    , p <- Set.toList $ Set.fromList cabalPackages `Set.difference` usedPackages]
+
+
+warnIncorrectOtherModules :: S -> [Warning]
+warnIncorrectOtherModules S{..} = concat
+    [ [Warning pkg [cabalSectionType] "Missing other-modules entry" Nothing (Just m) Nothing | m <- Set.toList missing] ++
+      [Warning pkg [cabalSectionType] "Excessive other-modules entry" Nothing (Just m) Nothing | m <- Set.toList excessive]
+    | (CabalSection{..}, (external, internal)) <- sections
+    , let imports = Map.fromList [(hiModuleName, Set.map identModule hiImportIdent) | Hi{..} <- map hi $ external ++ internal]
+    , let missing =  Set.filter (not . isPathsModule) $
+                     Set.unions (Map.elems imports) `Set.difference`
+                     Set.fromList (Map.keys imports)
+    , let excessive = Set.fromList (map (hiModuleName . hi) internal) `Set.difference`
+                      reachable (\k -> maybe [] Set.toList $ Map.lookup k imports) (map (hiModuleName . hi) external)
+    ]
+
+
+warnUnusedExport :: S -> [Warning]
+warnUnusedExport S{..} =
+    [ Warning pkg ss "Weeds exported" Nothing (Just $ hiModuleName $ hi m) (Just i)
+    | (m,(ss,is)) <- Map.toList unused, i <- Set.toList is]
+    where
+        unionsWith f = foldr (Map.unionWith f) Map.empty
+        -- important: for an identifer to be unused, it must be unused in all sections that use that key
+        unused = unionsWith (\(s1,i1) (s2,i2) -> (s1++s2, i1 `Set.intersection` i2))
+                 [ Map.fromList [(k, ([cabalSectionType], Set.fromList $ Map.lookupDefault [] (hiModuleName $ hi k) bad)) | k <- internal ++ external]
+                 | (CabalSection{..}, (external, internal)) <- sections
+                 , let bad = Map.fromListWith (++) $ map (identModule &&& return . identName) $ notUsedOrExposed (map hi external) (map hi internal)]
+
+notUsedOrExposed :: [Hi] -> [Hi] -> [Ident]
+notUsedOrExposed external internal = Set.toList $
+        privateAPI `Set.difference` Set.unions [publicAPI,supported,usedAnywhere]
+    where
+        modules = Map.fromList [(hiModuleName x, x) | x <- external ++ internal]
+
+        -- things exported from this package
+        publicAPI = Set.unions $ map hiExportIdent external
+
+        -- Types that are required to define things that are public
+        supported = Set.unions
+            [ Map.lookupDefault Set.empty x hiSignatures
+            | (m, xs) <- groupSort $ map (identModule &&& identName) $ Set.toList $ Set.union publicAPI usedAnywhere
+            , Just Hi{..} <- [Map.lookup m modules], x <- xs]
+
+        -- things that are defined in other modules and exported
+        -- (ignoring field name since they provide handy documentation)
+        privateAPI = Set.unions
+            [ Set.filter ((==) hiModuleName . identModule) $ hiExportIdent `Set.difference` hiFieldName
+            | Hi{..} <- internal]
+
+        -- things that are used anywhere, if someone imports and exports something
+        -- assume that isn't also a use (find some redundant warnings)
+        usedAnywhere = Set.unions
+            [ hiImportIdent `Set.difference` hiExportIdent
+            | Hi{..} <- external ++ internal]
diff --git a/src/CmdLine.hs b/src/CmdLine.hs
new file mode 100644
--- /dev/null
+++ b/src/CmdLine.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# OPTIONS_GHC -fno-warn-missing-fields -fno-cse -O0 #-}
+
+module CmdLine(
+    Cmd(..), getCmd
+    ) where
+
+import System.Console.CmdArgs.Implicit
+import Paths_weeder
+import Data.Version
+import Data.Functor
+import Prelude
+
+
+data Cmd = Cmd
+    {cmdProjects :: [FilePath]
+    ,cmdBuild :: Bool
+    ,cmdTest :: Bool
+    ,cmdMatch :: Bool
+    ,cmdJson :: Bool
+    ,cmdYaml :: Bool
+    ,cmdShowAll :: Bool
+    } deriving (Show, Data, Typeable)
+
+getCmd :: IO Cmd
+getCmd = automatic <$> cmdArgsRun mode
+
+automatic :: Cmd -> Cmd
+automatic cmd
+    | cmdTest cmd = cmd{cmdTest=False,cmdProjects=["test"],cmdBuild=True,cmdMatch=True}
+    | null $ cmdProjects cmd = cmd{cmdProjects=["."]}
+    | otherwise = cmd 
+
+mode :: Mode (CmdArgs Cmd)
+mode = cmdArgsMode $ Cmd
+    {cmdProjects = def &= args &= typ "DIR"
+    ,cmdBuild = nam "build" &= help "Build the project first"
+    ,cmdTest = nam "test" &= help "Run the test suite"
+    ,cmdMatch = nam "match" &= help "Make the .weeder.yaml perfectly match"
+    ,cmdJson = nam "json" &= help "Output JSON"
+    ,cmdYaml = nam "yaml" &= help "Output YAML"
+    ,cmdShowAll = nam "show-all" &= help "Show even ignored warnings"
+    } &= explicit &= name "weeder" &= verbosity
+    &= summary ("Weeder v" ++ showVersion version ++ ", (C) Neil Mitchell 2017")
+    where
+        nam xs = def &= explicit &= name xs &= name [head xs]
diff --git a/src/Hi.hs b/src/Hi.hs
new file mode 100644
--- /dev/null
+++ b/src/Hi.hs
@@ -0,0 +1,116 @@
+{-# LANGUAGE DeriveGeneric, RecordWildCards, GeneralizedNewtypeDeriving #-}
+
+module Hi(
+    HiKey(), Hi(..), Ident(..),
+    hiParseDirectory
+    ) where
+
+import qualified Data.HashSet as Set
+import qualified Data.HashMap.Lazy as Map
+import System.FilePath
+import System.Directory.Extra
+import GHC.Generics
+import Data.Tuple.Extra
+import Control.Monad
+import Data.Char
+import Data.Hashable
+import Data.List.Extra
+import Data.Monoid
+import Data.Functor
+import Util
+import System.IO.Extra
+import Prelude
+
+data Ident = Ident {identModule :: ModuleName, identName :: IdentName}
+    deriving (Show,Eq,Ord,Generic)
+instance Hashable Ident
+
+data Hi = Hi
+    {hiModuleName :: ModuleName
+        -- ^ Module name
+    ,hiImportPackage :: Set.HashSet PackageName
+        -- ^ Packages imported by this module
+    ,hiExportIdent :: Set.HashSet Ident
+        -- ^ Identifiers exported by this module
+    ,hiImportIdent :: Set.HashSet Ident
+        -- ^ Identifiers used by this module
+    ,hiSignatures :: Map.HashMap IdentName (Set.HashSet Ident)
+        -- ^ Type signatures of functions defined in this module and the types they refer to
+    ,hiFieldName :: Set.HashSet Ident
+        -- ^ Things that are field names
+    } deriving (Show,Eq,Generic)
+instance Hashable Hi
+
+instance Monoid Hi where
+    mempty = Hi mempty mempty mempty mempty mempty mempty
+    mappend x y = Hi
+        {hiModuleName = f (?:) hiModuleName
+        ,hiImportPackage = f (<>) hiImportPackage
+        ,hiExportIdent = f (<>) hiExportIdent
+        ,hiImportIdent = f (<>) hiImportIdent
+        ,hiSignatures = f (Map.unionWith (<>)) hiSignatures
+        ,hiFieldName = f (<>) hiFieldName
+        }
+        where f op sel = sel x `op` sel y
+
+-- | Don't expose that we're just using the filename internally
+newtype HiKey = HiKey FilePath deriving (Eq,Ord,Hashable)
+
+hiParseDirectory :: FilePath -> IO (Map.HashMap FilePath HiKey, Map.HashMap HiKey Hi)
+hiParseDirectory dir = do
+    files <- filter ((==) ".dump-hi" . takeExtension) <$> listFilesRecursive dir
+    his <- forM files $ \file -> do
+        src <- readFile' file
+        return (drop (length dir + 1) file, trimSignatures $ hiParseContents src)
+    -- here we try and dedupe any identical Hi modules
+    let keys = Map.fromList $ map (second HiKey . swap) his
+    let mp1 = Map.fromList $ map (second (keys Map.!)) his
+    let mp2 = Map.fromList $ map swap $ Map.toList keys
+    return (mp1, mp2)
+
+-- note that in some cases we may get more/less internal signatures, so first remove them
+trimSignatures :: Hi -> Hi
+trimSignatures hi@Hi{..} = hi{hiSignatures = Map.filterWithKey (\k _ -> k `Set.member` names) hiSignatures}
+    where names = Set.fromList [s | Ident m s <- Set.toList hiExportIdent, m == hiModuleName]
+
+hiParseContents :: String -> Hi
+hiParseContents = mconcat . map f . parseHanging .  lines
+    where
+        f (x,xs)
+            | Just x <- stripPrefix "interface " x = mempty{hiModuleName = parseInterface x}
+            | Just x <- stripPrefix "exports:" x = mconcat $ map parseExports xs
+            | Just x <- stripPrefix "package dependencies:" x = mempty{hiImportPackage = Set.fromList $ map parsePackDep $ concatMap words $ x:xs}
+            | Just x <- stripPrefix "import " x = case xs of
+                [] -> mempty -- these are imports of modules from another package, we don't know what is actually used
+                xs -> mempty{hiImportIdent = Set.fromList $ map (Ident (words x !! 1) . fst . word1) $ dropWhile ("exports:" `isPrefixOf`) xs}
+            | length x == 32, all isHexDigit x,
+                (y,ys):_ <- parseHanging xs,
+                fun:"::":typ <- concatMap (wordsBy (`elem` ",()[]{} ")) $ y:ys,
+                not $ "$" `isPrefixOf` fun =
+                mempty{hiSignatures = Map.singleton fun $ Set.fromList $ map parseIdent typ}
+            | otherwise = mempty
+
+        -- "old-locale-1.0.0.7@old-locale-1.0.0.7-KGBP1BSKxH5GCm0LnZP04j" -> "old-locale"
+        parsePackDep = intercalate "-" . takeWhile (any isAlpha) . wordsBy (== '-') . takeWhile (/= '@')
+
+        -- "hlint-1.9.41-IPKy9tGF1918X9VRp9DMhp:HSE.All 8002" -> "HSE.All"
+        parseInterface = drop 1 . snd . breakOn ":" . fst . word1
+
+        -- "Apply.applyHintFile"
+        -- "Language.Haskell.PPHsMode{Language.Haskell.PPHsMode caseIndent}
+        -- Return the identifiers and the fields. Fields are never qualified but everything else is.
+        parseExports x = mempty
+            {hiExportIdent = Set.fromList $ y : [Ident (a ?: identModule y) b | Ident a b <- ys]
+            ,hiFieldName = Set.fromList [Ident (identModule y) b | Ident "" b <- ys]
+            ,hiSignatures = Map.fromList [(b, Set.singleton y) | Ident _ b <- ys, b /= identName y]
+            }
+            where y:ys = map parseIdent $ wordsBy (`elem` "|{} ") x
+
+        -- "Language.Haskell.PPHsMode" -> Ident "Language.Haskell" "PPHsMode"
+        parseIdent x
+            | isHaskellSymbol $ last x =
+                let (a,b) = spanEnd isHaskellSymbol x
+                in if null a then Ident "" b else Ident a $ tail b
+            | otherwise =
+                let (a,b) = breakOnEnd "." x
+                in Ident (if null a then "" else init a) b
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE TupleSections, RecordWildCards, ScopedTypeVariables #-}
+
+module Main(main) where
+
+import Hi
+import Cabal
+import Stack
+import Data.List.Extra
+import Data.Functor
+import Data.Tuple.Extra
+import Control.Monad.Extra
+import System.Exit
+import System.IO.Extra
+import qualified Data.HashMap.Strict as Map
+import System.Directory.Extra
+import System.FilePath
+import Check
+import Warning
+import CmdLine
+import Prelude
+
+
+data Result = Good | Bad deriving Eq
+
+main :: IO ()
+main = do
+    cmd@Cmd{..} <- getCmd
+    res <- mapM (weedDirectory cmd) cmdProjects
+    when (Bad `elem` res) exitFailure
+
+
+weedDirectory :: Cmd -> FilePath -> IO Result
+weedDirectory Cmd{..} dir = do
+    file <- do b <- doesDirectoryExist dir; return $ if b then dir </> "stack.yaml" else dir
+    when cmdBuild $ buildStack file
+    Stack{..} <- parseStack file
+    cabals <- forM stackPackages $ \x -> do
+        file <- selectCabalFile $ dir </> x
+        (file,) <$> parseCabal file
+
+    ignore <- do
+        let x = takeDirectory file </> ".weeder.yaml"
+        b <- doesFileExist x
+        if not b then return [] else readWarningsFile x
+    let quiet = cmdJson || cmdYaml
+
+    res <- forM cabals $ \(cabalFile, Cabal{..}) -> do
+        (fileToKey, keyToHi) <- hiParseDirectory $ takeDirectory cabalFile </> stackDistDir
+        let full = check (keyToHi Map.!) cabalName $
+                   map (id &&& selectHiFiles fileToKey) cabalSections
+        let warn = if cmdShowAll || cmdMatch then full else ignoreWarnings ignore full
+        unless quiet $
+            putStrLn $ unlines $ showWarningsPretty cabalName warn
+        return (length full - length warn, warn)
+    let (ignored, warns) = sum *** concat $ unzip res
+
+    when cmdJson $ putStrLn $ showWarningsJson warns
+    when cmdYaml $ putStrLn $ showWarningsYaml warns
+    if cmdMatch then
+        if sort ignore == sort warns then do
+            putStrLn "Warnings match"
+            return Good
+        else do
+            putStrLn "MISSING WARNINGS"
+            putStrLn $ unlines $ showWarningsPretty "" $ ignore \\ warns
+            putStrLn "EXTRA WARNINGS"
+            putStrLn $ unlines $ showWarningsPretty "" $ warns \\ ignore
+            return Bad
+     else do
+        when (ignored > 0 && not quiet) $
+            putStrLn $ "Ignored " ++ show ignored ++ " weeds (pass --show-all to see them)"
+        return $ if null warns then Good else Bad
diff --git a/src/Stack.hs b/src/Stack.hs
new file mode 100644
--- /dev/null
+++ b/src/Stack.hs
@@ -0,0 +1,38 @@
+{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
+
+module Stack(Stack(..), parseStack, buildStack) where
+
+import Data.Yaml
+import Data.List.Extra
+import Control.Exception
+import qualified Data.Text as T
+import qualified Data.HashMap.Strict as Map
+import qualified Data.Vector as V
+import System.Process
+import Data.Functor
+import Prelude
+
+
+data Stack = Stack
+    {stackPackages :: [FilePath]
+    ,stackDistDir :: FilePath
+    }
+
+buildStack :: FilePath -> IO ()
+buildStack file = callProcess "stack" ["build","--stack-yaml=" ++ file,"--test","--bench","--no-run-tests","--no-run-benchmarks"]
+
+-- | Note that in addition to parsing the stack.yaml file it also runs @stack@ to
+--   compute the dist-dir.
+parseStack :: FilePath -> IO Stack
+parseStack file = do
+    stackDistDir <- fst . line1 <$> readCreateProcess (proc "stack" ["path","--dist-dir","--stack-yaml=" ++ file]) ""
+    stackPackages <- either throwIO (return . f) =<< decodeFileEither file
+    return Stack{..}
+    where
+        fromObject (Object x) = x
+        fromArray (Array xs) = V.toList xs
+        fromString (String s) = T.unpack s
+
+        f x = case Map.lookup "packages" $ fromObject x of
+            Nothing -> ["."]
+            Just xs -> map fromString $ fromArray xs
diff --git a/src/Util.hs b/src/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Util.hs
@@ -0,0 +1,59 @@
+
+module Util(
+    PackageName, ModuleName, IdentName,
+    parseHanging,
+    (?:),
+    isHaskellSymbol,
+    reachable,
+    isPathsModule
+    ) where
+
+import Data.Char
+import Data.Monoid
+import Data.Hashable
+import Data.List.Extra
+import Data.Tuple.Extra
+import qualified Data.HashSet as Set
+import Prelude
+
+
+type PackageName = String
+type ModuleName = String
+type IdentName = String
+
+
+-- | Return the first non-empty argument in a left-to-right manner
+(?:) :: (Eq a, Monoid a) => a -> a -> a
+a ?: b = if a == mempty then b else a
+
+-- | Parse a hanging lines of lines.
+parseHanging :: [String] -> [(String, [String])]
+parseHanging = repeatedly (\(x:xs) -> first (\a -> (x, unindent a)) $ span (\x -> null x || " " `isPrefixOf` x) xs)
+
+unindent :: [String] -> [String]
+unindent xs = map (drop n) xs
+    where
+        n = minimum $ top : map f xs
+        f x = let (a,b) = span isSpace x in if null b then top else length a
+        top = 1000
+
+-- | Is the character a member of possible Haskell symbol characters,
+--   according to the Haskell report.
+isHaskellSymbol :: Char -> Bool
+isHaskellSymbol x =
+    x `elem` "!#$%&*+./<=>?@\\^|-~" ||
+    (isSymbol x && x `notElem` "\"'_(),;[]`{}")
+
+
+-- | Given a list of mappings, and an initial set, find which items can be reached
+reachable :: (Eq k, Hashable k) => (k -> [k]) -> [k] -> Set.HashSet k
+reachable follow = f Set.empty
+    where
+        f done [] = done
+        f done (x:xs)
+            | x `Set.member` done = f done xs
+            | otherwise = f (Set.insert x done) $ follow x ++ xs
+
+-- | Is a given module name the specially generated cabal Paths_foo module
+isPathsModule :: ModuleName -> Bool
+isPathsModule = isPrefixOf "Paths_"
diff --git a/src/Warning.hs b/src/Warning.hs
new file mode 100644
--- /dev/null
+++ b/src/Warning.hs
@@ -0,0 +1,132 @@
+{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-}
+
+module Warning(
+    Warning(..),
+    showWarningsPretty,
+    showWarningsYaml,
+    showWarningsJson,
+    readWarningsFile,
+    ignoreWarnings
+    ) where
+
+import Cabal
+import Util
+import Data.Maybe
+import Data.List.Extra
+import Control.Exception
+import Data.Aeson as JSON
+import Data.Yaml as Yaml
+import qualified Data.Vector as V
+import qualified Data.Text as T
+import qualified Data.HashMap.Strict as Map
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy.Char8 as LBS
+
+
+data Warning = Warning
+    {warningPackage :: String
+    ,warningSections :: [CabalSectionType]
+    ,warningMessage :: String
+    ,warningDepends :: Maybe PackageName
+    ,warningModule :: Maybe ModuleName
+    ,warningIdentifier :: Maybe IdentName
+    } deriving (Show,Eq,Ord)
+
+warningLabels = ["package","section","message","depends","module","identifier"]
+
+warningPath :: Warning -> [Maybe String]
+warningPath Warning{..} =
+    [Just warningPackage
+    ,Just $ unwords $ map show warningSections
+    ,Just warningMessage
+    ,warningDepends
+    ,warningModule
+    ,warningIdentifier]
+
+warningUnpath :: [String] -> Warning
+warningUnpath [pkg,sect,msg,deps,mod,ident] = Warning
+    pkg (map read $ words sect) msg
+    (f deps) (f mod) (f ident)
+    where f s = if null s then Nothing else Just s
+
+showWarningsPretty :: PackageName -> [Warning] -> [String]
+showWarningsPretty pkg [] = ["= Package " ++ pkg ++ " =","No warnings"]
+showWarningsPretty _ warn = warningTree
+    ([\x -> "= Package " ++ x ++ " =",\x -> "\n== Section " ++ x ++ " ==",id,("* "++),("  - "++)] ++ repeat id) $
+    map (catMaybes . warningPath) warn
+
+warningTree :: Ord a => [a -> a] -> [[a]] -> [a]
+warningTree (f:fs) xs = concat
+    [ f title : warningTree fs inner
+    | (title,inner) <- groupSort $ mapMaybe uncons xs]
+
+
+-- (section, name, children)
+data Val = Val String String [Val]
+         | End String [String]
+
+valToValue :: [Val] -> Value
+valToValue = Array . V.fromList . map f
+    where
+        pair k v = Object $ Map.singleton (T.pack k) v
+        f (Val sect name xs) = pair sect $ Array $ V.fromList $
+            pair "name" (String $ T.pack name) : map f xs
+        f (End sect [x]) = pair sect $ String $ T.pack x
+        f (End sect xs) = pair sect $ Array $ V.fromList $ map (String . T.pack) xs
+
+valueToVal :: Value -> [Val]
+valueToVal = f
+    where
+        f (Array xs) = concatMap f $ V.toList xs
+        f (Object mp) | [(k,v)] <- Map.toList mp = return $ case v of
+            v | Just (n, rest) <- findName v -> Val (T.unpack k) (T.unpack n) $ f rest
+            Array xs -> End (T.unpack k) $ map (T.unpack . fromString) $ V.toList xs
+            String x -> End (T.unpack k) [T.unpack x]
+        fromString (String x) = x
+
+        findName (Array xs)
+            | ([name], rest) <- partition (isJust . fromName) $ V.toList xs
+            = Just (fromJust $ fromName name, Array $ V.fromList rest)
+        findName _ = Nothing
+
+        fromName (Object mp) | [(k,String v)] <- Map.toList mp, T.unpack k == "name" = Just v
+        fromName _ = Nothing
+
+showWarningsValue :: [Warning] -> Value
+showWarningsValue = valToValue . f warningLabels . map (dropWhileEnd isNothing . warningPath)
+    where
+        f (name:names) xs
+            | all (\x -> length x <= 1) xs = [End name $ sort [x | [Just x] <- xs]]
+            | otherwise = concat
+                [ case a of
+                    Nothing -> f names b
+                    Just a -> [Val name a $ f names b]
+                | (a,b) <- groupSort $ mapMaybe uncons xs]
+
+showWarningsJson :: [Warning] -> String
+showWarningsJson = LBS.unpack . JSON.encode . showWarningsValue
+
+showWarningsYaml :: [Warning] -> String
+showWarningsYaml = BS.unpack . Yaml.encode . showWarningsValue
+
+
+readWarningsFile :: FilePath -> IO [Warning]
+readWarningsFile file = do
+    x <- either throwIO return =<< Yaml.decodeFileEither file
+    return $ map warningUnpath $ concatMap (f warningLabels) $ valueToVal x
+    where
+        f :: [String] -> Val -> [[String]]
+        f names (End sect ns) = concatMap (\n -> f names $ Val sect n []) ns
+        f (name:names) val@(Val sect n xs)
+            | sect == name = if null xs
+                then [n : replicate (length names) ""]
+                else map (n:) $ concatMap (f names) xs
+            | otherwise = map ("":) $ f names val
+
+
+-- | Ignore all found warnings that are covered by a template
+ignoreWarnings :: [Warning] -> [Warning] -> [Warning]
+ignoreWarnings template = filter (\x -> not $ any (`match` x) template)
+    where
+        unpack = map (fromMaybe "") . warningPath
+        match template found = and $ zipWith (\t f -> t == "" || t == f) (unpack template) (unpack found)
diff --git a/weeder.cabal b/weeder.cabal
new file mode 100644
--- /dev/null
+++ b/weeder.cabal
@@ -0,0 +1,50 @@
+cabal-version:      >= 1.18
+build-type:         Simple
+name:               weeder
+version:            0.1
+license:            BSD3
+license-file:       LICENSE
+category:           Development
+author:             Neil Mitchell <ndmitchell@gmail.com>
+maintainer:         Neil Mitchell <ndmitchell@gmail.com>
+copyright:          Neil Mitchell 2017
+synopsis:           Detect dead code
+description:
+    Find redundant package dependencies or redundant module exports.
+homepage:           https://github.com/ndmitchell/weeder#readme
+bug-reports:        https://github.com/ndmitchell/weeder/issues
+extra-doc-files:
+    README.md
+    CHANGES.txt
+tested-with:        GHC==8.0.2, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3
+
+source-repository head
+    type:     git
+    location: https://github.com/ndmitchell/weeder.git
+
+executable weeder
+    default-language:   Haskell2010
+    main-is:            Main.hs
+    hs-source-dirs:     src
+    build-depends:
+        base == 4.*,
+        text,
+        unordered-containers,
+        yaml,
+        vector,
+        hashable,
+        filepath,
+        cmdargs,
+        aeson,
+        yaml,
+        bytestring,
+        process >= 1.2.3.0,
+        extra
+    other-modules:
+        Cabal
+        Hi
+        Stack
+        Util
+        Check
+        Warning
+        CmdLine
