weeder 1.0.9 → 2.0.0
raw patch · 19 files changed
+769/−1082 lines, 19 filesdep +algebraic-graphsdep +containersdep +dhalldep −aesondep −cmdargsdep −deepseqdep ~basedep ~bytestringdep ~directorysetup-changednew-uploader
Dependencies added: algebraic-graphs, containers, dhall, generic-lens, ghc, lens, mtl, optparse-applicative, regex-tdfa, transformers
Dependencies removed: aeson, cmdargs, deepseq, extra, foundation, hashable, process, semigroups, text, unordered-containers, vector, yaml
Dependency ranges changed: base, bytestring, directory, filepath
Files
- CHANGELOG.md +5/−0
- CHANGES.txt +0/−64
- LICENSE +2/−1
- README.md +78/−49
- Setup.hs +0/−2
- exe-weeder/Main.hs +2/−0
- src/Cabal.hs +0/−154
- src/Check.hs +0/−128
- src/CmdLine.hs +0/−49
- src/Hi.hs +0/−156
- src/Main.hs +0/−11
- src/Stack.hs +0/−48
- src/Str.hs +0/−28
- src/Util.hs +0/−109
- src/Warning.hs +0/−150
- src/Weeder.hs +388/−74
- src/Weeder/Config.hs +37/−0
- src/Weeder/Main.hs +205/−0
- weeder.cabal +52/−59
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# 2.0.0.0++Weeder 2.0 is a ground up rewrite of Weeder using `.hie` files. It is now+maintained by Ollie Charles (@ocharles on GitHub).+
− CHANGES.txt
@@ -1,64 +0,0 @@-Changelog for Weeder--1.0.9, released 2020-06-11- #55, fix handling of internal libraries-1.0.8, released 2018-08-26- #42, make paths case-insensitive on MacOS-1.0.7, released 2018-08-23- Don't warn on base as it is used by Paths_ modules- #42, make --verbose print out the version number- #41, make the --help output clear you can pass a stack.yaml-1.0.6, released 2018-06-16- Don't fail with an error if stack setup is necessary- If you fail to find stack.yaml give a better error message-1.0.5, released 2018-05-05- #39, provide weeder as a library-1.0.4, released 2018-05-02- #38, make sure you parse bracketed version ranges properly-1.0.3, released 2018-03-04- #35, support ^>= operator in Cabal-1.0.2, released 2018-03-01- Add lower bounds for Yaml and Aeson-1.0.1, released 2018-02-23- #34, support -any for version numbers-1.0, released 2018-01-22- #30, bump the version number to 1.0-0.1.13, released 2018-01-17- #32, find .hi files in more places- #32, always disable color when running stack-0.1.12, released 2018-01-16- Make available on Mac-0.1.11, released 2017-12-29- #29, deal with case-insensitive FilePath on Windows-0.1.10, released 2017-12-28- Make --verbose print out the directory when running commands- Don't report semigroups as unused on any platforms-0.1.9, released 2017-12-07- Don't report Win32/unix as unused on the alternate platform-0.1.8, released 2017-12-06- Follow both branches for if/else containing dependencies/modules-0.1.7, released 2017-08-09- #21, detect dependencies that are only required transitively- #13, respect the STACK_YAML environment variable- #20, add verbosity messages in a lot of places- #15, tone down unused import if exporting a cross-package type- #11, optimise execution speed (~3x faster)-0.1.6, released 2017-06-18- #10, find files generated by alex/happy-0.1.5, released 2017-06-02- If --yaml and no hints give no output-0.1.4, released 2017-05-27- #9, allow --dist-dir to set the stack dist-dir- Deal with operators including | in them- Allow arrays of arrays of strings in the .weeder.yaml-0.1.3, released 2017-05-08- #5, document how to install weeder- #8, detect unused imports, even import Foo()- #7, don't say modules with only instances are always redundant- #6, don't give partial pattern matches when reading .weeder.yaml-0.1.2, released 2017-04-29- #3, deal with space-separated hs-source-dirs-0.1.1, released 2017-04-29- #2, use "stack query" rather than parsing stack.yaml-0.1, released 2017-04-28- Initial version
LICENSE view
@@ -1,4 +1,5 @@-Copyright Neil Mitchell 2017-2020.+Copyright Oliver Charles 2020.+ All rights reserved. Redistribution and use in source and binary forms, with or without
README.md view
@@ -1,81 +1,110 @@-# Weeder [](https://hackage.haskell.org/package/weeder) [](https://www.stackage.org/package/weeder) [](https://travis-ci.org/ndmitchell/weeder) [](https://ci.appveyor.com/project/ndmitchell/weeder)+# Weeder -# Weeder has moved!+Weeder is an application to perform whole-program dead-code analysis. Dead code+is code that is written, but never reachable from any other code. Over the+lifetime of a project, this happens as code is added and removed, and leftover+code is never cleaned up. While GHC has warnings to detect dead code is a single+module, these warnings don't extend across module boundaries - this is where+Weeder comes in. -Weeder 2.0 is being developed at https://github.com/ocharles/weeder on different foundations. This repo is for historical reference only.+Weeder uses HIE files produced by GHC - these files can be thought of as source+code that has been enhanced by GHC, adding full symbol resolution and type+information. Weeder builds a dependency graph from these files to understand how+code interacts. Once all analysis is done, Weeder performs a traversal of this+graph from a set of roots (e.g., your `main` function), and determines which+code is reachable and which code is dead. --------------------+# Using Weeder -Most projects accumulate code over time. Weeder detects unused Haskell exports, allowing dead code to be removed (pulling up the weeds). Weeder piggy-backs off files generated by [`stack`](https://www.haskellstack.org), so first obtain stack, then:+## Preparing Your Code for Weeder -* Install `weeder` by running `stack install weeder --resolver=nightly`.-* Ensure your project has a `stack.yaml` file. If you don't normally build with `stack` then run `stack init` to generate one.-* Make sure you have `ghc-options: {"$locals": -ddump-to-file -ddump-hi}` in your `stack.yaml`.-* Run `weeder . --build`, which builds your project with `stack` and reports any weeds.+To use Weeder, you will need to generate `.hie` files from your source code. If+you use Cabal, this is easily done by adding one line to your+`cabal.project.local` file: -## What does Weeder detect?+``` cabal+package *+ ghc-options: -fwrite-ide-info+``` -Weeder detects a bunch of weeds, including:+Once this has been added, perform a full rebuild of your project: -* You export a function `helper` from module `Foo.Bar`, but nothing else in your package uses `helper`, and `Foo.Bar` is not an `exposed-module`. Therefore, the export of `helper` is a weed. Note that `helper` itself may or may not be a weed - once it is no longer exported `-fwarn-unused-binds` will tell you if it is entirely redundant.-* Your package `depends` on another package but doesn't use anything from it - the dependency should usually be deleted. This functionality is quite like [packunused](https://hackage.haskell.org/package/packunused), but implemented quite differently.-* Your package has entries in the `other-modules` field that are either unused (and thus should be deleted), or are missing (and thus should be added). The `stack` tool warns about the latter already.-* A source file is used between two different sections in a `.cabal` file - e.g. in both the library and the executable. Usually it's better to arrange for the executable to depend on the library, but sometimes that would unnecessarily pollute the interface. Useful to be aware of, and sometimes worth fixing, but not always.-* A file has not been compiled despite being mentioned in the `.cabal` file. This situation can be because the file is unused, or the `stack` compilation was incomplete. I recommend compiling both benchmarks and tests to avoid this warning where possible - running `weeder . --build` will use a suitable command line.+``` shell+cabal clean+cabal build all+``` -Beware of conditional compilation (e.g. `CPP` and the [Cabal `flag` mechanism](https://www.haskell.org/cabal/users-guide/developing-packages.html#configurations)), as these may mean that something is currently a weed, but in different configurations it is not.+## Calling Weeder -I recommend fixing the warnings relating to `other-modules` and files not being compiled first, as these may cause other warnings to disappear.+To call Weeder, you first need to provide a configuration file. Weeder uses+[Dhall](https://dhall-lang.org) as its configuration format, and configuration+files have the type: -## What else should I use?+``` dhall+{ roots : List Text, type-class-roots : Bool }+``` -Weeder detects dead exports, which can be deleted. To get the most code deleted from removing an export, use:+`roots` is a list of regular expressions of symbols that are considered as+alive. If you're building an executable, the pattern `^Main.main$` is a+good starting point - specifying that `main` is a root. -* GHC with `-fwarn-unused-binds -fwarn-unused-imports`, which finds unused definitions and unused imports in a module.-* [HLint](https://github.com/ndmitchell/hlint#readme), looking for "Redundant extension" hints, which finds unused extensions.-* [Unused](https://github.com/joshuaclayton/unused), which works at the level of `ctags` information, so can be used if you don't want to use `stack`, can't compile your code, or want to detect unused code between projects.+`type-class-roots` configures whether or not Weeder should consider anything in+a type class instance as a root. Weeder is currently unable to add dependency+edges into type class instances, and without this flag may produce false+positives. It's recommended to initially set this to `True`: -## Ignoring weeds+``` dhall+{ roots = [ "^Main.main$" ], type-class-roots = True }+``` -If you want your package to be detected as "weed free", but it has some weeds you know about but don't consider important, you can add a `.weeder.yaml` file adjacent to the `stack.yaml` with a list of exclusions. To generate an initial list of exclusions run `weeder . --yaml > .weeder.yaml`.+Now invoke the `weeder` executable, and - if your project has weeds - you will+see something like the following: -You may wish to generalise/simplify the `.weeder.yaml` by removing anything above or below the interesting part. As an example of the [`.weeder.yaml` file from `ghcid`](https://github.com/ndmitchell/ghcid/blob/master/.weeder.yaml):+``` shell+$ weeder -```yaml-- message: Module reused between components-- message:- - name: Weeds exported- - identifier: withWaiterPoll-```+src/Dhall/TH.hs:187:1: error: toDeclaration is unused -This configuration declares that I am not interested in the message about modules being reused between components (that's the way `ghcid` works, and I am aware of it). It also says that I am not concerned about `withWaiterPoll` being a weed - it's a simplified method of file change detection I use for debugging, so even though it's dead now, I sometimes do switch to it.+ 185 ┃ -> HaskellType (Expr s a)+ 186 ┃ -> Q Dec+ 187 ┃ toDeclaration haskellTypes MultipleConstructors{..} = do+ 188 ┃ case code of+ 189 ┃ Union kts -> do -## Running with Continuous Integration+ Delete this definition or add ‘Dhall.TH.toDeclaration’ as a root to fix this error. -Before running Weeder on your continuous integration (CI) server, you should first ensure there are no existing weeds. One way to achieve that is to ignore existing hints by running `weeder . --yaml > .weeder.yaml` and checking in the resulting `.weeder.yaml`. -On the CI you should then run `weeder .` (or `weeder . --build` to compile as well). To avoid the cost of compilation you may wish to fetch the [latest Weeder binary release](https://github.com/ndmitchell/weeder/releases/latest).+src/Dhall/TH.hs:106:1: error: toNestedHaskellType is unused -For the CI systems [Travis](https://travis-ci.org/), [Appveyor](https://www.appveyor.com/) and [Azure Pipelines](https://azure.microsoft.com/en-gb/services/devops/pipelines/) add the line:+ 104 ┃ -- ^ Dhall expression to convert to a simple Haskell type+ 105 ┃ -> Q Type+ 106 ┃ toNestedHaskellType haskellTypes = loop+ 107 ┃ where+ 108 ┃ loop dhallType = case dhallType of -```sh-curl -sSL https://raw.github.com/ndmitchell/weeder/master/misc/run.sh | sh -s .+ Delete this definition or add ‘Dhall.TH.toNestedHaskellType’ as a root to fix this error. ``` -The arguments after `-s` are passed to `weeder`, so modify the final `.` if you want other arguments. This command works on Windows, Mac and Linux.--## What about Cabal users?+(Please note these warnings are just for demonstration and not necessarily weeds+in the Dhall project). -Weeder requires the textual `.hi` file for each source file in the project. Stack generates that already, so it was easy to integrate in to. There's no reason that information couldn't be extracted by either passing flags to Cabal, or converting the `.hi` files afterwards. I welcome patches to do that integration.+# Limitations -## What about false positives?+Weeder currently has a few limitations: -Weeder strives to avoid incorrectly warning about something that is required, if you find such an instance please report it on [the issue tracker](https://github.com/ndmitchell/weeder/issues). Unfortunately there are some cases where there are still false positives, as GHC doesn't put enough information in the `.hi` files:+## Type Class Instances -**Data.Coerce** If you use `Data.Coerce.coerce` the constructors for the data type must be in scope, but if they aren't used anywhere other than automatically by `coerce` then Weeder will report unused imports. You can ignore such warnings by adding `- message: Unused import` to your `.weeder.yaml` file.+Weeder is not currently able to analyse whether a type class instance is used.+For this reason, Weeder adds all symbols referenced to from a type class+instance to the root set, keeping this code alive. In short, this means Weeder+might not detect dead code if it's used from a type class instance which is+never actually needed. -**Declaration QuasiQuotes** If you use a declaration-level quasi-quote then weeder won't see the use of the quoting function, potentially leading to an unused import warning, and marking the quoting function as a weed. The only solution is to ignore the entries with a `.weeder.yaml` file.+You can toggle whether Weeder consider type class instances as roots with the+`type-class-roots` configuration option. -**Stack extra-deps** Packages marked extra-deps in your `stack.yaml` will be weeded, due to a bug in [`stack`](https://github.com/commercialhaskell/stack/issues/3258). The only solution is to ignore the packages with a `.weeder.yaml` file.+## Template Haskell -**Linking to C functions** If a library provides C functions, and these are used directly from another library/executable, the library providing these functions may be marked as a redundant `build-depends`, see [more details](https://github.com/ndmitchell/weeder/issues/40).+Weeder is currently unable to parse the result of a Template Haskell splice. If+some Template Haskell code refers to other source code, this dependency won't be+tracked by Weeder, and thus Weeder might end up with false positives.
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
+ exe-weeder/Main.hs view
@@ -0,0 +1,2 @@+module Main ( main ) where+import Weeder.Main
− src/Cabal.hs
@@ -1,154 +0,0 @@-{-# 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.Char-import Data.Maybe-import Data.List.Extra-import Data.Tuple.Extra-import Data.Either.Extra-import Data.Semigroup-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, not found)-selectHiFiles :: FilePath -> Map.HashMap FilePathEq a -> CabalSection -> ([a], [a], [ModuleName])-selectHiFiles distDir 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 FilePathEq a -> CabalSection -> Either FilePath ModuleName -> Either ModuleName a- findHi his cabal@CabalSection{..} name =- -- error $ show (poss, Map.keys his)- maybe (Left mname) Right $ firstJust (`Map.lookup` his) poss- where- mname = either takeFileName id name- poss = map filePathEq $ possibleHi distDir cabalSourceDirs cabalSectionType $ either (return . dropExtension) (splitOn ".") name----- | This code is fragile and keeps going wrong, should probably try a less "guess everything"--- and a more refined filter and test.-possibleHi :: FilePath -> [FilePath] -> CabalSectionType -> [String] -> [FilePath]-possibleHi distDir sourceDirs sectionType components =- [ joinPath (root : x : components) <.> "dump-hi"- | extra <- [".",distDir]- , root <- concat [["build" </> extra </> x </> (x ++ "-tmp")- ,"build" </> extra </> x </> x- ,"build" </> extra </> x </> (x ++ "-tmp") </> distDir </> "build" </> x </> (x ++ "-tmp")]- | Just x <- [cabalSectionTypeName sectionType]] ++- ["build", "build" </> distDir </> "build"]- , x <- sourceDirs ++ ["."]]---data Cabal = Cabal- {cabalName :: PackageName- ,cabalSections :: [CabalSection]- } deriving Show--instance Semigroup Cabal where- Cabal x1 x2 <> Cabal y1 y2 = Cabal (x1?:y1) (x2++y2)--instance Monoid Cabal where- mempty = Cabal "" []- mappend = (<>)--data CabalSectionType = Library (Maybe String) | Executable String | TestSuite String | Benchmark String- deriving (Eq,Ord)--cabalSectionTypeName :: CabalSectionType -> Maybe String-cabalSectionTypeName (Library x) = x-cabalSectionTypeName (Executable x) = Just x-cabalSectionTypeName (TestSuite x) = Just x-cabalSectionTypeName (Benchmark x) = Just x--instance Show CabalSectionType where- show (Library Nothing) = "library"- show (Library (Just x)) = "library:" ++ x- show (Executable x) = "exe:" ++ x- show (TestSuite x) = "test:" ++ x- show (Benchmark x) = "bench:" ++ x--instance Read CabalSectionType where- readsPrec _ "library" = [(Library Nothing,"")]- readsPrec _ x- | Just x <- stripPrefix "exe:" x = [(Executable x, "")]- | Just x <- stripPrefix "test:" x = [(TestSuite x, "")]- | Just x <- stripPrefix "bench:" x = [(Benchmark x, "")]- | Just x <- stripPrefix "library:" x = [(Library (Just x), "")]- readsPrec _ _ = []--data CabalSection = CabalSection- {cabalSectionType :: CabalSectionType- ,cabalMainIs :: FilePath- ,cabalExposedModules :: [ModuleName]- ,cabalOtherModules :: [ModuleName]- ,cabalSourceDirs :: [FilePath]- ,cabalPackages :: [PackageName]- } deriving Show--instance Semigroup CabalSection where- 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)--instance Monoid CabalSection where- mempty = CabalSection (Library Nothing) "" [] [] [] []- mappend = (<>)--parseCabal :: FilePath -> IO Cabal-parseCabal = fmap parseTop . readFile'--parseTop = mconcatMap 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" -> case name of- "" -> mempty{cabalSections=[parseSection (Library Nothing) xs]}- x -> mempty{cabalSections=[parseSection (Library (Just x)) 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} <> parse xs- where- parse = mconcatMap f . parseHanging- keyValues (x,xs) = let (x1,x2) = word1 x in (lower x1, trimEqual $ filter (not . null) $ x2:xs)- trimEqual xs = map (drop n) xs- where n = minimum $ 0 : map (length . takeWhile isSpace) xs- listSplit = concatMap (wordsBy (`elem` " ,"))- isPackageNameChar x = isAlphaNum x || x == '-'- parsePackage = dropSuffix "-any" . takeWhile isPackageNameChar . trim-- f (keyValues -> (k,vs)) = case k of- "if" -> parse vs- "else" -> parse vs- "build-depends:" -> mempty{cabalPackages = map parsePackage . splitOn "," $ unwords vs}- "hs-source-dirs:" -> mempty{cabalSourceDirs=listSplit vs}- "exposed-modules:" -> mempty{cabalExposedModules=listSplit vs}- "other-modules:" -> mempty{cabalOtherModules=listSplit vs}- "main-is:" -> mempty{cabalMainIs=headDef "" vs}- _ -> mempty
− src/Check.hs
@@ -1,128 +0,0 @@-{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-}--module Check(check) where--import Hi-import Cabal-import Util-import Data.Maybe-import Data.List.Extra-import Data.Tuple.Extra-import System.Info.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], [ModuleName]))]- }--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 s ++- warnUnusedImport s- where- s = S{..}- sections = map (second $ \(a,b,c) -> let aa = nubOrd a in (aa,nubOrd b \\ aa,c)) sections2---warnNotCompiled :: S -> [Warning]-warnNotCompiled S{..} =- [ Warning pkg [cabalSectionType s] "Module not compiled" Nothing (Just m) Nothing- | (s, (_, _, missing)) <- sections, 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 (Set.map fst . hiImportPackageModule . hi) $ x1 ++ x2- , not $ "" `Set.member` usedPackages -- Sometimes we don't get the package name at all, e.g. https://gitlab.haskell.org/ghc/ghc/issues/16886- , p <- Set.toList $ Set.fromList cabalPackages `Set.difference` usedPackages- , p /= if isWindows then "unix" else "Win32" -- ignore packages that must be conditional on the other platform- , p /= "semigroups" -- ignore packages that are often conditional- , p /= "base" -- used by Paths_ modules which we have thrown away by this point- ]---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, hiImportModule) | 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)- ]----- Primarily looking for import Foo() where Foo is not an orphan-warnUnusedImport :: S -> [Warning]-warnUnusedImport S{..} =- [ Warning pkg [cabalSectionType] "Unused import" Nothing (Just $ hiModuleName mod) (Just $ hiModuleName imp)- | (CabalSection{..}, (external, internal,_)) <- sections- , let mods = Map.fromList $ map ((hiModuleName &&& id) . hi) $ external ++ internal- , mod <- Map.elems mods- , imp <- mapMaybe (`Map.lookup` mods) $ Set.toList $- hiImportModule mod `Set.difference`- (Set.map identModule (hiImportIdent mod) `Set.union` hiImportOrphan mod)- , Set.null $ hiImportIdent mod `Set.intersection` hiExportIdent imp -- reexporting for someone else- , Set.null $ Set.map snd (hiImportPackageModule mod) `Set.intersection` Set.map identModule (hiExportIdent imp) -- reexporting for another package- , Set.null $ Set.map identModule (Set.filter (isHaskellCtor . identName) $ hiExportIdent imp) `Set.difference` Set.insert (hiModuleName imp) (hiImportModule imp) -- reexport a type for another package, see #15- ]--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]
− src/CmdLine.hs
@@ -1,49 +0,0 @@-{-# 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 System.Environment-import Prelude---data Cmd = Cmd- {cmdProjects :: [FilePath]- ,cmdBuild :: Bool- ,cmdTest :: Bool- ,cmdMatch :: Bool- ,cmdJson :: Bool- ,cmdYaml :: Bool- ,cmdShowAll :: Bool- ,cmdDistDir :: Maybe String- } deriving (Show, Data, Typeable)--getCmd :: [String] -> IO Cmd-getCmd args = withArgs args $ 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 OR stack.yaml"- ,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"- ,cmdDistDir = nam "dist-dir" &= typDir &= help "Stack dist-dir, defaults to 'stack path --dist-dir'"- } &= explicit &= verbosity- &= name "weeder" &= program "weeder" &= summary ("Weeder v" ++ showVersion version ++ ", (C) Neil Mitchell 2017-2020")- where- nam xs = def &= explicit &= name xs &= name [head xs]
− src/Hi.hs
@@ -1,156 +0,0 @@-{-# LANGUAGE DeriveGeneric, RecordWildCards, GeneralizedNewtypeDeriving, OverloadedStrings #-}--module Hi(- HiKey(), Hi(..), Ident(..),- hiParseDirectory- ) where--import qualified Data.HashSet as Set-import qualified Data.HashMap.Lazy as Map-import System.Console.CmdArgs.Verbosity-import System.FilePath-import System.Directory.Extra-import System.Time.Extra-import GHC.Generics-import Data.Tuple.Extra-import Data.Maybe-import Control.Monad-import Control.Exception-import Control.DeepSeq-import Data.Char-import Data.Hashable-import Data.List.Extra-import Data.Semigroup-import Data.Functor-import Util-import qualified Str as S-import System.IO.Extra-import Prelude--data Ident = Ident {identModule :: ModuleName, identName :: IdentName}- deriving (Show,Eq,Ord,Generic)-instance Hashable Ident-instance NFData 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- ,hiImportModule :: Set.HashSet ModuleName- -- ^ Modules imported and used by this module- -- Normally equivalent to @Set.map identModule hiImportIdent@, unless a module supplies only instances- ,hiImportOrphan :: Set.HashSet ModuleName- -- ^ Orphans that are in scope in this module- ,hiImportPackageModule :: Set.HashSet (PackageName, ModuleName)- -- ^ Modules imported from other packages- ,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 NFData Hi--instance Semigroup Hi where- x <> y = Hi- {hiModuleName = f (?:) hiModuleName- ,hiImportPackage = f (<>) hiImportPackage- ,hiExportIdent = f (<>) hiExportIdent- ,hiImportIdent = f (<>) hiImportIdent- ,hiImportModule = f (<>) hiImportModule- ,hiImportPackageModule = f (<>) hiImportPackageModule- ,hiImportOrphan = f (<>) hiImportOrphan- ,hiSignatures = f (Map.unionWith (<>)) hiSignatures- ,hiFieldName = f (<>) hiFieldName- }- where f op sel = sel x `op` sel y--instance Monoid Hi where- mempty = Hi mempty mempty mempty mempty mempty mempty mempty mempty mempty- mappend = (<>)---- | Don't expose that we're just using the filename internally-newtype HiKey = HiKey FilePathEq deriving (Eq,Ord,Hashable)--hiParseDirectory :: FilePath -> IO (Map.HashMap FilePathEq HiKey, Map.HashMap HiKey Hi)-hiParseDirectory dir = do- whenLoud $ putStrLn $ "Reading hi directory " ++ dir- files <- filter ((==) ".dump-hi" . takeExtension) <$> listFilesRecursive dir- his <- forM files $ \file -> do- let name = drop (length dir + 1) file- whenLoud $ do- putStr $ "Reading hi file " ++ name ++ " ... "- hFlush stdout- (time, (len, res)) <- duration $ do- src <- S.readFileUTF8 file- len <- evaluate $ S.length src- let res = trimSignatures $ hiParseContents src- evaluate $ rnf res- return (len, res)- whenLoud $ putStrLn $ S.showLength len ++ " bytes in " ++ showDuration time- return (filePathEq name, res)- -- here we try and dedupe any identical Hi modules- let keys = Map.fromList $ map (second HiKey . swap) his- mp1 <- evaluate $ Map.fromList $ map (second (keys Map.!)) his- mp2 <- evaluate $ Map.fromList $ map swap $ Map.toList keys- whenLoud $ putStrLn $ "Found " ++ show (Map.size mp1) ++ " files, " ++ show (Map.size mp2) ++ " distinct"- 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 :: Str -> Hi-hiParseContents = mconcatMap f . parseHanging2 . S.linesCR- where- f (x,xs)- | Just x <- S.stripPrefix "interface " x = mempty{hiModuleName = parseInterface $ S.toList x}- | Just x <- S.stripPrefix "exports:" x = mconcatMap (parseExports . S.toList) $ unindent2 xs- | Just x <- S.stripPrefix "orphans:" x = mempty{hiImportOrphan = Set.fromList $ map parseInterface $ concatMap (words . S.toList) $ x:xs}- | Just x <- S.stripPrefix "package dependencies:" x = mempty{hiImportPackage = Set.fromList $ map parsePackDep $ concatMap (words . S.toList) $ x:xs}- | Just x <- S.stripPrefix "import " x = case unindent2 xs of- [] | let s = words (S.toList x) !! 1- , (pkg, mod) <- fromMaybe ("", s) $ stripInfix ":" s -> mempty- {hiImportPackageModule = Set.singleton (parsePackDep pkg, mod)}- xs -> let m = words (S.toList x) !! 1 in mempty- {hiImportModule = Set.singleton m- ,hiImportIdent = Set.fromList $ map (Ident m . fst . word1 . S.toList) $ dropWhile ("exports:" `S.isPrefixOf`) xs}- | S.length x == S.ugly 32, S.all isHexDigit x,- (y,ys):_ <- parseHanging2 $ map (S.drop $ S.ugly 2) xs,- fun:"::":typ <- concatMap (wordsBy (`elem` (",()[]{} " :: String)) . S.toList) $ 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"- -- "old-locale-1.0.0.7" -> "old-locale"- parsePackDep = intercalate "-" . takeWhile (any isAlpha) . wordsBy (== '-') . takeWhile (/= '@')-- -- "hlint-1.9.41-IPKy9tGF1918X9VRp9DMhp:HSE.All 8002" -> "HSE.All"- -- "HSE.All 8002" -> "HSE.All"- parseInterface = takeWhileEnd (/= ':') . 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` ("{} " :: String)) 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
− src/Main.hs
@@ -1,11 +0,0 @@-module Main(main) where--import Weeder-import System.Exit-import System.Environment-import Control.Monad--main :: IO ()-main = do- bad <- weeder =<< getArgs- when (bad > 0) exitFailure
− src/Stack.hs
@@ -1,48 +0,0 @@-{-# LANGUAGE OverloadedStrings, RecordWildCards, ScopedTypeVariables #-}--module Stack(Stack(..), findStack, parseStack, buildStack) where--import Data.Yaml-import Data.List.Extra-import Control.Exception-import Control.Monad.Extra-import System.Directory.Extra-import qualified Data.Text as T-import qualified Data.HashMap.Strict as Map-import qualified Data.ByteString.Char8 as BS-import Util-import Data.Functor-import Prelude---data Stack = Stack- {stackPackages :: [FilePath]- ,stackDistDir :: FilePath- }--findStack :: FilePath -> IO FilePath-findStack dir = withCurrentDirectory dir $ do- let args = ["path","--config-location","--color=never"]- -- it may do a stack setup, so there may be lots of garbage and then the actual info at the end- res <- maybe "" (trim . snd) . unsnoc . lines <$> cmdStdout "stack" args- when (res == "") $- fail $ "Failed to find stack.yaml file\nCommand: " ++ unwords ("stack":args)- return res--buildStack :: FilePath -> IO ()-buildStack file = cmd "stack" ["build","--stack-yaml=" ++ file,"--test","--bench","--no-run-tests","--no-run-benchmarks","--color=never"]---- | Note that in addition to parsing the stack.yaml file it also runs @stack@ to--- compute the dist-dir.-parseStack :: Maybe FilePath -> FilePath -> IO Stack-parseStack distDir file = do- stackDistDir <- case distDir of- Nothing -> fst . line1 <$> cmdStdout "stack" ["path","--dist-dir","--stack-yaml=" ++ file,"--color=never"]- Just x -> return x- stackPackages <- f . decodeYaml <$> cmdStdout "stack" ["query","locals","--stack-yaml=" ++ file,"--color=never"]- return Stack{..}- where- decodeYaml = either throw id . decodeEither' . BS.pack- fromObject (Object x) = x- fromString (String s) = T.unpack s- f = map (fromString . (Map.! "path") . fromObject) . Map.elems . fromObject
− src/Str.hs
@@ -1,28 +0,0 @@--module Str(- Str,- linesCR,- readFileUTF8,- S.null, S.isPrefixOf, S.drop, S.span, S.length, S.toList, S.all, S.uncons, S.stripPrefix,- ugly, showLength- ) where--import qualified Foundation as S-import qualified Foundation.String as S-import qualified Foundation.IO as S-import Data.Tuple.Extra---type Str = S.String--showLength :: S.CountOf a -> String-showLength (S.CountOf x) = show x--linesCR :: Str -> [Str]-linesCR = S.lines--ugly :: S.Integral a => Integer -> a-ugly = S.fromInteger--readFileUTF8 :: FilePath -> IO Str-readFileUTF8 = fmap (fst3 . S.fromBytes S.UTF8) . S.readFile . S.fromString
− src/Util.hs
@@ -1,109 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings, TupleSections #-}--module Util(- Str,- FilePathEq, filePathEq,- PackageName, ModuleName, IdentName,- parseHanging,- parseHanging2, unindent2,- (?:),- isHaskellCtor,- isHaskellSymbol,- reachable,- isPathsModule,- cmd, cmdStdout- ) where--import Data.Char-import Data.Monoid-import Data.Hashable-import Data.List.Extra-import Data.Tuple.Extra-import System.Process-import System.FilePath-import System.Directory-import System.Info.Extra-import System.Console.CmdArgs.Verbosity-import Str(Str)-import qualified Str as S-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 (maybe True ((== ' ') . fst) . uncons) xs)--parseHanging2 :: [Str] -> [(Str, [Str])]-parseHanging2 = repeatedly (\(x:xs) -> first (x,) $ span (maybe True ((== ' ') . fst) . S.uncons) 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--unindent2 :: [Str] -> [Str]-unindent2 xs = map (S.drop n) xs- where- n = minimum $ top : map f xs- f x = let (a,b) = S.span isSpace x in if S.null b then top else S.length a- top = S.ugly 1000---- | Is the character a member of possible Haskell symbol characters,--- according to the Haskell report.-isHaskellSymbol :: Char -> Bool-isHaskellSymbol x =- x `elem` ("!#$%&*+./<=>?@\\^|-~" :: String) ||- (isSymbol x && x `notElem` ("\"'_(),;[]`{}" :: String))--isHaskellCtor :: IdentName -> Bool-isHaskellCtor [] = False-isHaskellCtor (x:xs) = isUpper x || x == ':'---- | Normal 'FilePath' has 'Eq' but it allows non-normalised paths--- and on Windows/Mac is case-sensitive even when the underlying file system isn't.-newtype FilePathEq = FilePathEq FilePath- deriving (Hashable,Eq,Ord,Show)--filePathEq :: FilePath -> FilePathEq-filePathEq = FilePathEq . (if isWindows || isMac then lower else id) . normalise---- | 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_"---cmdTrace :: FilePath -> [String] -> IO ()-cmdTrace exe args = whenLoud $ do- dir <- getCurrentDirectory- putStrLn $ "Running: " ++ showCommandForUser exe args ++ " (in " ++ dir ++ ")"--cmd :: FilePath -> [String] -> IO ()-cmd exe args = do- cmdTrace exe args- callProcess exe args--cmdStdout :: FilePath -> [String] -> IO String-cmdStdout exe args = do- cmdTrace exe args- readCreateProcess (proc exe args) ""
− src/Warning.hs
@@ -1,150 +0,0 @@-{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-}--module Warning(- Warning(..),- showWarningsPretty,- showWarningsYaml,- showWarningsJson,- readWarningsFile,- ignoreWarnings- ) where--import Cabal-import Util-import Control.Monad.Extra-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]- deriving Show--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- badYaml want x = error $ "Failed to understand Yaml fragment, expected " ++ want ++ ", got:\n" ++ BS.unpack (Yaml.encode x)-- f Null = []- f (Object mp) | Map.null mp = []- 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- v | Just xs <- fromStrings v -> End (T.unpack k) xs- String x -> End (T.unpack k) [T.unpack x]- _ -> badYaml "either a dict with 'name' or a list/single string" $ Object mp- f x = badYaml "either a singleton dict or an array" x-- fromStrings (Array xs) = concatMapM fromStrings $ V.toList xs- fromStrings (String x) = Just [T.unpack x]- fromStrings x = Nothing-- 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] | 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 [] = "" -- no need to write anything in the file-showWarningsYaml xs = BS.unpack $ Yaml.encode $ showWarningsValue xs---readWarningsFile :: FilePath -> IO [Warning]-readWarningsFile file = do- x <- eitherM throwIO return $ Yaml.decodeFileEither file- let res = map warningUnpath $ concatMap (f warningLabels) $ valueToVal x- mapM_ evaluate res -- ensure exceptions happen immediately- return res- 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- | sect `notElem` names = error $- "Warnings file " ++ file ++ ", invalid section name:\n" ++- "Wanted one of: " ++ show (name:names) ++ "\n" ++- "Got: " ++ show sect- | 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)
src/Weeder.hs view
@@ -1,83 +1,397 @@-{-# LANGUAGE TupleSections, RecordWildCards, ScopedTypeVariables #-}+{-# language ApplicativeDo #-}+{-# language BlockArguments #-}+{-# language DeriveGeneric #-}+{-# language FlexibleContexts #-}+{-# language LambdaCase #-}+{-# language NamedFieldPuns #-}+{-# language NoImplicitPrelude #-}+{-# language OverloadedLabels #-}+{-# language OverloadedStrings #-}+{-# language PackageImports #-} --- | Run the @weeder@ program as a direct dependency.--- You are encouraged to use the binary in preference to the library.-module Weeder(weeder) where+module Weeder+ ( -- * Analysis+ Analysis(..)+ , analyseHieFile+ , emptyAnalysis+ , allDeclarations -import Hi-import Cabal-import Stack-import Data.Version-import Data.List.Extra-import Data.Functor-import Data.Tuple.Extra-import Control.Monad.Extra-import System.Console.CmdArgs.Verbosity-import System.IO.Extra-import qualified Data.HashMap.Strict as Map-import System.Directory.Extra-import System.FilePath-import Paths_weeder-import Check-import Warning-import CmdLine-import Prelude+ -- ** Reachability+ , Root(..)+ , reachable + -- * Declarations+ , Declaration(..)+ )+ where --- | Given the weeder command line arguments, return the number of warnings that were produced.--- If the number is @0@ that corresponds to a successful run.-weeder :: [String] -> IO Int-weeder args = do- cmd@Cmd{..} <- getCmd args- whenLoud $ putStrLn $ "Weeder version " ++ showVersion version- res <- mapM (weedPath cmd) cmdProjects- return $ sum res+-- algebraic-graphs+import Algebra.Graph ( Graph, edge, empty, overlay, vertex, vertexList )+import Algebra.Graph.ToGraph ( dfs ) +-- base+import Control.Applicative ( Alternative )+import Control.Monad ( guard, msum, when )+import Data.Foldable ( for_, traverse_ )+import Data.List ( intercalate )+import Data.Monoid ( First( First ) )+import GHC.Generics ( Generic )+import Prelude hiding ( span ) -weedPath :: Cmd -> FilePath -> IO Int-weedPath Cmd{..} proj = do- -- project may either be a directory name, or a stack.yaml file- file <- do- isDir <- doesDirectoryExist proj- if isDir then findStack proj else return proj- whenLoud $ putStrLn $ "Running on Stack file " ++ file- when cmdBuild $ buildStack file- Stack{..} <- parseStack cmdDistDir file- cabals <- forM stackPackages $ \x -> do- file <- selectCabalFile x- (file,) <$> parseCabal file+-- bytestring+import Data.ByteString ( ByteString ) - ignore <- do- let x = takeDirectory file </> ".weeder.yaml"- b <- doesFileExist x- if not b then return [] else do- whenLoud $ putStrLn $ "Reading ignored warnings from " ++ x- readWarningsFile x- let quiet = cmdJson || cmdYaml+-- containers+import Data.Map.Strict ( Map )+import qualified Data.Map.Strict as Map+import Data.Sequence ( Seq )+import Data.Set ( Set )+import qualified Data.Set as Set - res <- forM cabals $ \(cabalFile, Cabal{..}) -> do- (fileToKey, keyToHi) <- hiParseDirectory $ takeDirectory cabalFile </> stackDistDir- let full = check (keyToHi Map.!) cabalName $- map (id &&& selectHiFiles stackDistDir 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+-- generic-lens+import Data.Generics.Labels () - when cmdJson $ putStrLn $ showWarningsJson warns- when cmdYaml $ putStrLn $ showWarningsYaml warns- if cmdMatch then- if sort ignore == sort warns then do- putStrLn "Warnings match"- return 0- else do- putStrLn "MISSING WARNINGS"- putStrLn $ unlines $ showWarningsPretty "" $ ignore \\ warns- putStrLn "EXTRA WARNINGS"- putStrLn $ unlines $ showWarningsPretty "" $ warns \\ ignore- return 1- else do- when (ignored > 0 && not quiet) $- putStrLn $ "Ignored " ++ show ignored ++ " weeds (pass --show-all to see them)"- return $ length warns+-- ghc+import Avail ( AvailInfo( Avail, AvailTC ) )+import FieldLabel ( FieldLbl( FieldLabel, flSelector ) )+import HieTypes+ ( BindType( RegularBind )+ , ContextInfo( Decl, ValBind, PatternBind, Use, TyDecl, ClassTyDecl )+ , DeclType( DataDec, ClassDec, ConDec )+ , HieAST( Node, nodeInfo, nodeChildren, nodeSpan )+ , HieASTs( HieASTs )+ , HieFile( HieFile, hie_asts, hie_exports, hie_module, hie_hs_file, hie_hs_src )+ , IdentifierDetails( IdentifierDetails, identInfo )+ , NodeInfo( NodeInfo, nodeIdentifiers, nodeAnnotations )+ , Scope( ModuleScope )+ )+import Module ( Module, moduleStableString )+import Name ( Name, nameModule_maybe, nameOccName )+import OccName+ ( OccName+ , isDataOcc+ , isDataSymOcc+ , isTcOcc+ , isTvOcc+ , isVarOcc+ , occNameString+ )+import SrcLoc ( RealSrcSpan, realSrcSpanEnd, realSrcSpanStart )++-- lens+import Control.Lens ( (%=) )++-- mtl+import Control.Monad.State.Class ( MonadState )++-- transformers+import Control.Monad.Trans.Maybe ( runMaybeT )+++data Declaration =+ Declaration+ { declModule :: Module+ -- ^ The module this declaration occurs in.+ , declOccName :: OccName+ -- ^ The symbol name of a declaration.+ }+ deriving+ ( Eq, Ord )+++instance Show Declaration where+ show =+ declarationStableName+++declarationStableName :: Declaration -> String+declarationStableName Declaration { declModule, declOccName } =+ let+ namespace+ | isVarOcc declOccName = "var"+ | isTvOcc declOccName = "tv"+ | isTcOcc declOccName = "tc"+ | isDataOcc declOccName = "data"+ | isDataSymOcc declOccName = "dataSym"+ | otherwise = "unknown"++ in+ intercalate "$" [ namespace, moduleStableString declModule, "$", occNameString declOccName ]+++-- | All information maintained by 'analyseHieFile'.+data Analysis =+ Analysis+ { dependencyGraph :: Graph Declaration+ -- ^ A graph between declarations, capturing dependencies.+ , declarationSites :: Map Declaration ( Set RealSrcSpan )+ -- ^ A partial mapping between declarations and their definition site.+ -- This Map is partial as we don't always know where a Declaration was+ -- defined (e.g., it may come from a package without source code).+ -- We capture a set of spans, because a declaration may be defined in+ -- multiple locations, e.g., a type signature for a function separate+ -- from its definition.+ , implicitRoots :: Set Declaration+ -- ^ The Set of all Declarations that are always reachable. This is used+ -- to capture knowledge not yet modelled in weeder, such as instance+ -- declarations depending on top-level functions.+ , exports :: Map Module ( Set Declaration )+ -- ^ All exports for a given module.+ , modulePaths :: Map Module FilePath+ -- ^ A map from modules to the file path to the .hs file defining them.+ , moduleSource :: Map Module ByteString+ }+ deriving+ ( Generic )+++-- | The empty analysis - the result of analysing zero @.hie@ files.+emptyAnalysis :: Analysis+emptyAnalysis =+ Analysis empty mempty mempty mempty mempty mempty+++-- | A root for reachability analysis.+data Root+ = -- | A given declaration is a root.+ DeclarationRoot Declaration+ | -- | All exported declarations in a module are roots.+ ModuleRoot Module+ deriving+ ( Eq, Ord )+++-- | Determine the set of all declaration reachable from a set of roots.+reachable :: Analysis -> Set Root -> Set Declaration+reachable Analysis{ dependencyGraph, exports } roots =+ Set.fromList ( dfs ( foldMap rootDeclarations roots ) dependencyGraph )++ where++ rootDeclarations = \case+ DeclarationRoot d -> [ d ]+ ModuleRoot m -> foldMap Set.toList ( Map.lookup m exports )+++-- | The set of all known declarations, including usages.+allDeclarations :: Analysis -> Set Declaration+allDeclarations Analysis{ dependencyGraph } =+ Set.fromList ( vertexList dependencyGraph )+++-- | Incrementally update 'Analysis' with information in a 'HieFile'.+analyseHieFile :: MonadState Analysis m => HieFile -> m ()+analyseHieFile HieFile{ hie_asts = HieASTs hieASTs, hie_exports, hie_module, hie_hs_file, hie_hs_src } = do+ #modulePaths %= Map.insert hie_module hie_hs_file+ #moduleSource %= Map.insert hie_module hie_hs_src++ for_ hieASTs \ast -> do+ addAllDeclarations ast+ topLevelAnalysis ast++ for_ hie_exports ( analyseExport hie_module )+++analyseExport :: MonadState Analysis m => Module -> AvailInfo -> m ()+analyseExport m = \case+ Avail name ->+ for_ ( nameToDeclaration name ) addExport++ AvailTC name pieces fields -> do+ for_ ( nameToDeclaration name ) addExport+ for_ pieces ( traverse_ addExport . nameToDeclaration )+ for_ fields \FieldLabel{ flSelector } -> for_ ( nameToDeclaration flSelector ) addExport++ where++ addExport d = #exports %= Map.insertWith (<>) m ( Set.singleton d )+++-- | @addDependency x y@ adds the information that @x@ depends on @y@.+addDependency :: MonadState Analysis m => Declaration -> Declaration -> m ()+addDependency x y =+ #dependencyGraph %= overlay ( edge x y )+++addImplicitRoot :: MonadState Analysis m => Declaration -> m ()+addImplicitRoot x =+ #implicitRoots %= Set.insert x+++define :: MonadState Analysis m => Declaration -> RealSrcSpan -> m ()+define decl span =+ when ( realSrcSpanStart span /= realSrcSpanEnd span ) do+ #declarationSites %= Map.insertWith Set.union decl ( Set.singleton span )+ #dependencyGraph %= overlay ( vertex decl )+++addDeclaration :: MonadState Analysis m => Declaration -> m ()+addDeclaration decl =+ #dependencyGraph %= overlay ( vertex decl )+++-- | Try and add vertices for all declarations in an AST - both+-- those declared here, and those referred to from here.+addAllDeclarations :: ( MonadState Analysis m ) => HieAST a -> m ()+addAllDeclarations n@Node{ nodeChildren } = do+ for_ ( findIdentifiers ( const True ) n ) addDeclaration++ for_ nodeChildren addAllDeclarations+++topLevelAnalysis :: MonadState Analysis m => HieAST a -> m ()+topLevelAnalysis n@Node{ nodeChildren } = do+ analysed <-+ runMaybeT+ ( msum+ [+ -- analyseStandaloneDeriving n+ -- ,+ analyseInstanceDeclaration n+ , analyseBinding n+ , analyseRewriteRule n+ , analyseClassDeclaration n+ , analyseDataDeclaration n+ ]+ )++ case analysed of+ Nothing ->+ -- We didn't find a top level declaration here, check all this nodes+ -- children.+ traverse_ topLevelAnalysis nodeChildren++ Just () ->+ -- Top level analysis succeeded, there's nothing more to do for this node.+ return ()+++analyseBinding :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseBinding n@Node{ nodeSpan, nodeInfo = NodeInfo{ nodeAnnotations } } = do+ guard $ ( "FunBind", "HsBindLR" ) `Set.member` nodeAnnotations++ for_ ( findDeclarations n ) \d -> do+ define d nodeSpan++ for_ ( uses n ) \use ->+ addDependency d use+++analyseRewriteRule :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseRewriteRule n@Node{ nodeInfo = NodeInfo{ nodeAnnotations } } = do+ guard ( ( "HsRule", "RuleDecl" ) `Set.member` nodeAnnotations )++ for_ ( uses n ) addImplicitRoot+++analyseInstanceDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseInstanceDeclaration n@Node{ nodeInfo = NodeInfo{ nodeAnnotations } } = do+ guard ( ( "ClsInstD", "InstDecl" ) `Set.member` nodeAnnotations )++ traverse_ addImplicitRoot ( uses n )+++analyseClassDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseClassDeclaration n@Node{ nodeInfo = NodeInfo{ nodeAnnotations } } = do+ guard ( ( "ClassDecl", "TyClDecl" ) `Set.member` nodeAnnotations )++ for_ ( findIdentifiers isClassDeclaration n ) \d ->+ for_ ( findIdentifiers ( const True ) n ) ( addDependency d )++ where++ isClassDeclaration =+ not . Set.null . Set.filter \case+ Decl ClassDec _ ->+ True++ _ ->+ False+++analyseDataDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseDataDeclaration n@Node { nodeInfo = NodeInfo{ nodeAnnotations } } = do+ guard ( ( "DataDecl", "TyClDecl" ) `Set.member` nodeAnnotations )++ for_+ ( foldMap+ ( First . Just )+ ( findIdentifiers ( any isDataDec ) n )+ )+ \dataTypeName ->+ for_ ( constructors n ) \constructor ->+ for_ ( foldMap ( First . Just ) ( findIdentifiers ( any isConDec ) constructor ) ) \conDec -> do+ addDependency conDec dataTypeName++ for_ ( uses constructor ) ( addDependency conDec )++ where++ isDataDec = \case+ Decl DataDec _ -> True+ _ -> False++ isConDec = \case+ Decl ConDec _ -> True+ _ -> False+++constructors :: HieAST a -> Seq ( HieAST a )+constructors n@Node { nodeChildren, nodeInfo = NodeInfo{ nodeAnnotations } } =+ if any ( \( _, t ) -> t == "ConDecl" ) nodeAnnotations then+ pure n++ else+ foldMap constructors nodeChildren+++findDeclarations :: HieAST a -> Seq Declaration+findDeclarations =+ findIdentifiers+ ( not+ . Set.null+ . Set.filter \case+ -- Things that count as declarations+ ValBind RegularBind ModuleScope _ -> True+ PatternBind ModuleScope _ _ -> True+ Decl _ _ -> True+ TyDecl -> True+ ClassTyDecl{} -> True++ -- Anything else is not a declaration+ _ -> False+ )+++findIdentifiers+ :: ( Set ContextInfo -> Bool )+ -> HieAST a+ -> Seq Declaration+findIdentifiers f Node{ nodeInfo = NodeInfo{ nodeIdentifiers }, nodeChildren } =+ foldMap+ ( \case+ ( Left _, _ ) ->+ mempty++ ( Right name, IdentifierDetails{ identInfo } ) ->+ if f identInfo then+ foldMap pure ( nameToDeclaration name )++ else+ mempty+ )++ ( Map.toList nodeIdentifiers )+ <> foldMap ( findIdentifiers f ) nodeChildren+++uses :: HieAST a -> Set Declaration+uses =+ foldMap Set.singleton+ . findIdentifiers \identInfo -> Use `Set.member` identInfo+++nameToDeclaration :: Name -> Maybe Declaration+nameToDeclaration name = do+ m <- nameModule_maybe name+ return Declaration { declModule = m, declOccName = nameOccName name }
+ src/Weeder/Config.hs view
@@ -0,0 +1,37 @@+{-# language ApplicativeDo #-}+{-# language BlockArguments #-}+{-# language OverloadedStrings #-}+{-# language RecordWildCards #-}++module Weeder.Config ( Config(..), config ) where++-- containers+import Data.Set ( Set )+import qualified Data.Set as Set++-- dhall+import qualified Dhall+++-- | Configuration for Weeder analysis.+data Config = Config+ { rootPatterns :: Set String+ -- ^ Any declarations matching these regular expressions will be added to+ -- the root set.+ , typeClassRoots :: Bool+ -- ^ If True, consider all declarations in a type class as part of the root+ -- set. Weeder is currently unable to identify whether or not a type class+ -- instance is used - enabling this option can prevent false positives.+ }+++-- | A Dhall expression decoder for 'Config'.+--+-- This parses Dhall expressions of the type @{ roots : List Text, type-class-roots : Bool }@.+config :: Dhall.Decoder Config+config =+ Dhall.record do+ rootPatterns <- Set.fromList <$> Dhall.field "roots" ( Dhall.list Dhall.string )+ typeClassRoots <- Dhall.field "type-class-roots" Dhall.bool++ return Config{..}
+ src/Weeder/Main.hs view
@@ -0,0 +1,205 @@+{-# language ApplicativeDo #-}+{-# language BlockArguments #-}+{-# language FlexibleContexts #-}+{-# language LambdaCase #-}+{-# language NamedFieldPuns #-}+{-# language OverloadedStrings #-}++-- | This module provides an entry point to the Weeder executable.++module Weeder.Main ( main, mainWithConfig ) where++-- base+import Control.Monad ( guard, unless )+import Control.Monad.IO.Class ( liftIO )+import Data.Bool+import Data.Foldable+import Text.Printf ( printf )+import System.Exit ( exitFailure )++-- bytestring+import qualified Data.ByteString.Char8 as BS++-- containers+import qualified Data.Map.Strict as Map+import qualified Data.Set as Set++-- dhall+import qualified Dhall++-- directory+import System.Directory ( canonicalizePath, doesDirectoryExist, doesFileExist, doesPathExist, listDirectory, withCurrentDirectory )++-- filepath+import System.FilePath ( isExtensionOf )++-- ghc+import HieBin ( HieFileResult( HieFileResult, hie_file_result ) )+import HieBin ( readHieFile )+import Module ( moduleName, moduleNameString )+import NameCache ( initNameCache )+import OccName ( occNameString )+import SrcLoc ( realSrcSpanStart, srcLocCol, srcLocLine )+import UniqSupply ( mkSplitUniqSupply )++-- regex-tdfa+import Text.Regex.TDFA ( (=~) )++-- optparse-applicative+import Options.Applicative++-- transformers+import Control.Monad.Trans.State.Strict ( execStateT )++-- weeder+import Weeder+import Weeder.Config+++-- | Parse command line arguments and into a 'Config' and run 'mainWithConfig'.+main :: IO ()+main = do+ configExpr <-+ execParser $+ info+ ( strOption+ ( long "config"+ <> help "A Dhall expression for Weeder's configuration. Can either be a file path (a Dhall import) or a literal Dhall expression."+ <> value "./weeder.dhall"+ )+ )+ mempty++ Dhall.input config configExpr >>= mainWithConfig+++-- | Run Weeder in the current working directory with a given 'Config'.+--+-- This will recursively find all @.hie@ files in the current directory, perform+-- analysis, and report all unused definitions according to the 'Config'.+mainWithConfig :: Config -> IO ()+mainWithConfig Config{ rootPatterns, typeClassRoots } = do+ hieFilePaths <-+ getHieFilesIn "./."++ nameCache <- do+ uniqSupply <- mkSplitUniqSupply 'z'+ return ( initNameCache uniqSupply [] )++ analysis <-+ flip execStateT emptyAnalysis do+ for_ hieFilePaths \hieFilePath -> do+ ( HieFileResult{ hie_file_result }, _ ) <-+ liftIO ( readHieFile nameCache hieFilePath )++ analyseHieFile hie_file_result++ let+ roots =+ Set.filter+ ( \d ->+ any+ ( ( moduleNameString ( moduleName ( declModule d ) ) <> "." <> occNameString ( declOccName d ) ) =~ )+ rootPatterns+ )+ ( allDeclarations analysis )++ reachableSet =+ reachable+ analysis+ ( Set.map DeclarationRoot roots <> bool mempty ( Set.map DeclarationRoot ( implicitRoots analysis ) ) typeClassRoots )++ dead =+ allDeclarations analysis Set.\\ reachableSet++ warnings =+ Map.unionsWith (++) $+ foldMap+ ( \d ->+ fold $ do+ moduleFilePath <- Map.lookup ( declModule d ) ( modulePaths analysis )+ moduleSource <- Map.lookup ( declModule d ) ( moduleSource analysis )++ spans <- Map.lookup d ( declarationSites analysis )+ guard $ not $ null spans++ let snippets = do+ srcSpan <- Set.toList spans++ let start = realSrcSpanStart srcSpan+ let firstLine = max 0 ( srcLocLine start - 3 )++ return ( start, take 5 $ drop firstLine $ zip [1..] $ BS.lines moduleSource )++ return [ Map.singleton moduleFilePath ( liftA2 (,) snippets (pure d) ) ]+ )+ dead++ for_ ( Map.toList warnings ) \( path, declarations ) ->+ for_ declarations \( ( start, snippet ), d ) -> do+ putStrLn $+ unwords+ [ foldMap ( <> ":" ) [ path, show ( srcLocLine start ), show ( srcLocCol start ) ]+ , "error:"+ , occNameString ( declOccName d )+ , "is unused"+ ]++ putStrLn ""+ for_ snippet \( n, line ) ->+ putStrLn $+ replicate 4 ' '+ <> printf "% 4d" ( n :: Int )+ <> " ┃ "+ <> BS.unpack line+ putStrLn ""++ putStrLn $+ replicate 4 ' '+ <> "Delete this definition or add ‘"+ <> moduleNameString ( moduleName ( declModule d ) )+ <> "."+ <> occNameString ( declOccName d )+ <> "’ as a root to fix this error."+ putStrLn ""+ putStrLn ""++ putStrLn $ "Weeds detected: " <> show ( sum ( length <$> warnings ) )++ unless ( null warnings ) exitFailure+++-- | Recursively search for .hie files in given directory+getHieFilesIn :: FilePath -> IO [FilePath]+getHieFilesIn path = do+ exists <-+ doesPathExist path++ if exists+ then do+ isFile <-+ doesFileExist path++ if isFile && "hie" `isExtensionOf` path+ then do+ path' <-+ canonicalizePath path++ return [ path' ]++ else do+ isDir <-+ doesDirectoryExist path++ if isDir+ then do+ cnts <-+ listDirectory path++ withCurrentDirectory path ( foldMap getHieFilesIn cnts )++ else+ return []++ else+ return []
weeder.cabal view
@@ -1,65 +1,58 @@-cabal-version: >= 1.18-build-type: Simple-name: weeder-version: 1.0.9-license: BSD3-license-file: LICENSE-category: Development-author: Neil Mitchell <ndmitchell@gmail.com>-maintainer: Neil Mitchell <ndmitchell@gmail.com>-copyright: Neil Mitchell 2017-2020-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+cabal-version: 2.0+license: BSD3+license-file: LICENSE+name: weeder+author: Ollie Charles <ollie@ocharles.org.uk>+maintainer: Ollie Charles <ollie@ocharles.org.uk>+build-type: Simple+version: 2.0.0+copyright: Neil Mitchell 2017-2020, Oliver Charles 2020+synopsis: Detect dead code+description: Find declarations.+homepage: https://github.com/ocharles/weeder#readme+bug-reports: https://github.com/ocharles/weeder/issues+category: Development extra-doc-files: README.md- CHANGES.txt-tested-with: GHC==8.10.1, GHC==8.8.3, GHC==8.6.5, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2--source-repository head- type: git- location: https://github.com/ndmitchell/weeder.git+ CHANGELOG.md library- default-language: Haskell2010- hs-source-dirs: src- if impl(ghc < 8.0)- build-depends: semigroups >= 0.18- build-depends:- base >= 4.6 && < 5,- text,- unordered-containers,- yaml,- vector,- hashable,- directory,- deepseq,- filepath,- cmdargs,- yaml >= 0.5.0,- aeson >= 1.1.2.0,- bytestring,- foundation >= 0.0.13,- process >= 1.2.3.0,- extra >= 1.6.4- exposed-modules:- Weeder- other-modules:- Cabal- Hi- Stack- Util- Check- Warning- CmdLine- Str- Paths_weeder+ build-depends:+ algebraic-graphs ^>= 0.4 ,+ base ^>= 4.13.0.0 ,+ bytestring ^>= 0.10.9.0 ,+ containers ^>= 0.6.2.1 ,+ dhall ^>= 1.30.0 ,+ directory ^>= 1.3.3.2 ,+ filepath ^>= 1.4.2.1 ,+ generic-lens ^>= 1.1.0.0 || ^>= 2.0.0.0 ,+ ghc ^>= 8.8.1 ,+ lens ^>= 4.18.1 ,+ mtl ^>= 2.2.2 ,+ optparse-applicative ^>= 0.14.3.0 ,+ regex-tdfa ^>= 1.3.1.0 ,+ transformers ^>= 0.5.6.2+ hs-source-dirs: src+ exposed-modules:+ Weeder+ Weeder.Config+ Weeder.Main+ ghc-options: -Wall -fwarn-incomplete-uni-patterns+ default-language: Haskell2010 + executable weeder- default-language: Haskell2010- main-is: src/Main.hs- build-depends:- base,- weeder+ build-depends:+ base ^>= 4.13.0.0 ,+ bytestring ^>= 0.10.9.0 ,+ containers ^>= 0.6.2.1 ,+ directory ^>= 1.3.3.2 ,+ filepath ^>= 1.4.2.1 ,+ ghc ^>= 8.8.1 ,+ optparse-applicative ^>= 0.14.3.0 ,+ transformers ^>= 0.5.6.2 ,+ weeder+ main-is: Main.hs+ hs-source-dirs: exe-weeder+ ghc-options: -Wall -fwarn-incomplete-uni-patterns+ default-language: Haskell2010