weeder 2.6.0 → 2.7.0
raw patch · 78 files changed
+1632/−250 lines, 78 filesdep +asyncdep +paralleldep −silentlydep ~containersnew-uploader
Dependencies added: async, parallel
Dependencies removed: silently
Dependency ranges changed: containers
Files
- CHANGELOG.md +10/−0
- README.md +61/−12
- src/Weeder.hs +440/−84
- src/Weeder/Config.hs +177/−9
- src/Weeder/Main.hs +201/−115
- src/Weeder/Run.hs +159/−0
- test/Spec.hs +50/−26
- test/Spec/ApplicativeDo.failing +2/−0
- test/Spec/ApplicativeDo.stdout +0/−0
- test/Spec/ApplicativeDo.toml +3/−0
- test/Spec/ApplicativeDo/ApplicativeDo.hs +17/−0
- test/Spec/ConfigInstanceModules.stdout +2/−0
- test/Spec/ConfigInstanceModules.toml +6/−0
- test/Spec/ConfigInstanceModules/Module1.hs +3/−0
- test/Spec/ConfigInstanceModules/Module2.hs +3/−0
- test/Spec/ConfigInstanceModules/Module3.hs +3/−0
- test/Spec/DeriveGeneric.stdout +1/−0
- test/Spec/DeriveGeneric.toml +3/−0
- test/Spec/DeriveGeneric/DeriveGeneric.hs +15/−0
- test/Spec/InstanceRootConstraint.stdout +0/−0
- test/Spec/InstanceRootConstraint.toml +5/−0
- test/Spec/InstanceRootConstraint/InstanceRootConstraint.hs +13/−0
- test/Spec/InstanceTypeclass.stdout +2/−0
- test/Spec/InstanceTypeclass.toml +5/−0
- test/Spec/InstanceTypeclass/InstanceTypeclass.hs +20/−0
- test/Spec/Monads.failing +3/−0
- test/Spec/Monads.stdout +0/−0
- test/Spec/Monads.toml +3/−0
- test/Spec/Monads/Monads.hs +38/−0
- test/Spec/NumInstance.stdout +0/−0
- test/Spec/NumInstance.toml +3/−0
- test/Spec/NumInstance/NumInstance.hs +17/−0
- test/Spec/NumInstanceLiteral.failing +1/−0
- test/Spec/NumInstanceLiteral.stdout +0/−0
- test/Spec/NumInstanceLiteral.toml +5/−0
- test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs +12/−0
- test/Spec/OverloadedLabels.stdout +1/−0
- test/Spec/OverloadedLabels.toml +3/−0
- test/Spec/OverloadedLabels/OverloadedLabels.hs +24/−0
- test/Spec/OverloadedLists.failing +1/−0
- test/Spec/OverloadedLists.stdout +0/−0
- test/Spec/OverloadedLists.toml +6/−0
- test/Spec/OverloadedLists/OverloadedLists.hs +16/−0
- test/Spec/OverloadedStrings.failing +1/−0
- test/Spec/OverloadedStrings.stdout +0/−0
- test/Spec/OverloadedStrings.toml +8/−0
- test/Spec/OverloadedStrings/OverloadedStrings.hs +22/−0
- test/Spec/RangeEnum.failing +1/−0
- test/Spec/RangeEnum.stdout +0/−0
- test/Spec/RangeEnum.toml +7/−0
- test/Spec/RangeEnum/RangeEnum.hs +26/−0
- test/Spec/RootClasses.stdout +1/−0
- test/Spec/RootClasses.toml +5/−0
- test/Spec/RootClasses/RootClasses.hs +15/−0
- test/Spec/StandaloneDeriving.stdout +1/−0
- test/Spec/StandaloneDeriving.toml +5/−0
- test/Spec/StandaloneDeriving/StandaloneDeriving.hs +13/−0
- test/Spec/TypeAliasGADT.stdout +0/−0
- test/Spec/TypeAliasGADT.toml +7/−0
- test/Spec/TypeAliasGADT/TypeAliasGADT.hs +14/−0
- test/Spec/TypeDataDecl.stdout +0/−0
- test/Spec/TypeDataDecl.toml +7/−0
- test/Spec/TypeDataDecl/TypeDataDecl.hs +12/−0
- test/Spec/TypeFamilies.failing +0/−0
- test/Spec/TypeFamilies.stdout +1/−0
- test/Spec/TypeFamilies.toml +5/−0
- test/Spec/TypeFamilies/TypeFamilies.hs +9/−0
- test/Spec/TypeFamilies/TypeFamilyUsage.hs +6/−0
- test/Spec/Types.stdout +1/−0
- test/Spec/Types.toml +9/−0
- test/Spec/Types/Types.hs +19/−0
- test/Spec/Types/Usages.hs +23/−0
- test/Spec/TypesUnused.stdout +4/−0
- test/Spec/TypesUnused.toml +5/−0
- test/Spec/TypesUnused/TypesUnused.hs +12/−0
- test/UnitTests.hs +1/−0
- test/UnitTests/Weeder/ConfigSpec.hs +25/−0
- weeder.cabal +33/−4
CHANGELOG.md view
@@ -5,6 +5,16 @@ and is generated by [Changie](https://github.com/miniscruff/changie). +## 2.7.0 - 2023-08-17+### Added+* Weeder now supports type class instances. Type class instances can be marked as roots with the `root-instances` configuration option. (#126, #133, #136)+* Weeder now optionally detects uses of types, excluding type family instances. This can be enabled with the `unused-types` configuration option. (#132)+* Weeder's analysis now runs in parallel. This can almost halve execution time when given enough cores. Enabled by calling Weeder with `-j X` or `-N`. (#137)+* `--write-default-config` flag to write and read a default configuration, if no configuration file is found. (#133)+### Changed+* All configuration options now have default values. This can be disabled with the `--no-default-fields` flag. (#133)+* Weeder will now use distinct exit codes for certain failures. See `README.md` for more details. (#134)+ ## 2.6.0 - 2023-07-07 ### Added * Weeder now supports GHC 9.6.
README.md view
@@ -64,10 +64,8 @@ add all exported functions as roots automatically but in many cases `main` from a test suite could be a good workaround for that -`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`:+`type-class-roots` configures whether or not Weeder should consider all instances+of type classes as roots. Defaults to `false`. ``` toml roots = [ "^Main.main$" ]@@ -87,25 +85,76 @@ (Please note these warnings are just for demonstration and not necessarily weeds in the Dhall project). +## Configuration options++| Name | Default value | Description |+| ---------------- | ------------------------------------ | --- |+| roots | `[ "Main.main", "^Paths_weeder.*" ]` | Any declarations matching these regular expressions will be considered as alive. |+| type-class-roots | `false` | Consider all instances of type classes as roots. Overrides `root-instances`. |+| root-instances | `[ {class = '\.IsString$'}, {class = '\.IsList$'} ]` | Type class instances that match on all specified fields will be considered as roots. Accepts the fields `instance` matching on the pretty-printed type of the instance (visible in the output), `class` matching on its parent class declaration, and `module` matching on the module the instance is in. |+| unused-types | `false` | Enable analysis of unused types. |++`root-instances` can also accept string literals as a shorthand for writing a table+containing only the `instance` field. See the following example from the test suite:++``` toml+root-instances = [ { module = "Spec.ConfigInstanceModules.Module1", instance = "Bounded T" }+ , "Read T" + , { module = "Spec.ConfigInstanceModules.Module3" }+ , { class = '\.Enum$' }+ , { module = "Spec.ConfigInstanceModules.Module2", class = '\.Show$' }+ ]+```++## Exit codes++Weeder emits the following exit codes:++| Exit code | Cause |+| --- | --- |+| 0 | No weeds were found |+| 228 | One or more weeds found |+| 1 | Generic failing exit code |+| 2 | Failure to read HIE file due to GHC version mismatch |+| 3 | Failure to parse config file |+| 4 | No HIE files found |+ # Tips - You may want to add `^Paths_.*` to the roots in `weeder.toml` to ignore the `Paths_packageName` module automatically generated by Cabal. +- You can automatically write and use a default configuration file by calling + Weeder with the `--write-default-config` flag, if no configuration file is+ found.++- You can mandate explicitly specifying every option in the configuration by + calling Weeder with the `--no-default-fields` flag. This can prevent being+ caught off guard by new configuration options or changes to default values.++- To mark all instances in a module `M` as roots, add `{ module = "^M$" }`+ to `root-instances`.+ # Limitations Weeder currently has a few limitations: -## Type Class Instances+## Overloaded syntax -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.+On some versions of GHC, Weeder might report various type classes that are used+for syntax extensions as weeds. For example, `Num` and `IsString` classes might be+flagged as weeds if they are only used for overloaded literal syntax (that is,+the `fromInteger` and `fromString` methods). -You can toggle whether Weeder consider type class instances as roots with the-`type-class-roots` configuration option.+You can add instances of specific type classes as roots with the `root-instances` +field, or toggle whether Weeder considers all type class instances as roots with +the `type-class-roots` configuration option.++## Type families++Weeder cannot yet analyse uses of type family instances. For this reason type+family instances will be marked as implicit roots if analysis of types is+enabled via `unused-types`. ## Template Haskell
src/Weeder.hs view
@@ -1,19 +1,22 @@ {-# language ApplicativeDo #-} {-# language BlockArguments #-} {-# language DeriveGeneric #-}+{-# language DeriveAnyClass #-} {-# language FlexibleContexts #-} {-# language LambdaCase #-} {-# language NamedFieldPuns #-} {-# language NoImplicitPrelude #-} {-# language OverloadedLabels #-} {-# language OverloadedStrings #-}+{-# language TupleSections #-} module Weeder ( -- * Analysis Analysis(..)+ , analyseEvidenceUses , analyseHieFile , emptyAnalysis- , allDeclarations+ , outputableDeclarations -- ** Reachability , Root(..)@@ -25,15 +28,18 @@ where -- algebraic-graphs-import Algebra.Graph ( Graph, edge, empty, overlay, vertex, vertexList )+import Algebra.Graph ( Graph, edge, empty, overlay, vertex, stars, star, overlays ) import Algebra.Graph.ToGraph ( dfs ) -- base import Control.Applicative ( Alternative )-import Control.Monad ( guard, msum, when )-import Data.Foldable ( for_, traverse_ )+import Control.Monad ( guard, msum, when, unless, mzero )+import Data.Traversable ( for )+import Data.Maybe ( mapMaybe )+import Data.Foldable ( for_, traverse_, toList )+import Data.Function ( (&) ) import Data.List ( intercalate )-import Data.Monoid ( First( First ) )+import Data.Monoid ( First( First ), getFirst ) import GHC.Generics ( Generic ) import Prelude hiding ( span ) @@ -43,6 +49,8 @@ import Data.Sequence ( Seq ) import Data.Set ( Set ) import qualified Data.Set as Set+import Data.Tree (Tree)+import qualified Data.Tree as Tree -- generic-lens import Data.Generics.Labels ()@@ -56,18 +64,38 @@ import GHC.Types.FieldLabel ( FieldLabel( FieldLabel, flSelector ) ) import GHC.Iface.Ext.Types ( BindType( RegularBind )- , ContextInfo( Decl, ValBind, PatternBind, Use, TyDecl, ClassTyDecl )- , DeclType( DataDec, ClassDec, ConDec )+ , ContextInfo( Decl, ValBind, PatternBind, Use, TyDecl, ClassTyDecl, EvidenceVarBind, RecField )+ , DeclType( DataDec, ClassDec, ConDec, SynDec, FamDec )+ , EvVarSource ( EvInstBind, cls ) , HieAST( Node, nodeChildren, nodeSpan, sourcedNodeInfo ) , HieASTs( HieASTs )- , HieFile( HieFile, hie_asts, hie_exports, hie_module, hie_hs_file )- , IdentifierDetails( IdentifierDetails, identInfo )+ , HieFile( HieFile, hie_asts, hie_exports, hie_module, hie_hs_file, hie_types )+ , HieType( HTyVarTy, HAppTy, HTyConApp, HForAllTy, HFunTy, HQualTy, HLitTy, HCastTy, HCoercionTy )+ , HieArgs( HieArgs )+ , HieTypeFix( Roll )+ , IdentifierDetails( IdentifierDetails, identInfo, identType ) , NodeAnnotation( NodeAnnotation, nodeAnnotType ) , NodeInfo( nodeIdentifiers, nodeAnnotations ) , Scope( ModuleScope )+ , RecFieldContext ( RecFieldOcc )+ , TypeIndex , getSourcedNodeInfo )+import GHC.Iface.Ext.Utils+ ( EvidenceInfo( EvidenceInfo, evidenceVar )+ , RefMap+ , findEvidenceUse+ , getEvidenceTree+ , hieTypeToIface+ , recoverFullType+ ) import GHC.Unit.Module ( Module, moduleStableString )+import GHC.Utils.Outputable ( defaultSDocContext, showSDocOneLine )+import GHC.Iface.Type+ ( ShowForAllFlag (ShowForAllWhen)+ , pprIfaceSigmaType+ , IfaceTyCon (IfaceTyCon, ifaceTyConName)+ ) import GHC.Types.Name ( Name, nameModule_maybe, nameOccName , OccName@@ -78,18 +106,26 @@ , isVarOcc , occNameString )-import GHC.Types.SrcLoc ( RealSrcSpan, realSrcSpanEnd, realSrcSpanStart )+import GHC.Types.SrcLoc ( RealSrcSpan, realSrcSpanEnd, realSrcSpanStart, srcLocLine ) -- lens import Control.Lens ( (%=) ) -- mtl import Control.Monad.State.Class ( MonadState )+import Control.Monad.Reader.Class ( MonadReader, asks ) +-- parallel+import Control.Parallel.Strategies ( NFData )+ -- transformers import Control.Monad.Trans.Maybe ( runMaybeT )+import Control.Monad.Trans.Reader ( runReaderT ) +-- weeder+import Weeder.Config ( Config, ConfigType( Config, typeClassRoots, unusedTypes ) ) + data Declaration = Declaration { declModule :: Module@@ -98,7 +134,7 @@ -- ^ The symbol name of a declaration. } deriving- ( Eq, Ord )+ ( Eq, Ord, Generic, NFData ) instance Show Declaration where@@ -126,39 +162,70 @@ Analysis { dependencyGraph :: Graph Declaration -- ^ A graph between declarations, capturing dependencies.- , declarationSites :: Map Declaration ( Set RealSrcSpan )- -- ^ A partial mapping between declarations and their definition site.+ , declarationSites :: Map Declaration (Set Int)+ -- ^ A partial mapping between declarations and their line numbers. -- 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+ -- We capture a set of sites, 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.+ , implicitRoots :: Set Root+ -- ^ Stores information on Declarations that may be automatically marked+ -- as always reachable. This is used, for example, to capture knowledge + -- not yet modelled in weeder, or to mark all instances of a class as + -- roots. , 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.+ , prettyPrintedType :: Map Declaration String+ -- ^ Used to match against the types of instances and to replace the+ -- appearance of declarations in the output+ , requestedEvidence :: Map Declaration (Set Name)+ -- ^ Map from declarations to the names containing evidence uses that+ -- should be followed and treated as dependencies of the declaration.+ -- We use this to be able to delay analysing evidence uses until later,+ -- allowing us to begin the rest of the analysis before we have read all+ -- hie files. } deriving- ( Generic )+ ( Generic, NFData ) +instance Semigroup Analysis where+ (<>) (Analysis a1 b1 c1 d1 e1 f1 g1) (Analysis a2 b2 c2 d2 e2 f2 g2)= + Analysis (a1 `overlay` a2) (Map.unionWith (<>) b1 b2) (c1 <> c2) (Map.unionWith (<>) d1 d2) (e1 <> e2) (f1 <> f2) (Map.unionWith (<>) g1 g2)+++instance Monoid Analysis where+ mempty = emptyAnalysis+++data AnalysisInfo =+ AnalysisInfo+ { currentHieFile :: HieFile+ , weederConfig :: Config+ }++ -- | The empty analysis - the result of analysing zero @.hie@ files. emptyAnalysis :: Analysis-emptyAnalysis = Analysis empty mempty mempty mempty mempty+emptyAnalysis = Analysis empty mempty mempty mempty mempty mempty mempty -- | A root for reachability analysis. data Root = -- | A given declaration is a root. DeclarationRoot Declaration+ | -- | We store extra information for instances in order to be able+ -- to specify e.g. all instances of a class as roots.+ InstanceRoot + Declaration -- ^ Declaration of the instance+ Declaration -- ^ Declaration of the parent class | -- | All exported declarations in a module are roots. ModuleRoot Module deriving- ( Eq, Ord )+ ( Eq, Ord, Generic, NFData ) -- | Determine the set of all declaration reachable from a set of roots.@@ -170,27 +237,100 @@ rootDeclarations = \case DeclarationRoot d -> [ d ]+ InstanceRoot d _ -> [ d ] -- filter InstanceRoots in `Main.hs` 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 )+-- | The set of all declarations that could possibly+-- appear in the output.+outputableDeclarations :: Analysis -> Set Declaration+outputableDeclarations Analysis{ declarationSites } =+ Map.keysSet declarationSites +-- Generate an initial graph of the current HieFile.+initialGraph :: AnalysisInfo -> Graph Declaration+initialGraph info =+ let hf@HieFile{ hie_asts = HieASTs hieAsts } = currentHieFile info+ Config{ unusedTypes } = weederConfig info+ asts = Map.elems hieAsts+ decls = concatMap (toList . findIdentifiers' (const True)) asts+ in if unusedTypes+ then stars do + (d, IdentifierDetails{identType}, _) <- decls+ t <- maybe mzero pure identType+ let ns = Set.toList $ typeToNames (lookupType hf t)+ ds = mapMaybe nameToDeclaration ns+ guard $ not (null ds)+ pure (d, ds)+ else mempty++ -- | 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 } = do+analyseHieFile :: (MonadState Analysis m) => Config -> HieFile -> m ()+analyseHieFile weederConfig hieFile =+ let info = AnalysisInfo hieFile weederConfig+ in runReaderT analyseHieFile' info+++analyseHieFile' :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => m ()+analyseHieFile' = do+ HieFile{ hie_asts = HieASTs hieASTs, hie_exports, hie_module, hie_hs_file } <- asks currentHieFile #modulePaths %= Map.insert hie_module hie_hs_file+ + g <- asks initialGraph+ #dependencyGraph %= overlay g - for_ hieASTs \ast -> do- addAllDeclarations ast- topLevelAnalysis ast+ for_ hieASTs topLevelAnalysis for_ hie_exports ( analyseExport hie_module ) +lookupType :: HieFile -> TypeIndex -> HieTypeFix+lookupType hf t = recoverFullType t $ hie_types hf+++lookupPprType :: MonadReader AnalysisInfo m => TypeIndex -> m String+lookupPprType t = do+ hf <- asks currentHieFile+ pure . renderType $ lookupType hf t++ where++ renderType = showSDocOneLine defaultSDocContext . pprIfaceSigmaType ShowForAllWhen . hieTypeToIface+++-- | Names mentioned within the type.+typeToNames :: HieTypeFix -> Set Name+typeToNames (Roll t) = case t of+ HTyVarTy n -> Set.singleton n++ HAppTy a (HieArgs args) ->+ typeToNames a <> hieArgsTypes args++ HTyConApp (IfaceTyCon{ifaceTyConName}) (HieArgs args) ->+ Set.singleton ifaceTyConName <> hieArgsTypes args++ HForAllTy _ a -> typeToNames a++ HFunTy _mult b c ->+ typeToNames b <> typeToNames c++ HQualTy a b ->+ typeToNames a <> typeToNames b++ HLitTy _ -> mempty++ HCastTy a -> typeToNames a++ HCoercionTy -> mempty++ where++ hieArgsTypes :: [(Bool, HieTypeFix)] -> Set Name+ hieArgsTypes = foldMap (typeToNames . snd) . filter fst++ analyseExport :: MonadState Analysis m => Module -> AvailInfo -> m () analyseExport m = \case Avail (NormalGreName name) ->@@ -203,8 +343,8 @@ for_ ( nameToDeclaration name ) addExport for_ pieces \case- NormalGreName name ->- traverse_ addExport $ nameToDeclaration name+ NormalGreName name' ->+ traverse_ addExport $ nameToDeclaration name' FieldGreName (FieldLabel{ flSelector }) -> traverse_ addExport $ nameToDeclaration flSelector@@ -223,43 +363,48 @@ addImplicitRoot :: MonadState Analysis m => Declaration -> m () addImplicitRoot x =- #implicitRoots %= Set.insert x+ #implicitRoots %= Set.insert (DeclarationRoot x) +addInstanceRoot :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => Declaration -> TypeIndex -> Name -> m ()+addInstanceRoot x t cls = do+ for_ (nameToDeclaration cls) \cls' ->+ #implicitRoots %= Set.insert (InstanceRoot x cls')++ -- since instances will not appear in the output if typeClassRoots is True+ Config{ typeClassRoots } <- asks weederConfig+ unless typeClassRoots $ do+ str <- lookupPprType t+ #prettyPrintedType %= Map.insert x str++ 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 )+ #declarationSites %= Map.insertWith Set.union decl ( Set.singleton . srcLocLine $ realSrcSpanStart 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 = do- for_ ( findIdentifiers ( const True ) n ) addDeclaration---topLevelAnalysis :: MonadState Analysis m => HieAST a -> m ()+topLevelAnalysis :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m () topLevelAnalysis n@Node{ nodeChildren } = do+ Config{ unusedTypes } <- asks weederConfig analysed <- runMaybeT- ( msum+ ( msum $ [- -- analyseStandaloneDeriving n- -- ,- analyseInstanceDeclaration n+ analyseStandaloneDeriving n+ , analyseInstanceDeclaration n , analyseBinding n , analyseRewriteRule n , analyseClassDeclaration n , analyseDataDeclaration n , analysePatternSynonyms n- ]+ ] ++ if unusedTypes then+ [ analyseTypeSynonym n+ , analyseFamilyDeclaration n+ , analyseFamilyInstance n+ , analyseTypeSignature n+ ] else [] ) case analysed of@@ -273,38 +418,60 @@ return () -analyseBinding :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()-analyseBinding n@Node{ nodeSpan, sourcedNodeInfo } = do+annsContain :: HieAST a -> (String, String) -> Bool+annsContain Node{ sourcedNodeInfo } ann =+ any (Set.member ann . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo+++analyseBinding :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST a -> m ()+analyseBinding n@Node{ nodeSpan } = do let bindAnns = Set.fromList [("FunBind", "HsBindLR"), ("PatBind", "HsBindLR")]- guard $ any (not . Set.disjoint bindAnns . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo+ guard $ any (annsContain n) bindAnns for_ ( findDeclarations n ) \d -> do define d nodeSpan + requestEvidence n d+ for_ ( uses n ) $ addDependency d analyseRewriteRule :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()-analyseRewriteRule n@Node{ sourcedNodeInfo } = do- guard $ any (Set.member ("HsRule", "RuleDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo+analyseRewriteRule n = do+ guard $ annsContain n ("HsRule", "RuleDecl") for_ ( uses n ) addImplicitRoot -analyseInstanceDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()-analyseInstanceDeclaration n@Node{ sourcedNodeInfo } = do- guard $ any (Set.member ("ClsInstD", "InstDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo+analyseInstanceDeclaration :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m ()+analyseInstanceDeclaration n@Node{ nodeSpan } = do+ guard $ annsContain n ("ClsInstD", "InstDecl") - traverse_ addImplicitRoot ( uses n )+ for_ ( findEvInstBinds n ) \(d, cs, ids, _) -> do+ -- This makes instance declarations show up in + -- the output if type-class-roots is set to False.+ define d nodeSpan + requestEvidence n d -analyseClassDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()-analyseClassDeclaration n@Node{ sourcedNodeInfo } = do- guard $ any (Set.member ("ClassDecl", "TyClDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo+ for_ ( uses n ) $ addDependency d - for_ ( findIdentifiers isClassDeclaration n ) $- for_ ( findIdentifiers ( const True ) n ) . addDependency+ case identType ids of+ Just t -> for_ cs (addInstanceRoot d t)+ Nothing -> pure () ++analyseClassDeclaration :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST a -> m ()+analyseClassDeclaration n@Node{ nodeSpan } = do+ guard $ annsContain n ("ClassDecl", "TyClDecl")++ for_ ( findIdentifiers isClassDeclaration n ) $ \d -> do+ define d nodeSpan++ requestEvidence n d++ (for_ ( findIdentifiers ( const True ) n ) . addDependency) d+ where isClassDeclaration =@@ -316,22 +483,44 @@ False -analyseDataDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()-analyseDataDeclaration n@Node{ sourcedNodeInfo } = do- guard $ any (Set.member ("DataDecl", "TyClDecl") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo+analyseDataDeclaration :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m ()+analyseDataDeclaration n = do+ guard $ annsContain n ("DataDecl", "TyClDecl") + Config{ unusedTypes } <- asks weederConfig+ for_ ( foldMap ( First . Just ) ( findIdentifiers ( any isDataDec ) n ) )- \dataTypeName ->- for_ ( constructors n ) \constructor ->- for_ ( foldMap ( First . Just ) ( findIdentifiers ( any isConDec ) constructor ) ) \conDec -> do+ \dataTypeName -> do+ when unusedTypes $+ define dataTypeName (nodeSpan n)++ -- Without connecting constructors to the data declaration TypeAliasGADT.hs + -- fails with a false positive for A+ conDecs <- for ( constructors n ) \constructor ->+ for ( foldMap ( First . Just ) ( findIdentifiers ( any isConDec ) constructor ) ) \conDec -> do addDependency conDec dataTypeName+ pure conDec - for_ ( uses constructor ) ( addDependency conDec )+ -- To keep acyclicity in record declarations+ let isDependent d = Just d `elem` fmap getFirst conDecs + for_ ( uses n ) (\d -> unless (isDependent d) $ addDependency dataTypeName d)++ for_ ( derivedInstances n ) \(d, cs, ids, ast) -> do+ define d (nodeSpan ast)++ requestEvidence ast d++ for_ ( uses ast ) $ addDependency d++ case identType ids of+ Just t -> for_ cs (addInstanceRoot d t)+ Nothing -> pure ()+ where isDataDec = \case@@ -344,19 +533,124 @@ constructors :: HieAST a -> Seq ( HieAST a )-constructors n@Node{ nodeChildren, sourcedNodeInfo } =- if any (any ( ("ConDecl" ==) . unpackFS . nodeAnnotType) . nodeAnnotations) (getSourcedNodeInfo sourcedNodeInfo) then+constructors = findNodeTypes "ConDecl"+++derivedInstances :: HieAST a -> Seq (Declaration, Set Name, IdentifierDetails a, HieAST a)+derivedInstances n = findNodeTypes "HsDerivingClause" n >>= findEvInstBinds+++findNodeTypes :: String -> HieAST a -> Seq ( HieAST a )+findNodeTypes t n@Node{ nodeChildren, sourcedNodeInfo } =+ if any (any ( (t ==) . unpackFS . nodeAnnotType) . nodeAnnotations) (getSourcedNodeInfo sourcedNodeInfo) then pure n else- foldMap constructors nodeChildren+ foldMap (findNodeTypes t) nodeChildren ++analyseStandaloneDeriving :: ( Alternative m, MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST TypeIndex -> m ()+analyseStandaloneDeriving n@Node{ nodeSpan } = do+ guard $ annsContain n ("DerivDecl", "DerivDecl")++ for_ (findEvInstBinds n) \(d, cs, ids, _) -> do+ define d nodeSpan++ requestEvidence n d++ for_ (uses n) $ addDependency d++ case identType ids of+ Just t -> for_ cs (addInstanceRoot d t)+ Nothing -> pure ()+++analyseTypeSynonym :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseTypeSynonym n@Node{ nodeSpan } = do+ guard $ annsContain n ("SynDecl", "TyClDecl")++ for_ ( findIdentifiers isTypeSynonym n ) $ \d -> do+ define d nodeSpan++ for_ (uses n) (addDependency d)++ where++ isTypeSynonym =+ any \case+ Decl SynDec _ -> True+ _ -> False+++analyseFamilyDeclaration :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseFamilyDeclaration n@Node{ nodeSpan } = do+ guard $ annsContain n ("FamDecl", "TyClDecl")++ for_ ( findIdentifiers isFamDec n ) $ \d -> do+ define d nodeSpan++ for_ (uses n) (addDependency d)++ where++ isFamDec =+ any \case+ Decl FamDec _ -> True+ _ -> False+++analyseFamilyInstance :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseFamilyInstance n = do+ guard $ annsContain n ("TyFamInstD", "InstDecl")++ for_ ( uses n ) addImplicitRoot+++analyseTypeSignature :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()+analyseTypeSignature n = do+ guard $ annsContain n ("TypeSig", "Sig")++ for_ (findIdentifiers isTypeSigDecl n) $+ for_ ( uses n ) . addDependency++ where++ isTypeSigDecl =+ any \case+ TyDecl -> True+ _ -> False++ analysePatternSynonyms :: ( Alternative m, MonadState Analysis m ) => HieAST a -> m ()-analysePatternSynonyms n@Node{ sourcedNodeInfo } = do- guard $ any (Set.member ("PatSynBind", "HsBindLR") . Set.map unNodeAnnotation . nodeAnnotations) $ getSourcedNodeInfo sourcedNodeInfo+analysePatternSynonyms n = do+ guard $ annsContain n ("PatSynBind", "HsBindLR") for_ ( findDeclarations n ) $ for_ ( uses n ) . addDependency ++findEvInstBinds :: HieAST a -> Seq (Declaration, Set Name, IdentifierDetails a, HieAST a)+findEvInstBinds n = (\(d, ids, ast) -> (d, getClassNames ids, ids, ast)) <$>+ findIdentifiers'+ ( not+ . Set.null+ . getEvVarSources+ ) n++ where++ getEvVarSources :: Set ContextInfo -> Set EvVarSource+ getEvVarSources = foldMap (maybe mempty Set.singleton) .+ Set.map \case+ EvidenceVarBind a@EvInstBind{} ModuleScope _ -> Just a+ _ -> Nothing++ getClassNames :: IdentifierDetails a -> Set Name+ getClassNames =+ Set.map cls+ . getEvVarSources+ . identInfo++ findDeclarations :: HieAST a -> Seq Declaration findDeclarations = findIdentifiers@@ -379,29 +673,49 @@ :: ( Set ContextInfo -> Bool ) -> HieAST a -> Seq Declaration-findIdentifiers f Node{ sourcedNodeInfo, nodeChildren } =+findIdentifiers f = fmap (\(d, _, _) -> d) . findIdentifiers' f+++-- | Version of findIdentifiers containing more information,+-- namely the IdentifierDetails of the declaration and the+-- node it was found in.+findIdentifiers'+ :: ( Set ContextInfo -> Bool )+ -> HieAST a+ -> Seq (Declaration, IdentifierDetails a, HieAST a)+findIdentifiers' f n@Node{ sourcedNodeInfo, nodeChildren } = foldMap- ( \case+ (\case ( Left _, _ ) -> mempty - ( Right name, IdentifierDetails{ identInfo } ) ->+ ( Right name, ids@IdentifierDetails{ identInfo } ) -> if f identInfo then- foldMap pure ( nameToDeclaration name )+ (, ids, n) <$> foldMap pure (nameToDeclaration name) else mempty ) (foldMap (Map.toList . nodeIdentifiers) (getSourcedNodeInfo sourcedNodeInfo))- <> foldMap ( findIdentifiers f ) nodeChildren+ <> foldMap ( findIdentifiers' f ) nodeChildren uses :: HieAST a -> Set Declaration uses = foldMap Set.singleton- . findIdentifiers \identInfo -> Use `Set.member` identInfo+ . findIdentifiers (any isUse) +isUse :: ContextInfo -> Bool+isUse = \case+ Use -> True+ -- not RecFieldMatch and RecFieldDecl because they occur under+ -- data declarations, which we do not want to add as dependencies+ -- because that would make the graph no longer acyclic+ -- RecFieldAssign will be most likely accompanied by the constructor+ RecField RecFieldOcc _ -> True+ _ -> False + nameToDeclaration :: Name -> Maybe Declaration nameToDeclaration name = do m <- nameModule_maybe name@@ -410,3 +724,45 @@ unNodeAnnotation :: NodeAnnotation -> (String, String) unNodeAnnotation (NodeAnnotation x y) = (unpackFS x, unpackFS y)+++-- | Add evidence uses found under the given node to 'requestedEvidence'.+requestEvidence :: ( MonadState Analysis m, MonadReader AnalysisInfo m ) => HieAST a -> Declaration -> m ()+requestEvidence n d = do+ Config{ typeClassRoots } <- asks weederConfig++ -- If type-class-roots flag is set then we don't need to follow+ -- evidence uses as the binding sites will be roots anyway+ unless typeClassRoots $+ #requestedEvidence %= Map.insertWith (<>) d (Set.fromList names)++ where++ names = concat . Tree.flatten $ evidenceUseTree n++ evidenceUseTree :: HieAST a -> Tree [Name]+ evidenceUseTree Node{ sourcedNodeInfo, nodeChildren } = Tree.Node+ { Tree.rootLabel = concatMap (findEvidenceUse . nodeIdentifiers) (getSourcedNodeInfo sourcedNodeInfo)+ , Tree.subForest = map evidenceUseTree nodeChildren+ }+++-- | Follow the given evidence uses back to their instance bindings,+-- and connect the declaration to those bindings.+followEvidenceUses :: RefMap TypeIndex -> Declaration -> Set Name -> Graph Declaration+followEvidenceUses refMap d names =+ let getEvidenceTrees = mapMaybe (getEvidenceTree refMap) . Set.toList+ evidenceInfos = concatMap Tree.flatten (getEvidenceTrees names)+ instanceEvidenceInfos = evidenceInfos & filter \case+ EvidenceInfo _ _ _ (Just (EvInstBind _ _, ModuleScope, _)) -> True+ _ -> False+ evBindSiteDecls = mapMaybe (nameToDeclaration . evidenceVar) instanceEvidenceInfos+ in star d evBindSiteDecls+++-- | Follow evidence uses listed under 'requestedEvidence' back to their +-- instance bindings, and connect their corresponding declaration to those bindings.+analyseEvidenceUses :: RefMap TypeIndex -> Analysis -> Analysis+analyseEvidenceUses rf a@Analysis{ requestedEvidence, dependencyGraph } =+ let graphs = map (uncurry (followEvidenceUses rf)) $ Map.toList requestedEvidence+ in a { dependencyGraph = overlays (dependencyGraph : graphs) }
src/Weeder/Config.hs view
@@ -2,30 +2,198 @@ {-# language BlockArguments #-} {-# language OverloadedStrings #-} {-# language RecordWildCards #-}+{-# language LambdaCase #-}+{-# language PatternSynonyms #-}+{-# language FlexibleInstances #-}+{-# language DeriveTraversable #-}+{-# language NamedFieldPuns #-} -module Weeder.Config ( Config(..) ) where+module Weeder.Config+ ( -- * Config+ Config+ , ConfigParsed+ , ConfigType(..)+ , compileConfig+ , configToToml+ , decodeNoDefaults+ , defaultConfig+ -- * Marking instances as roots+ , InstancePattern+ , modulePattern+ , instancePattern+ , classPattern+ , pattern InstanceOnly+ , pattern ClassOnly+ , pattern ModuleOnly+ )+ where +-- base+import Control.Applicative ((<|>), empty)+import Data.Bifunctor (bimap)+import Data.Char (toLower)+import Data.List (intersperse, intercalate)+ -- containers-import Data.Set ( Set )+import Data.Containers.ListUtils (nubOrd) +-- regex-tdfa+import Text.Regex.TDFA ( Regex, RegexOptions ( defaultExecOpt, defaultCompOpt ) )+import Text.Regex.TDFA.TDFA ( patternToRegex )+import Text.Regex.TDFA.ReadRegex ( parseRegex )+ -- toml-reader import qualified TOML -- | Configuration for Weeder analysis.-data Config = Config- { rootPatterns :: Set String+type Config = ConfigType Regex+++-- | Configuration that has been parsed from TOML (and can still be +-- converted back), but not yet compiled to a 'Config'.+type ConfigParsed = ConfigType String+++-- | Underlying type for 'Config' and 'ConfigParsed'.+data ConfigType a = Config+ { rootPatterns :: [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. Weeder is currently unable to identify whether or not a type class- -- instance is used - enabling this option can prevent false positives.+ -- set. Overrides root-instances.+ , rootInstances :: [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.+ } deriving (Eq, Show)+++-- | Construct via InstanceOnly, ClassOnly or ModuleOnly, +-- and combine with the Semigroup instance. The Semigroup+-- instance ignores duplicate fields, prioritising the +-- left argument.+data InstancePattern a = InstancePattern+ { instancePattern :: Maybe a+ , classPattern :: Maybe a+ , modulePattern :: Maybe a+ } deriving (Eq, Show, Ord, Functor, Foldable, Traversable)+++instance Semigroup (InstancePattern a) where+ InstancePattern i c m <> InstancePattern i' c' m' =+ InstancePattern (i <|> i') (c <|> c') (m <|> m')+++pattern InstanceOnly, ClassOnly, ModuleOnly :: a -> InstancePattern a+pattern InstanceOnly t = InstancePattern (Just t) Nothing Nothing+pattern ClassOnly c = InstancePattern Nothing (Just c) Nothing+pattern ModuleOnly m = InstancePattern Nothing Nothing (Just m)+++defaultConfig :: ConfigParsed+defaultConfig = Config+ { rootPatterns = [ "Main.main", "^Paths_.*"]+ , typeClassRoots = False+ , rootInstances = [ ClassOnly "\\.IsString$", ClassOnly "\\.IsList$" ]+ , unusedTypes = False } + instance TOML.DecodeTOML Config where tomlDecoder = do- rootPatterns <- TOML.getField "roots"- typeClassRoots <- TOML.getField "type-class-roots"+ conf <- TOML.tomlDecoder+ either fail pure $ compileConfig conf - return Config{..}++instance TOML.DecodeTOML ConfigParsed where+ tomlDecoder = do+ rootPatterns <- TOML.getFieldOr (rootPatterns defaultConfig) "roots"+ typeClassRoots <- TOML.getFieldOr (typeClassRoots defaultConfig) "type-class-roots"+ rootInstances <- TOML.getFieldOr (rootInstances defaultConfig) "root-instances" + unusedTypes <- TOML.getFieldOr (unusedTypes defaultConfig) "unused-types"++ pure Config{..}+++decodeNoDefaults :: TOML.Decoder Config+decodeNoDefaults = do+ rootPatterns <- TOML.getField "roots"+ typeClassRoots <- TOML.getField "type-class-roots"+ rootInstances <- TOML.getField "root-instances"+ unusedTypes <- TOML.getField "unused-types"++ either fail pure $ compileConfig Config{..}+++instance TOML.DecodeTOML (InstancePattern String) where+ tomlDecoder = decodeInstancePattern+++-- | Decoder for a value of any of the forms:+--+-- @{instance = t, class = c, module = m} -> InstanceClassAndModule t c m@+--+-- @a -> InstanceOnly a@+--+-- @{instance = t} -> InstanceOnly t@+--+-- @{class = m} -> ClassOnly c@+--+-- etc.+decodeInstancePattern :: TOML.Decoder (InstancePattern String)+decodeInstancePattern = decodeTable <|> decodeStringLiteral <|> decodeInstanceError++ where++ decodeStringLiteral = InstanceOnly <$> TOML.tomlDecoder++ decodeTable = do+ t <- fmap InstanceOnly <$> TOML.getFieldOpt "instance"+ c <- fmap ClassOnly <$> TOML.getFieldOpt "class"+ m <- fmap ModuleOnly <$> TOML.getFieldOpt "module"+ maybe empty pure (t <> c <> m)++ decodeInstanceError = TOML.makeDecoder $+ TOML.invalidValue "Need to specify at least one of 'instance', 'class', or 'module'"+++showInstancePattern :: Show a => InstancePattern a -> String+showInstancePattern = \case+ InstanceOnly a -> show a+ p -> "{ " ++ table ++ " }"+ where+ table = intercalate ", " . filter (not . null) $+ [ maybe mempty typeField (instancePattern p)+ , maybe mempty classField (classPattern p)+ , maybe mempty moduleField (modulePattern p)+ ]+ typeField t = "instance = " ++ show t+ classField c = "class = " ++ show c+ moduleField m = "module = " ++ show m+++compileRegex :: String -> Either String Regex+compileRegex = bimap show (\p -> patternToRegex p defaultCompOpt defaultExecOpt) . parseRegex+++compileConfig :: ConfigParsed -> Either String Config+compileConfig conf@Config{ rootInstances, rootPatterns } = do+ rootInstances' <- traverse (traverse compileRegex) . nubOrd $ rootInstances+ rootPatterns' <- traverse compileRegex $ nubOrd rootPatterns+ pure conf{ rootInstances = rootInstances', rootPatterns = rootPatterns' }+++configToToml :: ConfigParsed -> String+configToToml Config{..}+ = unlines . intersperse mempty $+ [ "roots = " ++ show rootPatterns+ , "type-class-roots = " ++ map toLower (show typeClassRoots)+ , "root-instances = " ++ "[" ++ intercalate "," (map showInstancePattern rootInstances') ++ "]"+ , "unused-types = " ++ map toLower (show unusedTypes)+ ]+ where+ rootInstances' = rootInstances
src/Weeder/Main.hs view
@@ -3,27 +3,26 @@ {-# language FlexibleContexts #-} {-# language NamedFieldPuns #-} {-# language OverloadedStrings #-}+{-# language LambdaCase #-}+{-# language RecordWildCards #-} -- | This module provides an entry point to the Weeder executable. -module Weeder.Main ( main, mainWithConfig ) where+module Weeder.Main ( main, mainWithConfig, getHieFiles ) where +-- async+import Control.Concurrent.Async ( async, link, ExceptionInLinkedThread ( ExceptionInLinkedThread ) )+ -- base-import Control.Exception ( throwIO )-import Control.Monad ( guard, when )-import Control.Monad.IO.Class ( liftIO )-import Data.Bool+import Control.Exception ( Exception, throwIO, displayException, catches, Handler ( Handler ), SomeException ( SomeException ) )+import Control.Concurrent ( getChanContents, newChan, writeChan, setNumCapabilities )+import Control.Monad ( unless, when ) import Data.Foldable-import Data.List ( isSuffixOf, sortOn )+import Data.List ( isSuffixOf )+import Data.Maybe ( isJust, catMaybes ) import Data.Version ( showVersion )-import System.Exit ( exitFailure, ExitCode(..), exitWith )---- containers-import qualified Data.Map.Strict as Map-import qualified Data.Set as Set---- text-import qualified Data.Text as T+import System.Exit ( ExitCode(..), exitWith )+import System.IO ( stderr, hPutStrLn ) -- toml-reader import qualified TOML@@ -37,65 +36,168 @@ -- ghc import GHC.Iface.Ext.Binary ( HieFileResult( HieFileResult, hie_file_result ), readHieFileWithVersion ) import GHC.Iface.Ext.Types ( HieFile( hie_hs_file ), hieVersion )-import GHC.Unit.Module ( moduleName, moduleNameString ) import GHC.Types.Name.Cache ( initNameCache, NameCache )-import GHC.Types.Name ( occNameString )-import GHC.Types.SrcLoc ( RealSrcLoc, realSrcSpanStart, srcLocLine ) --- regex-tdfa-import Text.Regex.TDFA ( (=~) )- -- optparse-applicative import Options.Applicative --- transformers-import Control.Monad.Trans.State.Strict ( execStateT )+-- text+import qualified Data.Text.IO as T -- weeder-import Weeder+import Weeder.Run import Weeder.Config import Paths_weeder (version) +-- | Each exception corresponds to an exit code.+data WeederException + = ExitNoHieFilesFailure+ | ExitHieVersionFailure + FilePath -- ^ Path to HIE file+ Integer -- ^ HIE file's header version+ | ExitConfigFailure+ String -- ^ Error message+ | ExitWeedsFound+ deriving Show+++weederExitCode :: WeederException -> ExitCode+weederExitCode = \case+ ExitWeedsFound -> ExitFailure 228+ ExitHieVersionFailure _ _ -> ExitFailure 2+ ExitConfigFailure _ -> ExitFailure 3+ ExitNoHieFilesFailure -> ExitFailure 4+++instance Exception WeederException where+ displayException = \case+ ExitNoHieFilesFailure -> noHieFilesFoundMessage+ ExitHieVersionFailure path v -> hieVersionMismatchMessage path v+ ExitConfigFailure s -> s+ ExitWeedsFound -> mempty+ where++ noHieFilesFoundMessage = + "No HIE files found: check that the directory is correct "+ <> "and that the -fwrite-ide-info compilation flag is set."++ hieVersionMismatchMessage path v = unlines+ [ "incompatible hie file: " <> path+ , " this version of weeder was compiled with GHC version "+ <> show hieVersion+ , " the hie files in this project were generated with GHC version "+ <> show v+ , " weeder must be built with the same GHC version"+ <> " as the project it is used on"+ ]+++-- | Convert 'WeederException' to the corresponding 'ExitCode' and emit an error +-- message to stderr.+--+-- Additionally, unwrap 'ExceptionInLinkedThread' exceptions: this is for+-- 'getHieFiles'.+handleWeederException :: IO a -> IO a+handleWeederException a = catches a handlers + where+ handlers = [ Handler rethrowExits+ , Handler unwrapLinks+ ]+ rethrowExits w = do+ hPutStrLn stderr (displayException w)+ exitWith (weederExitCode w)+ unwrapLinks (ExceptionInLinkedThread _ (SomeException w)) =+ throwIO w+++data CLIArguments = CLIArguments+ { configPath :: FilePath+ , hieExt :: String+ , hieDirectories :: [FilePath]+ , requireHsFiles :: Bool+ , writeDefaultConfig :: Bool+ , noDefaultFields :: Bool+ , capabilities :: Maybe Int+ }+++parseCLIArguments :: Parser CLIArguments+parseCLIArguments = do+ configPath <- strOption+ ( long "config"+ <> help "A file path for Weeder's configuration."+ <> value "./weeder.toml"+ <> metavar "<weeder.toml>"+ )+ hieExt <- strOption+ ( long "hie-extension"+ <> value ".hie"+ <> help "Extension of HIE files"+ <> showDefault+ )+ hieDirectories <- many (+ strOption+ ( long "hie-directory"+ <> help "A directory to look for .hie files in. Maybe specified multiple times. Default ./."+ )+ )+ requireHsFiles <- switch+ ( long "require-hs-files"+ <> help "Skip stale .hie files with no matching .hs modules"+ )+ writeDefaultConfig <- switch+ ( long "write-default-config"+ <> help "Write a default configuration file if the one specified by --config does not exist"+ )+ noDefaultFields <- switch+ ( long "no-default-fields"+ <> help "Do not use default field values for missing fields in the configuration."+ )+ capabilities <- nParser <|> jParser+ pure CLIArguments{..}+ where+ jParser = Just <$> option auto+ ( short 'j'+ <> value 1+ <> help "Number of cores to use."+ <> showDefault)+ nParser = flag' Nothing+ ( short 'N'+ <> help "Use all available cores."+ )++ -- | Parse command line arguments and into a 'Config' and run 'mainWithConfig'.+--+-- Exits with one of the listed Weeder exit codes on failure. main :: IO ()-main = do- (configExpr, hieExt, hieDirectories, requireHsFiles) <-+main = handleWeederException do+ CLIArguments{..} <- execParser $- info (optsP <**> helper <**> versionP) mempty+ info (parseCLIArguments <**> helper <**> versionP) mempty - (exitCode, _) <- - TOML.decodeFile (T.unpack configExpr)- >>= either throwIO pure- >>= mainWithConfig hieExt hieDirectories requireHsFiles- - exitWith exitCode+ traverse_ setNumCapabilities capabilities++ configExists <-+ doesFileExist configPath++ unless (writeDefaultConfig ==> configExists) do+ hPutStrLn stderr $ "Did not find config: wrote default config to " ++ configPath+ writeFile configPath (configToToml defaultConfig)++ decodeConfig noDefaultFields configPath+ >>= either throwConfigError pure+ >>= mainWithConfig hieExt hieDirectories requireHsFiles where- optsP = (,,,)- <$> strOption- ( long "config"- <> help "A file path for Weeder's configuration."- <> value "./weeder.toml"- <> metavar "<weeder.toml>"- <> showDefaultWith T.unpack- )- <*> strOption- ( long "hie-extension"- <> value ".hie"- <> help "Extension of HIE files"- <> showDefault- )- <*> many (- strOption- ( long "hie-directory"- <> help "A directory to look for .hie files in. Maybe specified multiple times. Default ./."- )- )- <*> switch- ( long "require-hs-files"- <> help "Skip stale .hie files with no matching .hs modules"- )+ throwConfigError e =+ throwIO $ ExitConfigFailure (show e) + decodeConfig noDefaultFields =+ if noDefaultFields+ then fmap (TOML.decodeWith decodeNoDefaults) . T.readFile+ else TOML.decodeFile+ versionP = infoOption ( "weeder version " <> showVersion version <> "\nhie version "@@ -107,8 +209,31 @@ -- -- This will recursively find all files with the given extension in the given directories, perform -- analysis, and report all unused definitions according to the 'Config'.-mainWithConfig :: String -> [FilePath] -> Bool -> Config -> IO (ExitCode, Analysis)-mainWithConfig hieExt hieDirectories requireHsFiles Config{ rootPatterns, typeClassRoots } = do+--+-- Exits with one of the listed Weeder exit codes on failure.+mainWithConfig :: String -> [FilePath] -> Bool -> Config -> IO ()+mainWithConfig hieExt hieDirectories requireHsFiles weederConfig = handleWeederException do+ hieFiles <-+ getHieFiles hieExt hieDirectories requireHsFiles++ when (null hieFiles) $ throwIO ExitNoHieFilesFailure++ let+ (weeds, _) =+ runWeeder weederConfig hieFiles++ mapM_ (putStrLn . formatWeed) weeds++ unless (null weeds) $ throwIO ExitWeedsFound+++-- | Find and read all .hie files in the given directories according to the given parameters,+-- exiting if any are incompatible with the current version of GHC.+-- The .hie files are returned as a lazy stream in the form of a list.+--+-- Will rethrow exceptions as 'ExceptionInLinkedThread' to the calling thread.+getHieFiles :: String -> [FilePath] -> Bool -> IO [HieFile]+getHieFiles hieExt hieDirectories requireHsFiles = do hieFilePaths <- concat <$> traverse ( getFilesIn hieExt )@@ -122,60 +247,28 @@ then getFilesIn ".hs" "./." else pure [] + hieFileResultsChan <- newChan+ nameCache <- initNameCache 'z' [] - analysis <-- flip execStateT emptyAnalysis do- for_ hieFilePaths \hieFilePath -> do- hieFileResult <- liftIO ( readCompatibleHieFileOrExit nameCache hieFilePath )- let hsFileExists = any ( hie_hs_file hieFileResult `isSuffixOf` ) hsFilePaths- when (requireHsFiles ==> hsFileExists) do- analyseHieFile hieFileResult-- 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 )- spans <- Map.lookup d ( declarationSites analysis )- guard $ not $ null spans- let starts = map realSrcSpanStart $ Set.toList spans- return [ Map.singleton moduleFilePath ( liftA2 (,) starts (pure d) ) ]- )- dead-- for_ ( Map.toList warnings ) \( path, declarations ) ->- for_ (sortOn (srcLocLine . fst) declarations) \( start, d ) ->- putStrLn $ showWeed path start d+ a <- async $ handleWeederException do+ readHieFiles nameCache hieFilePaths hieFileResultsChan hsFilePaths+ writeChan hieFileResultsChan Nothing+ + link a - let exitCode = if null warnings then ExitSuccess else ExitFailure 1+ catMaybes . takeWhile isJust <$> getChanContents hieFileResultsChan - pure (exitCode, analysis)+ where -showWeed :: FilePath -> RealSrcLoc -> Declaration -> String-showWeed path start d =- path <> ":" <> show ( srcLocLine start ) <> ": "- <> occNameString ( declOccName d)+ readHieFiles nameCache hieFilePaths hieFileResultsChan hsFilePaths =+ for_ hieFilePaths \hieFilePath -> do+ hieFileResult <-+ readCompatibleHieFileOrExit nameCache hieFilePath+ let hsFileExists = any ( hie_hs_file hieFileResult `isSuffixOf` ) hsFilePaths+ when (requireHsFiles ==> hsFileExists) $+ writeChan hieFileResultsChan (Just hieFileResult) -- | Recursively search for files with the given extension in given directory@@ -226,15 +319,8 @@ case res of Right HieFileResult{ hie_file_result } -> return hie_file_result- Left ( v, _ghcVersion ) -> do- putStrLn $ "incompatible hie file: " <> path- putStrLn $ " this version of weeder was compiled with GHC version "- <> show hieVersion- putStrLn $ " the hie files in this project were generated with GHC version "- <> show v- putStrLn $ " weeder must be built with the same GHC version"- <> " as the project it is used on"- exitFailure+ Left ( v, _ghcVersion ) ->+ throwIO $ ExitHieVersionFailure path v infixr 5 ==>
+ src/Weeder/Run.hs view
@@ -0,0 +1,159 @@+{-# language RecordWildCards #-}+{-# language BlockArguments #-}+{-# language LambdaCase #-}+{-# language NamedFieldPuns #-}+{-# LANGUAGE FlexibleContexts #-}++module Weeder.Run ( runWeeder, Weed(..), formatWeed ) where++-- base+import Control.Applicative ( liftA2 )+import Control.Monad ( guard )+import Data.List ( sortOn )+import Data.Foldable ( fold, foldl' )+import Data.Function ( (&) )++-- containers+import Data.Set ( Set )+import qualified Data.Set as Set+import qualified Data.Map.Strict as Map++-- ghc+import GHC.Plugins + ( occNameString+ , moduleName+ , moduleNameString + )+import GHC.Iface.Ext.Types ( HieFile( hie_asts ), getAsts )+import GHC.Iface.Ext.Utils (generateReferencesMap)++-- parallel+import Control.Parallel (pseq)+import Control.Parallel.Strategies (parMap, rdeepseq)++-- regex-tdfa+import Text.Regex.TDFA ( matchTest )++-- transformers+import Control.Monad.State.Strict ( execState )++-- weeder+import Weeder+import Weeder.Config+++data Weed = Weed+ { weedPath :: FilePath+ , weedLoc :: Int+ , weedDeclaration :: Declaration+ , weedPrettyPrintedType :: Maybe String+ }+++formatWeed :: Weed -> String+formatWeed Weed{..} =+ weedPath <> ":" <> show weedLoc <> ": "+ <> case weedPrettyPrintedType of+ Nothing -> occNameString ( declOccName weedDeclaration )+ Just t -> "(Instance) :: " <> t++-- | Run Weeder on the given .hie files with the given 'Config'.+--+-- Returns a list of 'Weed's that can be displayed using+-- 'formatWeed', and the final 'Analysis'.+runWeeder :: Config -> [HieFile] -> ([Weed], Analysis)+runWeeder weederConfig@Config{ rootPatterns, typeClassRoots, rootInstances } hieFiles =+ let + asts = concatMap (Map.elems . getAsts . hie_asts) hieFiles++ rf = generateReferencesMap asts++ analyses =+ parMap rdeepseq (\hf -> execState (analyseHieFile weederConfig hf) emptyAnalysis) hieFiles++ analyseEvidenceUses' = + if typeClassRoots+ then id+ else analyseEvidenceUses rf++ analysis1 = + foldl' mappend mempty analyses++ -- Evaluating 'analysis1' first allows us to begin analysis + -- while hieFiles is still being read (since rf depends on all hie files)+ analysis = analysis1 `pseq`+ analyseEvidenceUses' analysis1++ -- We limit ourselves to outputable declarations only rather than all+ -- declarations in the graph. This has a slight performance benefit,+ -- at the cost of having to assume that a non-outputable declaration+ -- will always either be an implicit root or irrelevant.+ roots =+ Set.filter+ ( \d ->+ any+ (`matchTest` displayDeclaration d)+ rootPatterns+ )+ ( outputableDeclarations analysis )++ reachableSet =+ reachable+ analysis+ ( Set.map DeclarationRoot roots <> filterImplicitRoots analysis ( implicitRoots analysis ) )++ -- We only care about dead declarations if they have a span assigned,+ -- since they don't show up in the output otherwise+ dead =+ outputableDeclarations analysis Set.\\ reachableSet++ warnings =+ Map.unionsWith (++) $+ foldMap+ ( \d ->+ fold $ do+ moduleFilePath <- Map.lookup ( declModule d ) ( modulePaths analysis )+ starts <- Map.lookup d ( declarationSites analysis )+ guard $ not $ null starts+ return [ Map.singleton moduleFilePath ( liftA2 (,) (Set.toList starts) (pure d) ) ]+ )+ dead++ weeds =+ Map.toList warnings & concatMap \( weedPath, declarations ) ->+ sortOn fst declarations & map \( weedLoc, weedDeclaration ) ->+ Weed { weedPrettyPrintedType = Map.lookup weedDeclaration (prettyPrintedType analysis)+ , weedPath+ , weedLoc+ , weedDeclaration+ }++ in (weeds, analysis)++ where++ filterImplicitRoots :: Analysis -> Set Root -> Set Root+ filterImplicitRoots Analysis{ prettyPrintedType, modulePaths } = 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++ 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)+++displayDeclaration :: Declaration -> String+displayDeclaration d = + moduleNameString ( moduleName ( declModule d ) ) <> "." <> occNameString ( declOccName d )
test/Spec.hs view
@@ -1,6 +1,8 @@ import qualified Weeder.Main+import qualified Weeder.Run import qualified Weeder import qualified TOML+import qualified UnitTests import Algebra.Graph.Export.Dot import GHC.Types.Name.Occurrence (occNameString)@@ -8,57 +10,79 @@ import System.Environment (getArgs, withArgs) import System.FilePath import System.Process-import System.IO.Silently (hCapture_)-import System.IO (stdout, stderr, hPrint)+import System.IO (stderr, hPrint) import Test.Hspec import Control.Monad (zipWithM_, when) import Control.Exception ( throwIO, IOException, handle )+import Data.Maybe (isJust)+import Data.List (find, sortOn) main :: IO () main = do args <- getArgs- stdoutFiles <- discoverIntegrationTests- let hieDirectories = map dropExtension stdoutFiles+ testOutputFiles <- fmap sortTests discoverIntegrationTests+ let hieDirectories = map (dropExtension . snd) testOutputFiles drawDots = mapM_ (drawDot . (<.> ".dot")) hieDirectories graphviz = "--graphviz" `elem` args withArgs (filter (/="--graphviz") args) $ hspec $ afterAll_ (when graphviz drawDots) $ do- describe "Weeder.Main" $- describe "mainWithConfig" $- zipWithM_ integrationTestSpec stdoutFiles hieDirectories+ describe "Weeder.Run" $+ describe "runWeeder" $+ zipWithM_ (uncurry integrationTestSpec) testOutputFiles hieDirectories+ UnitTests.spec where -- Draw a dotfile via graphviz drawDot f = callCommand $ "dot -Tpng " ++ f ++ " -o " ++ (f -<.> ".png")+ -- Sort the output files such that the failing ones go last+ sortTests = sortOn (isJust . fst) --- | Run weeder on hieDirectory, comparing the output to stdoutFile--- The directory containing hieDirectory must also have a .toml file--- with the same name as hieDirectory-integrationTestSpec :: FilePath -> FilePath -> Spec-integrationTestSpec stdoutFile hieDirectory = do- it ("produces the expected output for " ++ hieDirectory) $ do+-- | Run weeder on @hieDirectory@, comparing the output to @stdoutFile@.+--+-- The directory containing @hieDirectory@ must also have a @.toml@ file+-- with the same name as @hieDirectory@.+--+-- If @failingFile@ is @Just@, it is used as the expected output instead of+-- @stdoutFile@, and a different failure message is printed if the output+-- matches @stdoutFile@.+integrationTestSpec :: Maybe FilePath -> FilePath -> FilePath -> Spec+integrationTestSpec failingFile stdoutFile hieDirectory = do+ it (integrationTestText ++ hieDirectory) $ do expectedOutput <- readFile stdoutFile actualOutput <- integrationTestOutput hieDirectory- actualOutput `shouldBe` expectedOutput+ case failingFile of+ Just f -> do+ failingOutput <- readFile f+ actualOutput `shouldNotBe` expectedOutput+ actualOutput `shouldBe` failingOutput+ Nothing ->+ actualOutput `shouldBe` expectedOutput+ where+ integrationTestText = case failingFile of+ Nothing -> "produces the expected output for "+ Just _ -> "produces the expected (wrong) output for " --- | Returns detected .stdout files in ./test/Spec-discoverIntegrationTests :: IO [FilePath]+-- | Returns detected .failing and .stdout files in ./test/Spec+discoverIntegrationTests :: IO [(Maybe FilePath, FilePath)] discoverIntegrationTests = do- contents <- listDirectory "./test/Spec"- pure . map ("./test/Spec" </>) $ filter (".stdout" `isExtensionOf`) contents+ contents <- listDirectory testPath+ let stdoutFiles = map (testPath </>) $+ filter (".stdout" `isExtensionOf`) contents+ pure . map (\s -> (findFailing s contents, s)) $ stdoutFiles+ where+ findFailing s = fmap (testPath </>) . find (takeBaseName s <.> ".failing" ==)+ testPath = "./test/Spec" -- | Run weeder on the given directory for .hie files, returning stdout -- Also creates a dotfile containing the dependency graph as seen by Weeder integrationTestOutput :: FilePath -> IO String-integrationTestOutput hieDirectory = hCapture_ [stdout] $ do- isEmpty <- not . any (".hie" `isExtensionOf`) <$> listDirectory hieDirectory- when isEmpty $ fail "No .hie files found in directory, this is probably unintended"- (_, analysis) <-- TOML.decodeFile configExpr- >>= either throwIO pure- >>= Weeder.Main.mainWithConfig ".hie" [hieDirectory] True- let graph = Weeder.dependencyGraph analysis+integrationTestOutput hieDirectory = do+ hieFiles <- Weeder.Main.getHieFiles ".hie" [hieDirectory] True+ weederConfig <- TOML.decodeFile configExpr >>= either throwIO pure+ let (weeds, analysis) = Weeder.Run.runWeeder weederConfig hieFiles+ graph = Weeder.dependencyGraph analysis graph' = export (defaultStyle (occNameString . Weeder.declOccName)) graph handle (\e -> hPrint stderr (e :: IOException)) $ writeFile (hieDirectory <.> ".dot") graph'+ pure (unlines $ map Weeder.Run.formatWeed weeds) where configExpr = hieDirectory <.> ".toml"
+ test/Spec/ApplicativeDo.failing view
@@ -0,0 +1,2 @@+test/Spec/ApplicativeDo/ApplicativeDo.hs:6: (Instance) :: Functor Foo+test/Spec/ApplicativeDo/ApplicativeDo.hs:9: (Instance) :: Applicative Foo
+ test/Spec/ApplicativeDo.stdout view
+ test/Spec/ApplicativeDo.toml view
@@ -0,0 +1,3 @@+roots = [ "Spec.ApplicativeDo.ApplicativeDo.root" ]++type-class-roots = false
+ test/Spec/ApplicativeDo/ApplicativeDo.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE ApplicativeDo #-}+module Spec.ApplicativeDo.ApplicativeDo where++newtype Foo a = Foo a++instance Functor Foo where+ fmap f (Foo a) = Foo (f a)++instance Applicative Foo where+ pure = Foo+ Foo f <*> Foo a = Foo (f a)++root :: Foo Int+root = do+ a <- Foo 1+ b <- Foo 2+ pure (a + b)
+ test/Spec/ConfigInstanceModules.stdout view
@@ -0,0 +1,2 @@+test/Spec/ConfigInstanceModules/Module1.hs:3: (Instance) :: Show T+test/Spec/ConfigInstanceModules/Module2.hs:3: (Instance) :: Bounded T
+ test/Spec/ConfigInstanceModules.toml view
@@ -0,0 +1,6 @@+root-instances = [ { module = "Spec.ConfigInstanceModules.Module1", instance = "Bounded T" }+ , "Read T" + , { module = "Spec.ConfigInstanceModules.Module3" }+ , { class = '\.Enum$' }+ , { module = "Spec.ConfigInstanceModules.Module2", class = '\.Show$' } + ]
+ test/Spec/ConfigInstanceModules/Module1.hs view
@@ -0,0 +1,3 @@+module Spec.ConfigInstanceModules.Module1 where++data T = MkT deriving (Show, Bounded, Enum, Read)
+ test/Spec/ConfigInstanceModules/Module2.hs view
@@ -0,0 +1,3 @@+module Spec.ConfigInstanceModules.Module2 where++data T = MkT deriving (Show, Bounded, Enum, Read)
+ test/Spec/ConfigInstanceModules/Module3.hs view
@@ -0,0 +1,3 @@+module Spec.ConfigInstanceModules.Module3 where++data A = MkA deriving Bounded
+ test/Spec/DeriveGeneric.stdout view
@@ -0,0 +1,1 @@+test/Spec/DeriveGeneric/DeriveGeneric.hs:12: (Instance) :: FromJSON T
+ test/Spec/DeriveGeneric.toml view
@@ -0,0 +1,3 @@+roots = [ "Spec.DeriveGeneric.DeriveGeneric.t" ]++type-class-roots = false
+ test/Spec/DeriveGeneric/DeriveGeneric.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+module Spec.DeriveGeneric.DeriveGeneric where++import GHC.Generics+import Data.Aeson++newtype T = MkT Bool+ -- Generic and ToJSON must not be detected as unused+ -- but FromJSON should be detected as unused+ deriving ( Generic, ToJSON+ , FromJSON )++t :: Value+t = toJSON $ MkT True
+ test/Spec/InstanceRootConstraint.stdout view
+ test/Spec/InstanceRootConstraint.toml view
@@ -0,0 +1,5 @@+roots = []++type-class-roots = false++root-instances = [ 'Foo a => Foo \[a\]' ]
+ test/Spec/InstanceRootConstraint/InstanceRootConstraint.hs view
@@ -0,0 +1,13 @@+module Spec.InstanceRootConstraint.InstanceRootConstraint where++class Foo a where+ foo :: a -> Char++instance Foo Char where+ foo = id++instance Foo a => Foo [a] where+ foo = const a++a :: Char+a = foo 'a'
+ test/Spec/InstanceTypeclass.stdout view
@@ -0,0 +1,2 @@+test/Spec/InstanceTypeclass/InstanceTypeclass.hs:4: Foo+test/Spec/InstanceTypeclass/InstanceTypeclass.hs:10: (Instance) :: Foo Char
+ test/Spec/InstanceTypeclass.toml view
@@ -0,0 +1,5 @@+roots = []++type-class-roots = false++root-instances = [ "RootClass Char" ]
+ test/Spec/InstanceTypeclass/InstanceTypeclass.hs view
@@ -0,0 +1,20 @@+-- | Test for correct output of unreachable classes and instances+module Spec.InstanceTypeclass.InstanceTypeclass where++class Foo a where+ foo :: a -> Char++-- this instance is not marked as root,+-- therefore class Foo will show up in the output+-- as well+instance Foo Char where+ foo = id++class RootClass a where+ rootClass :: a -> Char++-- this instance is explicitly marked as root,+-- hence RootClass will not show up in the output+-- (note the way it is written in InstanceTypeclass.toml)+instance RootClass Char where+ rootClass = id
+ test/Spec/Monads.failing view
@@ -0,0 +1,3 @@+test/Spec/Monads/Monads.hs:20: (Instance) :: Functor Identity'+test/Spec/Monads/Monads.hs:23: (Instance) :: Applicative Identity'+test/Spec/Monads/Monads.hs:27: (Instance) :: Monad Identity'
+ test/Spec/Monads.stdout view
+ test/Spec/Monads.toml view
@@ -0,0 +1,3 @@+roots = [ "Spec.Monads.Monads.foo", "Spec.Monads.Monads.bar" ]++type-class-roots = false
+ test/Spec/Monads/Monads.hs view
@@ -0,0 +1,38 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+{-# OPTIONS_GHC -Wno-type-defaults #-}+module Spec.Monads.Monads where++newtype Identity a = Identity { runIdentity :: a }++instance Functor Identity where+ fmap f (Identity x) = Identity (f x)++instance Applicative Identity where+ pure = Identity+ Identity f <*> Identity x = Identity (f x)++instance Monad Identity where+ return = pure+ Identity x >>= f = f x++newtype Identity' a = Identity' { runIdentity' :: a}++instance Functor Identity' where+ fmap f (Identity' x) = Identity' (f x)++instance Applicative Identity' where+ pure = Identity'+ Identity' f <*> Identity' x = Identity' (f x)++instance Monad Identity' where+ return = pure+ Identity' x >>= f = f x++foo = do+ _x <- Identity 3+ Identity 4++bar :: Identity' Integer -- oh no (the type signature breaks the evidence variables)+bar = do+ _x <- Identity' 3+ Identity' 4
+ test/Spec/NumInstance.stdout view
+ test/Spec/NumInstance.toml view
@@ -0,0 +1,3 @@+roots = [ "Spec.NumInstance.NumInstance.two" ]++type-class-roots = false
+ test/Spec/NumInstance/NumInstance.hs view
@@ -0,0 +1,17 @@+{-# OPTIONS_GHC -Wno-missing-methods #-}+module Spec.NumInstance.NumInstance where++data Modulo2 = Zero | One++instance Num Modulo2 where+ (+) = add+ -- leave the rest undefined++-- add should not be detected as unused+add :: Modulo2 -> Modulo2 -> Modulo2+add One One = Zero+add Zero n = n+add n Zero = n++two :: Modulo2+two = One + One
+ test/Spec/NumInstanceLiteral.failing view
@@ -0,0 +1,1 @@+test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs:7: (Instance) :: Num Modulo1
+ test/Spec/NumInstanceLiteral.stdout view
+ test/Spec/NumInstanceLiteral.toml view
@@ -0,0 +1,5 @@+roots = [ "Spec.NumInstanceLiteral.NumInstanceLiteral.zero"+ , "Spec.NumInstanceLiteral.NumInstanceLiteral.Modulo1"+ ]++type-class-roots = false
+ test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs view
@@ -0,0 +1,12 @@+{-# OPTIONS_GHC -Wno-missing-methods #-}+module Spec.NumInstanceLiteral.NumInstanceLiteral where++data Modulo1 = Zero++-- $fNumModulo1 should not be detected as unused+instance Num Modulo1 where+ fromInteger _ = Zero+ -- leave the rest undefined++zero :: Modulo1+zero = 0 -- no evidence usage here at all in the HieAST (9.4.4 and 9.6.1)
+ test/Spec/OverloadedLabels.stdout view
@@ -0,0 +1,1 @@+test/Spec/OverloadedLabels/OverloadedLabels.hs:17: (Instance) :: Has Point "y" Int
+ test/Spec/OverloadedLabels.toml view
@@ -0,0 +1,3 @@+roots = [ "Spec.OverloadedLabels.OverloadedLabels.root" ]++type-class-roots = false
+ test/Spec/OverloadedLabels/OverloadedLabels.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE DataKinds, KindSignatures, + FunctionalDependencies, FlexibleInstances,+ OverloadedLabels, ScopedTypeVariables #-}+{-# OPTIONS_GHC -Wno-orphans #-}+module Spec.OverloadedLabels.OverloadedLabels where+import GHC.OverloadedLabels (IsLabel(..))+import GHC.TypeLits (Symbol)++data Label (l :: Symbol) = Get++class Has a l b | a l -> b where+ from :: a -> Label l -> b++data Point = Point Int Int -- odd behaviour with dependencies between Point and Int++instance Has Point "x" Int where from (Point x _) _ = x+instance Has Point "y" Int where from (Point _ y) _ = y++instance Has a l b => IsLabel l (a -> b) where+ fromLabel x = from x (Get :: Label l)++root :: Int+root = #x (Point 1 2) + -- surprisingly OverloadedLabels works perfectly out of the box
+ test/Spec/OverloadedLists.failing view
@@ -0,0 +1,1 @@+test/Spec/OverloadedLists/OverloadedLists.hs:9: (Instance) :: IsList (BetterList x)
+ test/Spec/OverloadedLists.stdout view
+ test/Spec/OverloadedLists.toml view
@@ -0,0 +1,6 @@+roots = [ "Spec.OverloadedLists.OverloadedLists.root"+ , "Spec.OverloadedLists.OverloadedLists.BetterList" ]++root-instances = []++type-class-roots = false
+ test/Spec/OverloadedLists/OverloadedLists.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE TypeFamilies #-}+module Spec.OverloadedLists.OverloadedLists where++import GHC.IsList ( IsList(..) )++data BetterList x = Nil | Cons x (BetterList x)++instance IsList (BetterList x) where+ type Item (BetterList x) = x+ fromList = foldr Cons Nil+ toList Nil = []+ toList (Cons x xs) = x : toList xs++root :: BetterList Int+root = [1, 2, 3]
+ test/Spec/OverloadedStrings.failing view
@@ -0,0 +1,1 @@+test/Spec/OverloadedStrings/OverloadedStrings.hs:10: (Instance) :: IsString BetterString
+ test/Spec/OverloadedStrings.stdout view
+ test/Spec/OverloadedStrings.toml view
@@ -0,0 +1,8 @@+roots = [ "Spec.OverloadedStrings.OverloadedStrings.root"+ , "Spec.OverloadedStrings.OverloadedStrings.root'" + , "Spec.OverloadedStrings.OverloadedStrings.BetterString"+ ]++root-instances = []++type-class-roots = false
+ test/Spec/OverloadedStrings/OverloadedStrings.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-missing-signatures #-}+module Spec.OverloadedStrings.OverloadedStrings where++import Data.String ( IsString(fromString) )++newtype BetterString = BetterString String++-- $fIsStringBetterString should not be detected as unused+instance IsString BetterString where+ fromString = BetterString++newtype BetterString' = BetterString' String++instance IsString BetterString' where+ fromString = BetterString'++-- Thought: this problem might be similar to RebindableSyntax, QualifiedDo, etc+root :: BetterString+root = "Hello World" -- no evidence variable usage here++root' = "Hello World" :: BetterString' -- evidence usage present
+ test/Spec/RangeEnum.failing view
@@ -0,0 +1,1 @@+test/Spec/RangeEnum/RangeEnum.hs:14: (Instance) :: Enum Colour
+ test/Spec/RangeEnum.stdout view
+ test/Spec/RangeEnum.toml view
@@ -0,0 +1,7 @@+roots = [ "Spec.RangeEnum.RangeEnum.planets"+ , "Spec.RangeEnum.RangeEnum.letters" + , "Spec.RangeEnum.RangeEnum.shapes"+ , "Spec.RangeEnum.RangeEnum.colours"+ ]++type-class-roots = false
+ test/Spec/RangeEnum/RangeEnum.hs view
@@ -0,0 +1,26 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+module Spec.RangeEnum.RangeEnum where++data Planet = Mercury | Venus | Earth+ deriving (Enum, Bounded)++data Letter = A | B | C+ deriving (Enum, Bounded, Show)++data Shape = Circle | Square | Triangle+ deriving (Enum, Bounded)++data Colour = Red | Green | Blue+ deriving (Enum, Bounded)++planets = [minBound .. (maxBound :: Planet)]++letters = map f [minBound .. maxBound]+ where+ f :: Letter -> String+ f = show++shapes = [minBound .. maxBound] :: [Shape]++colours :: [Colour]+colours = [minBound .. maxBound] :: [Colour] -- breaks
+ test/Spec/RootClasses.stdout view
@@ -0,0 +1,1 @@+test/Spec/RootClasses/RootClasses.hs:5: (Instance) :: Enum T
+ test/Spec/RootClasses.toml view
@@ -0,0 +1,5 @@+roots = []++type-class-roots = false++root-instances = [ {class = '\.Show$'}, {class = '\.Ord$'}, {class = '\.Bar$'} ]
+ test/Spec/RootClasses/RootClasses.hs view
@@ -0,0 +1,15 @@+-- | Test for marking classes as roots+{-# LANGUAGE StandaloneDeriving #-}+module Spec.RootClasses.RootClasses where++data T = MkT deriving (Eq, Show, Enum)++deriving instance Ord T++data V = MkV++class Bar a where+ bar :: a -> Char++instance Bar V where+ bar = const 'b'
+ test/Spec/StandaloneDeriving.stdout view
@@ -0,0 +1,1 @@+test/Spec/StandaloneDeriving/StandaloneDeriving.hs:6: (Instance) :: Show A
+ test/Spec/StandaloneDeriving.toml view
@@ -0,0 +1,5 @@+roots = [ "Spec.StandaloneDeriving.StandaloneDeriving.root"+ , "Spec.StandaloneDeriving.StandaloneDeriving.A"+ ]++type-class-roots = false
+ test/Spec/StandaloneDeriving/StandaloneDeriving.hs view
@@ -0,0 +1,13 @@+{-# LANGUAGE StandaloneDeriving #-}+module Spec.StandaloneDeriving.StandaloneDeriving where++data A = A++deriving instance Show A++data T = T++deriving instance Show T++root :: String+root = show T
+ test/Spec/TypeAliasGADT.stdout view
+ test/Spec/TypeAliasGADT.toml view
@@ -0,0 +1,7 @@+roots = [ "Spec.TypeAliasGADT.TypeAliasGADT.root" ]++type-class-roots = false++unused-types = true++root-instances = []
+ test/Spec/TypeAliasGADT/TypeAliasGADT.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE GADTs #-}++module Spec.TypeAliasGADT.TypeAliasGADT where++-- This is a false positive when the GADT language extension +-- is enabled and we do not consider type signatures+type Secret = String++data A = MkA String Int++root :: Secret -> Int -> Secret+root secret a =+ let _params = MkA mempty a+ in secret
+ test/Spec/TypeDataDecl.stdout view
+ test/Spec/TypeDataDecl.toml view
@@ -0,0 +1,7 @@+roots = [ "Spec.TypeDataDecl.TypeDataDecl.Root" ]++type-class-roots = false++unused-types = true++root-instances = []
+ test/Spec/TypeDataDecl/TypeDataDecl.hs view
@@ -0,0 +1,12 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE KindSignatures #-}++module Spec.TypeDataDecl.TypeDataDecl where++data Kind = MkKind++type Number = Int++data Root (l :: Kind) = MkRecord+ { recordField :: Number+ }
+ test/Spec/TypeFamilies.failing view
+ test/Spec/TypeFamilies.stdout view
@@ -0,0 +1,1 @@+test/Spec/TypeFamilies/TypeFamilies.hs:9: (Type Family Instance) :: Family Bool
+ test/Spec/TypeFamilies.toml view
@@ -0,0 +1,5 @@+roots = [ "Spec.TypeFamilies.TypeFamilyUsage.root" ]++type-class-roots = false++unused-types = true
+ test/Spec/TypeFamilies/TypeFamilies.hs view
@@ -0,0 +1,9 @@+{-# LANGUAGE TypeFamilies #-}+-- | NOTE: the .stdout file for this test is a placeholder+module Spec.TypeFamilies.TypeFamilies where++type family Family a++type instance Family Int = Bool++type instance Family Bool = Int
+ test/Spec/TypeFamilies/TypeFamilyUsage.hs view
@@ -0,0 +1,6 @@+module Spec.TypeFamilies.TypeFamilyUsage where++import Spec.TypeFamilies.TypeFamilies++root :: Family Int+root = True
+ test/Spec/Types.stdout view
@@ -0,0 +1,1 @@+test/Spec/Types/Usages.hs:16: notRoot
+ test/Spec/Types.toml view
@@ -0,0 +1,9 @@+roots = [ "Spec.Types.Usages.numberUsage"+ , "Spec.Types.Usages.modulo1Usage"+ , "Spec.Types.Usages.recordUsage"+ , "Spec.Types.Usages.vectorUsage"+ ]++type-class-roots = false++unused-types = true
+ test/Spec/Types/Types.hs view
@@ -0,0 +1,19 @@+{-# OPTIONS_GHC -Wno-missing-methods #-}+{-# LANGUAGE DeriveFunctor #-}++module Spec.Types.Types where++-- this Bounded will not depend on Modulo1 as intended+data Modulo1 = Zero deriving Bounded ++-- should be reachable via Number+data Modulo2 = Zero' | One'++type Number = Modulo2++newtype Vector a = MkVector (a, a, a) deriving (Functor)++data Record = MkRecord+ { recordField1 :: Int+ , recordField2 :: Double+ }
+ test/Spec/Types/Usages.hs view
@@ -0,0 +1,23 @@+{-# OPTIONS_GHC -Wno-missing-signatures #-}+{-# OPTIONS_GHC -Wno-unused-foralls #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Spec.Types.Usages where++import Spec.Types.Types++numberUsage :: forall a. Number -> Number+numberUsage = id++-- should depend on Modulo1+modulo1Usage = minBound++-- exists to force type inference of modulo1Root to Modulo1+notRoot :: Modulo1+notRoot = modulo1Usage++recordUsage = recordField1++vectorUsage = mapVector (const (0 :: Integer))+ where+ mapVector :: (b -> c) -> Vector b -> Vector c+ mapVector = fmap
+ test/Spec/TypesUnused.stdout view
@@ -0,0 +1,4 @@+test/Spec/TypesUnused/TypesUnused.hs:6: Modulo1+test/Spec/TypesUnused/TypesUnused.hs:8: Number+test/Spec/TypesUnused/TypesUnused.hs:10: Vector+test/Spec/TypesUnused/TypesUnused.hs:12: Family
+ test/Spec/TypesUnused.toml view
@@ -0,0 +1,5 @@+roots = []++type-class-roots = false++unused-types = true
+ test/Spec/TypesUnused/TypesUnused.hs view
@@ -0,0 +1,12 @@+{-# LANGUAGE TypeFamilies #-}+-- | Control version of Types that is not imported+-- All these declarations should show up in the output+module Spec.TypesUnused.TypesUnused where++data Modulo1 = Zero++type Number = Double++newtype Vector = MkVector (Double, Double, Double)++type family Family a
+ test/UnitTests.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=UnitTests #-}
+ test/UnitTests/Weeder/ConfigSpec.hs view
@@ -0,0 +1,25 @@+module UnitTests.Weeder.ConfigSpec (spec) where++import Weeder.Config+import qualified TOML+import qualified Data.Text as T+import Test.Hspec (Spec, describe, it)++spec :: Spec+spec = + describe "Weeder.Config" $+ describe "configToToml" $+ it "passes prop_configToToml" prop_configToToml++-- >>> prop_configToToml+-- True+prop_configToToml :: Bool+prop_configToToml =+ let cf = Config+ { rootPatterns = mempty+ , typeClassRoots = True+ , rootInstances = [InstanceOnly "Quux\\\\[\\]", ClassOnly "[\\[\\\\[baz" <> ModuleOnly "[Quuux]", InstanceOnly "[\\[\\\\[baz" <> ClassOnly "[Quuux]" <> ModuleOnly "[Quuuux]"]+ , unusedTypes = True+ }+ cf' = T.pack $ configToToml cf+ in TOML.decode cf' == 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.6.0+version: 2.7.0 copyright: Neil Mitchell 2017-2020, Oliver Charles 2020-2023 synopsis: Detect dead code description: Find declarations.@@ -18,10 +18,12 @@ extra-source-files: test/Spec/*.toml test/Spec/*.stdout+ test/Spec/*.failing library build-depends: , algebraic-graphs ^>= 0.7+ , async ^>= 2.2.0 , base ^>= 4.17.0.0 || ^>= 4.18.0.0 , bytestring ^>= 0.10.9.0 || ^>= 0.11.0.0 , containers ^>= 0.6.2.1@@ -32,6 +34,7 @@ , lens ^>= 5.1 || ^>= 5.2 , mtl ^>= 2.2.2 || ^>= 2.3 , optparse-applicative ^>= 0.14.3.0 || ^>= 0.15.1.0 || ^>= 0.16.0.0 || ^>= 0.17+ , parallel ^>= 3.2.0.0 , regex-tdfa ^>= 1.2.0.0 || ^>= 1.3.1.0 , text ^>= 2.0.1 , toml-reader ^>= 0.2.0.0@@ -40,12 +43,13 @@ exposed-modules: Weeder Weeder.Config+ Weeder.Run Weeder.Main autogen-modules: Paths_weeder other-modules: Paths_weeder- ghc-options: -Wall -fwarn-incomplete-uni-patterns+ ghc-options: -Wall -fwarn-incomplete-uni-patterns -threaded default-language: Haskell2010 @@ -62,7 +66,7 @@ , weeder main-is: Main.hs hs-source-dirs: exe-weeder- ghc-options: -Wall -fwarn-incomplete-uni-patterns+ ghc-options: -Wall -fwarn-incomplete-uni-patterns -threaded -no-rtsopts-suggestions -with-rtsopts=-N default-language: Haskell2010 test-suite weeder-test@@ -70,12 +74,12 @@ , aeson , algebraic-graphs , base+ , containers , directory , filepath , ghc , hspec , process- , silently , text , toml-reader , weeder@@ -86,7 +90,32 @@ Paths_weeder other-modules: Paths_weeder+ UnitTests -- Tests+ Spec.ApplicativeDo.ApplicativeDo Spec.BasicExample.BasicExample+ Spec.ConfigInstanceModules.Module1+ Spec.ConfigInstanceModules.Module2+ Spec.ConfigInstanceModules.Module3+ Spec.DeriveGeneric.DeriveGeneric+ Spec.InstanceRootConstraint.InstanceRootConstraint+ Spec.InstanceTypeclass.InstanceTypeclass+ Spec.Monads.Monads+ Spec.NumInstance.NumInstance+ Spec.NumInstanceLiteral.NumInstanceLiteral+ Spec.OverloadedLabels.OverloadedLabels+ Spec.OverloadedLists.OverloadedLists+ Spec.OverloadedStrings.OverloadedStrings+ Spec.RangeEnum.RangeEnum+ Spec.RootClasses.RootClasses+ Spec.StandaloneDeriving.StandaloneDeriving+ Spec.TypeAliasGADT.TypeAliasGADT+ Spec.TypeDataDecl.TypeDataDecl+ Spec.Types.Types+ Spec.Types.Usages+ Spec.TypeFamilies.TypeFamilies+ Spec.TypeFamilies.TypeFamilyUsage+ Spec.TypesUnused.TypesUnused+ UnitTests.Weeder.ConfigSpec ghc-options: -Wall -fwarn-incomplete-uni-patterns -fwrite-ide-info -hiedir ./test default-language: Haskell2010