packages feed

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 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