weeder 2.10.0 → 2.11.0
raw patch · 14 files changed
+311/−66 lines, 14 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Weeder.Run: Weed :: String -> FilePath -> Int -> Int -> Declaration -> Maybe String -> Weed
+ Weeder: localDeclarations :: Analysis -> Set Declaration
+ Weeder.Config: CompiledRegex :: String -> Regex -> CompiledRegex
+ Weeder.Config: Configured :: a -> Configured a
+ Weeder.Config: Default :: a -> Configured a
+ Weeder.Config: [compiledRegex] :: CompiledRegex -> Regex
+ Weeder.Config: [regexSource] :: CompiledRegex -> String
+ Weeder.Config: configuredValue :: Configured a -> a
+ Weeder.Config: data CompiledRegex
+ Weeder.Config: data Configured a
+ Weeder.Config: instance Data.Foldable.Foldable Weeder.Config.Configured
+ Weeder.Config: instance Data.Traversable.Traversable Weeder.Config.Configured
+ Weeder.Config: instance GHC.Base.Functor Weeder.Config.Configured
+ Weeder.Config: instance GHC.Classes.Eq a => GHC.Classes.Eq (Weeder.Config.Configured a)
+ Weeder.Config: instance GHC.Show.Show a => GHC.Show.Show (Weeder.Config.Configured a)
+ Weeder.Config: showInstancePattern :: Show a => InstancePattern a -> String
+ Weeder.Run: DeadRootInstance :: String -> DeadRoot
+ Weeder.Run: DeadRootModule :: String -> DeadRoot
+ Weeder.Run: DeadRootPattern :: String -> DeadRoot
+ Weeder.Run: DeclarationWeed :: String -> FilePath -> Int -> Int -> Declaration -> Maybe String -> DeclarationWeed
+ Weeder.Run: WeedDeclaration :: DeclarationWeed -> Weed
+ Weeder.Run: WeedRoot :: DeadRoot -> Weed
+ Weeder.Run: data DeadRoot
+ Weeder.Run: data DeclarationWeed
- Weeder.Config: Config :: [a] -> Bool -> [InstancePattern a] -> Bool -> [a] -> ConfigType a
+ Weeder.Config: Config :: Configured [a] -> Bool -> Configured [InstancePattern a] -> Bool -> Configured [a] -> ConfigType a
- Weeder.Config: [rootInstances] :: ConfigType a -> [InstancePattern a]
+ Weeder.Config: [rootInstances] :: ConfigType a -> Configured [InstancePattern a]
- Weeder.Config: [rootModules] :: ConfigType a -> [a]
+ Weeder.Config: [rootModules] :: ConfigType a -> Configured [a]
- Weeder.Config: [rootPatterns] :: ConfigType a -> [a]
+ Weeder.Config: [rootPatterns] :: ConfigType a -> Configured [a]
- Weeder.Config: type Config = ConfigType Regex
+ Weeder.Config: type Config = ConfigType CompiledRegex
- Weeder.Run: [weedCol] :: Weed -> Int
+ Weeder.Run: [weedCol] :: DeclarationWeed -> Int
- Weeder.Run: [weedDeclaration] :: Weed -> Declaration
+ Weeder.Run: [weedDeclaration] :: DeclarationWeed -> Declaration
- Weeder.Run: [weedLine] :: Weed -> Int
+ Weeder.Run: [weedLine] :: DeclarationWeed -> Int
- Weeder.Run: [weedPackage] :: Weed -> String
+ Weeder.Run: [weedPackage] :: DeclarationWeed -> String
- Weeder.Run: [weedPath] :: Weed -> FilePath
+ Weeder.Run: [weedPath] :: DeclarationWeed -> FilePath
- Weeder.Run: [weedPrettyPrintedType] :: Weed -> Maybe String
+ Weeder.Run: [weedPrettyPrintedType] :: DeclarationWeed -> Maybe String
Files
- CHANGELOG.md +5/−0
- README.md +29/−0
- src/Weeder.hs +14/−1
- src/Weeder/Config.hs +79/−24
- src/Weeder/Run.hs +129/−31
- test/Main.hs +4/−1
- test/Spec/DefaultsNotReported.stdout +0/−0
- test/Spec/DefaultsNotReported.toml +6/−0
- test/Spec/DefaultsNotReported/DefaultsNotReported.hs +10/−0
- test/Spec/SelfWeeding.stdout +3/−0
- test/Spec/SelfWeeding.toml +9/−0
- test/Spec/SelfWeeding/SelfWeeding.hs +12/−0
- test/UnitTests/Weeder/ConfigSpec.hs +3/−3
- weeder.cabal +8/−6
CHANGELOG.md view
@@ -3,6 +3,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2.11.0 - 2026-09-15++### Added+* Weeder now reports explicitly configured `roots` patterns, `root-instances` entries and `root-modules` patterns that match no identifiers, so stale configuration entries are surfaced as weeds (self-weeding). Defaults are left alone. (#204)+ ## 2.10.0 - 2025-07-29 ### Added
README.md view
@@ -111,6 +111,35 @@ ] ``` +## Self-weeding++Over the lifetime of a project, roots that were once needed can become stale:+the declaration they pointed at gets renamed or removed, leaving behind a+`roots` pattern, `root-instances` entry or `root-modules` pattern that no longer+matches anything. Such an entry is a weed in the configuration itself.++Weeder reports these automatically. Any `roots` pattern that matches no+declaration, any `root-instances` entry that matches no instance, and any+`root-modules` pattern that matches no module, is reported alongside the regular+weeds (and likewise contributes to the weeds-found exit code):++``` shell+$ weeder+no declaration matches roots entry "^Main.runServer$"+no instance matches root-instances entry { class = "\\.ToJSON$" }+no module matches root-modules entry "^Test\\."+```++A `roots` pattern counts as matching as long as it matches any identifier Weeder+is aware of, even one that never appears in the output (such as a type or+constructor when `unused-types` is disabled), so a root naming real code is not+reported just because of the current analysis mode.++Only entries you have explicitly configured are reported. Default `roots` and+`root-instances` are left alone, since telling you that a default you never+wrote is currently unused would not be actionable. When `type-class-roots` is+set, `root-instances` is ignored entirely, so its entries are not reported.+ ## Exit codes Weeder emits the following exit codes:
src/Weeder.hs view
@@ -18,6 +18,7 @@ , analyseHieFile , emptyAnalysis , outputableDeclarations+ , localDeclarations -- ** Reachability , Root(..)@@ -29,7 +30,7 @@ where -- algebraic-graphs-import Algebra.Graph ( Graph, edge, empty, overlay, vertex, stars, star, overlays )+import Algebra.Graph ( Graph, edge, empty, overlay, vertex, stars, star, overlays, vertexSet ) import Algebra.Graph.ToGraph ( dfs ) -- base@@ -245,6 +246,18 @@ outputableDeclarations :: Analysis -> Set Declaration outputableDeclarations Analysis{ declarationSites } = Map.keysSet declarationSites+++-- | Every declaration defined in one of the analysed modules, including ones+-- that never appear in the output (such as types and constructors when+-- @unused-types@ is disabled). Used to decide whether a configured root matches+-- any real identifier in the project, as opposed to whether it keeps anything+-- alive. Declarations from external packages — which appear in the graph only+-- as dependency targets — are excluded, so a root that matches only an external+-- symbol is still reported as unused.+localDeclarations :: Analysis -> Set Declaration+localDeclarations Analysis{ dependencyGraph, modulePaths } =+ Set.filter (\d -> declModule d `Map.member` modulePaths) (vertexSet dependencyGraph) -- Generate an initial graph of the current HieFile.
src/Weeder/Config.hs view
@@ -17,11 +17,17 @@ , configToToml , decodeNoDefaults , defaultConfig+ -- * Compiled regular expressions+ , CompiledRegex(..)+ -- * Configuration provenance+ , Configured(..)+ , configuredValue -- * Marking instances as roots , InstancePattern , modulePattern , instancePattern , classPattern+ , showInstancePattern , pattern InstanceOnly , pattern ClassOnly , pattern ModuleOnly@@ -37,6 +43,9 @@ -- containers import Data.Containers.ListUtils (nubOrd) +-- text+import Data.Text (Text)+ -- regex-tdfa import Text.Regex.TDFA ( Regex, RegexOptions ( defaultExecOpt, defaultCompOpt ) ) import Text.Regex.TDFA.TDFA ( patternToRegex )@@ -47,9 +56,35 @@ -- | Configuration for Weeder analysis.-type Config = ConfigType Regex+type Config = ConfigType CompiledRegex +-- | A compiled regular expression, paired with the source string it was+-- compiled from. We keep the source around so that we can report which+-- configured pattern is responsible when a pattern matches no identifiers.+data CompiledRegex = CompiledRegex+ { regexSource :: String+ , compiledRegex :: Regex+ }+++-- | A configured value, and whether it was set explicitly or left at its+-- default. We track this for the root sections so that self-weeding only+-- reports entries the user actually wrote: pointing out that a default they+-- never configured is unused would not be actionable.+data Configured a+ = Configured a+ | Default a+ deriving (Eq, Show, Functor, Foldable, Traversable)+++-- | The configured value, regardless of where it came from.+configuredValue :: Configured a -> a+configuredValue = \case+ Configured a -> a+ Default a -> a++ -- | Configuration that has been parsed from TOML (and can still be -- converted back), but not yet compiled to a 'Config'. type ConfigParsed = ConfigType String@@ -57,19 +92,19 @@ -- | Underlying type for 'Config' and 'ConfigParsed'. data ConfigType a = Config- { rootPatterns :: [a]+ { rootPatterns :: Configured [a] -- ^ 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. Overrides root-instances.- , rootInstances :: [InstancePattern a]+ , rootInstances :: Configured [InstancePattern a] -- ^ All matching instances will be added to the root set. An absent field -- will always match. , unusedTypes :: Bool -- ^ Toggle to look for and output unused types. Type family instances will -- be marked as implicit roots.- , rootModules :: [a]+ , rootModules :: Configured [a] -- ^ All matching modules will be added to the root set. } deriving (Eq, Show, Functor, Foldable, Traversable) @@ -98,14 +133,26 @@ defaultConfig :: ConfigParsed defaultConfig = Config- { rootPatterns = [ "Main.main", "^Paths_.*"]+ { rootPatterns = Default defaultRootPatterns , typeClassRoots = False- , rootInstances = [ ClassOnly "\\.IsString$", ClassOnly "\\.IsList$" ]+ , rootInstances = Default defaultRootInstances , unusedTypes = False- , rootModules = mempty+ , rootModules = Default defaultRootModules } +defaultRootPatterns :: [String]+defaultRootPatterns = [ "Main.main", "^Paths_.*" ]+++defaultRootInstances :: [InstancePattern String]+defaultRootInstances = [ ClassOnly "\\.IsString$", ClassOnly "\\.IsList$" ]+++defaultRootModules :: [String]+defaultRootModules = mempty++ instance TOML.DecodeTOML Config where tomlDecoder = do conf <- TOML.tomlDecoder@@ -114,22 +161,30 @@ instance TOML.DecodeTOML ConfigParsed where tomlDecoder = do- rootPatterns <- TOML.getFieldOr (rootPatterns defaultConfig) "roots"+ rootPatterns <- getConfigured defaultRootPatterns "roots" typeClassRoots <- TOML.getFieldOr (typeClassRoots defaultConfig) "type-class-roots"- rootInstances <- TOML.getFieldOr (rootInstances defaultConfig) "root-instances"+ rootInstances <- getConfigured defaultRootInstances "root-instances" unusedTypes <- TOML.getFieldOr (unusedTypes defaultConfig) "unused-types"- rootModules <- TOML.getFieldOr (rootModules defaultConfig) "root-modules"+ rootModules <- getConfigured defaultRootModules "root-modules" pure Config{..} +-- | Decode an optional field, marking it 'Configured' when present and falling+-- back to the given 'Default' otherwise.+getConfigured :: TOML.DecodeTOML a => a -> Text -> TOML.Decoder (Configured a)+getConfigured def key = maybe (Default def) Configured <$> TOML.getFieldOpt key++ decodeNoDefaults :: TOML.Decoder Config decodeNoDefaults = do- rootPatterns <- TOML.getField "roots"+ -- In this mode every field must be specified, so every root section is+ -- explicit by construction.+ rootPatterns <- Configured <$> TOML.getField "roots" typeClassRoots <- TOML.getField "type-class-roots"- rootInstances <- TOML.getField "root-instances"+ rootInstances <- Configured <$> TOML.getField "root-instances" unusedTypes <- TOML.getField "unused-types"- rootModules <- TOML.getField "root-modules"+ rootModules <- Configured <$> TOML.getField "root-modules" either fail pure $ compileConfig Config{..} @@ -181,28 +236,28 @@ moduleField m = "module = " ++ show m -compileRegex :: String -> Either String Regex-compileRegex = bimap show (\p -> patternToRegex p defaultCompOpt defaultExecOpt) . parseRegex+compileRegex :: String -> Either String CompiledRegex+compileRegex src =+ bimap show (\p -> CompiledRegex src (patternToRegex p defaultCompOpt defaultExecOpt)) (parseRegex src) compileConfig :: ConfigParsed -> Either String Config compileConfig conf@Config{ rootInstances, rootPatterns, rootModules } = traverse compileRegex conf' where- rootInstances' = nubOrd rootInstances- rootPatterns' = nubOrd rootPatterns- rootModules' = nubOrd rootModules- conf' = conf{ rootInstances = rootInstances', rootPatterns = rootPatterns', rootModules = rootModules' }+ conf' = conf+ { rootInstances = fmap nubOrd rootInstances+ , rootPatterns = fmap nubOrd rootPatterns+ , rootModules = fmap nubOrd rootModules+ } configToToml :: ConfigParsed -> String configToToml Config{..} = unlines . intersperse mempty $- [ "roots = " ++ show rootPatterns+ [ "roots = " ++ show (configuredValue rootPatterns) , "type-class-roots = " ++ map toLower (show typeClassRoots)- , "root-instances = " ++ "[" ++ intercalate "," (map showInstancePattern rootInstances') ++ "]"+ , "root-instances = " ++ "[" ++ intercalate "," (map showInstancePattern (configuredValue rootInstances)) ++ "]" , "unused-types = " ++ map toLower (show unusedTypes)- , "root-modules = " ++ show rootModules+ , "root-modules = " ++ show (configuredValue rootModules) ]- where- rootInstances' = rootInstances
src/Weeder/Run.hs view
@@ -4,7 +4,7 @@ {-# language NamedFieldPuns #-} {-# LANGUAGE FlexibleContexts #-} -module Weeder.Run ( runWeeder, Weed(..), formatWeed ) where+module Weeder.Run ( runWeeder, Weed(..), DeclarationWeed(..), DeadRoot(..), formatWeed ) where -- base import Control.Applicative ( liftA2 )@@ -44,7 +44,16 @@ import Weeder.Config -data Weed = Weed+-- | Something Weeder found that is unused: either a dead declaration in the+-- analysed code, or a configured root that matched no identifiers (a weed in+-- the configuration itself).+data Weed+ = WeedDeclaration DeclarationWeed+ | WeedRoot DeadRoot+++-- | A dead declaration: code that is written but never reachable from a root.+data DeclarationWeed = DeclarationWeed { weedPackage :: String , weedPath :: FilePath , weedLine :: Int@@ -54,12 +63,29 @@ } +-- | A configured root that no longer applies because it matches nothing.+data DeadRoot+ = -- | A @roots@ pattern (its source string) that matched no declaration.+ DeadRootPattern String+ | -- | A @root-instances@ entry (pretty-printed) that matched no instance.+ DeadRootInstance String+ | -- | A @root-modules@ pattern (its source string) that matched no module.+ DeadRootModule String++ formatWeed :: Weed -> String-formatWeed Weed{..} =- weedPackage <> ": " <> weedPath <> ":" <> show weedLine <> ":" <> show weedCol <> ": "- <> case weedPrettyPrintedType of- Nothing -> occNameString ( declOccName weedDeclaration )- Just t -> "(Instance) :: " <> t+formatWeed = \case+ WeedDeclaration DeclarationWeed{..} ->+ weedPackage <> ": " <> weedPath <> ":" <> show weedLine <> ":" <> show weedCol <> ": "+ <> case weedPrettyPrintedType of+ Nothing -> occNameString ( declOccName weedDeclaration )+ Just t -> "(Instance) :: " <> t+ WeedRoot (DeadRootPattern src) ->+ "no declaration matches roots entry " <> show src+ WeedRoot (DeadRootInstance s) ->+ "no instance matches root-instances entry " <> s+ WeedRoot (DeadRootModule src) ->+ "no module matches root-modules entry " <> show src -- | Run Weeder on the given .hie files with the given 'Config'. --@@ -96,14 +122,14 @@ Set.filter ( \d -> any- (`matchTest` displayDeclaration d)- rootPatterns+ ( \p -> matchTest ( compiledRegex p ) ( displayDeclaration d ) )+ ( configuredValue rootPatterns ) ) ( outputableDeclarations analysis ) matchingModules = Set.filter- ((\s -> any (`matchTest` s) rootModules) . moduleNameString . moduleName)+ ((\s -> any (\p -> matchTest ( compiledRegex p ) s) ( configuredValue rootModules )) . moduleNameString . moduleName) ( Map.keysSet $ exports analysis ) reachableSet =@@ -133,41 +159,113 @@ ) dead - weeds =+ declarationWeeds = Map.toList warnings & concatMap \( weedPath, declarations ) -> sortOn fst declarations & map \( (weedPackage, (weedLine, weedCol)) , weedDeclaration ) ->- Weed { weedPrettyPrintedType = Map.lookup weedDeclaration (prettyPrintedType analysis)- , weedPackage- , weedPath- , weedLine- , weedCol- , weedDeclaration- }+ WeedDeclaration DeclarationWeed+ { weedPrettyPrintedType = Map.lookup weedDeclaration (prettyPrintedType analysis)+ , weedPackage+ , weedPath+ , weedLine+ , weedCol+ , weedDeclaration+ } + -- A @roots@ pattern that matches no identifier in the project is a weed in+ -- the configuration itself: it no longer applies to any declaration. We+ -- match against every local declaration rather than only the outputable+ -- ones, so that a root naming a real type or constructor is not flagged just+ -- because @unused-types@ happens to be disabled. Only patterns the user+ -- explicitly configured are reported, since pointing out that an+ -- unconfigured default is unused is not actionable.+ deadRootPatterns =+ case rootPatterns of+ Default _ -> []+ Configured patterns ->+ [ regexSource p+ | p <- patterns+ , not $+ any+ ( \d -> matchTest ( compiledRegex p ) ( displayDeclaration d ) )+ ( localDeclarations analysis )+ ]++ -- A @root-instances@ entry that matches no instance is likewise a weed.+ -- When 'typeClassRoots' is set, @root-instances@ is ignored entirely, so we+ -- don't report its entries.+ instanceRoots =+ [ ( d, c ) | InstanceRoot d c <- Set.toList ( implicitRoots analysis ) ]++ deadRootInstances+ | typeClassRoots = []+ | otherwise =+ case rootInstances of+ Default _ -> []+ Configured patterns ->+ [ showInstancePattern ( regexSource <$> ip )+ | ip <- patterns+ , not $ any ( matchesInstancePattern analysis ip ) instanceRoots+ ]++ -- A @root-modules@ pattern that matches none of the modules Weeder analysed+ -- is also a weed.+ knownModuleNames =+ map ( moduleNameString . moduleName ) ( Map.keys ( modulePaths analysis ) )++ deadRootModules =+ case rootModules of+ Default _ -> []+ Configured patterns ->+ [ regexSource p+ | p <- patterns+ , not $ any ( \m -> matchTest ( compiledRegex p ) m ) knownModuleNames+ ]++ weeds =+ declarationWeeds+ <> map ( WeedRoot . DeadRootPattern ) deadRootPatterns+ <> map ( WeedRoot . DeadRootInstance ) deadRootInstances+ <> map ( WeedRoot . DeadRootModule ) deadRootModules+ in (weeds, analysis) where filterImplicitRoots :: Analysis -> Set Root -> Set Root- filterImplicitRoots Analysis{ prettyPrintedType, modulePaths } = Set.filter $ \case+ filterImplicitRoots analysis = Set.filter $ \case DeclarationRoot _ -> True -- keep implicit roots for rewrite rules etc ModuleRoot _ -> True - InstanceRoot d c -> typeClassRoots || matchingType- where- matchingType =- let mt = Map.lookup d prettyPrintedType- matches = maybe (const False) (flip matchTest) mt- in any (maybe True matches) filteredInstances+ -- [tag:RootInstanceMatching] The reachability check here and the+ -- dead-root-instance check in 'deadRootInstances' must agree on what it+ -- means for a 'root-instances' entry to match an instance; both go+ -- through 'matchesInstancePattern'.+ InstanceRoot d c ->+ typeClassRoots+ || any ( \ip -> matchesInstancePattern analysis ip ( d, c ) ) ( configuredValue rootInstances ) - filteredInstances =- map instancePattern- . filter (maybe True (`matchTest` displayDeclaration c) . classPattern)- . filter (maybe True modulePathMatches . modulePattern)- $ rootInstances - modulePathMatches p = maybe False (p `matchTest`) (Map.lookup ( declModule d ) modulePaths)+-- | Does a @root-instances@ pattern match a given instance root (the+-- declaration of the instance and the declaration of its parent class)? An+-- absent field always matches.+--+-- [ref:RootInstanceMatching]+matchesInstancePattern+ :: Analysis -> InstancePattern CompiledRegex -> ( Declaration, Declaration ) -> Bool+matchesInstancePattern Analysis{ prettyPrintedType, modulePaths } ip ( d, c ) =+ maybe True moduleMatches ( modulePattern ip )+ && maybe True classMatches ( classPattern ip )+ && maybe True typeMatches ( instancePattern ip )+ where+ moduleMatches p =+ maybe False ( matchTest ( compiledRegex p ) ) ( Map.lookup ( declModule d ) modulePaths )++ classMatches p =+ matchTest ( compiledRegex p ) ( displayDeclaration c )++ typeMatches p =+ maybe False ( matchTest ( compiledRegex p ) ) ( Map.lookup d prettyPrintedType ) displayDeclaration :: Declaration -> String
test/Main.hs view
@@ -78,7 +78,10 @@ handle (\e -> hPrint stderr (e :: IOException)) $ writeFile (hieDirectory <.> ".dot") graph' -- Normalize weedPackage. We get different values here based on our version of cabal-install/ghc.- let weeds' = map (\weed -> weed {Weeder.Run.weedPackage = "main"}) weeds+ let normalize w = case w of+ Weeder.Run.WeedDeclaration d -> Weeder.Run.WeedDeclaration d {Weeder.Run.weedPackage = "main"}+ _ -> w+ weeds' = map normalize weeds pure (LBS.fromStrict $ encodeUtf8 $ pack $ unlines $ map Weeder.Run.formatWeed weeds') where configExpr = hieDirectory <.> ".toml"
+ test/Spec/DefaultsNotReported.stdout view
+ test/Spec/DefaultsNotReported.toml view
@@ -0,0 +1,6 @@+# roots and root-instances are deliberately left unset, so they fall back to+# their defaults (which match nothing here). root-modules is set explicitly to+# keep the module alive. Self-weeding must stay silent: defaults are not weeds.+root-modules = [ '^Spec\.DefaultsNotReported\.' ]++type-class-roots = false
+ test/Spec/DefaultsNotReported/DefaultsNotReported.hs view
@@ -0,0 +1,10 @@+module Spec.DefaultsNotReported.DefaultsNotReported where++-- This module is kept entirely alive by an explicit `root-modules` entry, so+-- there are no dead declarations. The point of the test is that the *default*+-- `roots` (`Main.main`, `^Paths_.*`) and the default `root-instances`+-- (`IsString`, `IsList`) -- none of which match anything here -- are NOT+-- reported, because they were never configured explicitly. The output is empty.++value :: Int+value = 5
+ test/Spec/SelfWeeding.stdout view
@@ -0,0 +1,3 @@+no declaration matches roots entry "Spec.SelfWeeding.SelfWeeding.doesNotExist"+no instance matches root-instances entry { class = "\\.NoSuchClass$" }+no module matches root-modules entry "^No.Such.Module$"
+ test/Spec/SelfWeeding.toml view
@@ -0,0 +1,9 @@+roots = [ "Spec.SelfWeeding.SelfWeeding.root"+ , "Spec.SelfWeeding.SelfWeeding.doesNotExist"+ ]++type-class-roots = false++root-instances = [ { class = '\.NoSuchClass$' } ]++root-modules = [ "^No.Such.Module$" ]
+ test/Spec/SelfWeeding/SelfWeeding.hs view
@@ -0,0 +1,12 @@+module Spec.SelfWeeding.SelfWeeding where++-- A single reachable root and a helper it uses, so there are no dead+-- declarations: the only weeds in this test come from the configuration+-- itself (a root pattern, a root-instance and a root-module that match+-- nothing).++root :: Int+root = helper + 1++helper :: Int+helper = 41
test/UnitTests/Weeder/ConfigSpec.hs view
@@ -15,11 +15,11 @@ configToTomlTests :: Assertion configToTomlTests = let cf = Config- { rootPatterns = mempty+ { rootPatterns = Configured mempty , typeClassRoots = True- , rootInstances = [InstanceOnly "Quux\\\\[\\]", ClassOnly "[\\[\\\\[baz" <> ModuleOnly "[Quuux]", InstanceOnly "[\\[\\\\[baz" <> ClassOnly "[Quuux]" <> ModuleOnly "[Quuuux]"]+ , rootInstances = Configured [InstanceOnly "Quux\\\\[\\]", ClassOnly "[\\[\\\\[baz" <> ModuleOnly "[Quuux]", InstanceOnly "[\\[\\\\[baz" <> ClassOnly "[Quuux]" <> ModuleOnly "[Quuuux]"] , unusedTypes = True- , rootModules = ["Foo\\.Bar", "Baz"]+ , rootModules = Configured ["Foo\\.Bar", "Baz"] } cf' = T.pack $ configToToml cf in TOML.decode cf' `shouldBe` Right cf
weeder.cabal view
@@ -5,7 +5,7 @@ author: Ollie Charles <ollie@ocharles.org.uk> maintainer: Ollie Charles <ollie@ocharles.org.uk> build-type: Simple-version: 2.10.0+version: 2.11.0 copyright: Neil Mitchell 2017-2020, Oliver Charles 2020-2025 synopsis: Detect dead code description: Find declarations.@@ -26,20 +26,20 @@ library build-depends:- , algebraic-graphs ^>= 0.7+ , algebraic-graphs >= 0.7 && <0.9 , async ^>= 2.2.0- , base >= 4.17 && < 4.22+ , base >= 4.17 && < 4.23 , bytestring >= 0.10.9 && < 0.13 , containers >= 0.6.2.1 && < 0.9 , directory ^>= 1.3.3.2 , filepath ^>= 1.4.2.1 || ^>= 1.5- , generic-lens ^>= 2.2.0.0- , ghc >= 9.4 && < 9.13+ , generic-lens >= 2.2 && <2.4+ , ghc >= 9.4 && < 9.16 , Glob ^>= 0.9 || ^>= 0.10 , lens >= 5.1 && < 5.4 , mtl ^>= 2.2.2 || ^>= 2.3 , optparse-applicative >= 0.14.3 && < 0.20- , parallel ^>= 3.2.0.0+ , parallel >= 3.2 && <3.4 , regex-tdfa ^>= 1.2.0.0 || ^>= 1.3.1.0 , text ^>= 2.0.1 || ^>= 2.1 , toml-reader >= 0.2.0.0 && < 0.3.1@@ -100,6 +100,7 @@ Spec.ConfigInstanceModules.Module1 Spec.ConfigInstanceModules.Module2 Spec.ConfigInstanceModules.Module3+ Spec.DefaultsNotReported.DefaultsNotReported Spec.DeriveGeneric.DeriveGeneric Spec.InstanceRootConstraint.InstanceRootConstraint Spec.InstanceTypeclass.InstanceTypeclass@@ -113,6 +114,7 @@ Spec.OverloadedStrings.OverloadedStrings Spec.RangeEnum.RangeEnum Spec.RootClasses.RootClasses+ Spec.SelfWeeding.SelfWeeding Spec.StandaloneDeriving.StandaloneDeriving Spec.TypeAliasGADT.TypeAliasGADT Spec.TypeDataDecl.TypeDataDecl