Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for type class instances #126

Merged
merged 24 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0629417
Remove double addition of declarations
ryndubei Apr 19, 2023
9de03e1
Implement test suite
ryndubei May 29, 2023
ebb672e
Add mainWithConfig'
ryndubei Jun 1, 2023
12ed3e3
Export dotfiles of dependency graphs
ryndubei Jun 1, 2023
b4480b2
Draw dotfiles to PNG via graphviz
ryndubei Jun 1, 2023
3c3e49d
Flag for drawing graph PNGs
ryndubei Jun 1, 2023
96bf746
Type class instances have spans and dependencies
ryndubei Jun 2, 2023
2bb2079
Follow type class evidence uses back to bindings
ryndubei Jun 5, 2023
544bc7c
Add failing tests (number and string literals)
ryndubei Jun 7, 2023
34b2879
Failing test for OverloadedLists
ryndubei Jun 9, 2023
c7dafd1
More tests, not all failing
ryndubei Jun 15, 2023
cf65bd1
Merge branch 'master' into type-class-instances-pr
ryndubei Jun 26, 2023
87eece2
Mark tests as failing
ryndubei Jun 26, 2023
3273a6a
Tests for OverloadedLabels and ApplicativeDo
ryndubei Jun 23, 2023
e66e40f
Add InstanceRoot constructor
ryndubei Jun 27, 2023
2efa0eb
Store pretty-printed type in InstanceRoot
ryndubei Jun 29, 2023
424e95b
Add root-instances and root-classes fields
ryndubei Jun 29, 2023
481e150
Clean up tests
ryndubei Jun 30, 2023
70bdf3e
Show pretty-printed type of instances in output
ryndubei Jul 3, 2023
817c5f0
Omit instance OccNames in output
ryndubei Jul 4, 2023
23f21b1
MonadReader for pretty-printed instance types
ryndubei Jul 6, 2023
0c37d6f
MonadReader for following evidence uses
ryndubei Jul 6, 2023
3f6241a
Update test/Spec/InstanceRootConstraint.toml
ryndubei Jul 8, 2023
ef8e8b2
Merge branch 'master' into type-class-instances-pr-2
ryndubei Jul 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 225 additions & 32 deletions src/Weeder.hs

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/Weeder/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,21 @@ data Config = Config
-- ^ 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.
, rootClasses :: Set String
-- ^ All instances of type classes matching these regular expressions will
-- be added to the root set. Note that this does not mark the class itself
-- as a root, so if the class has no instances then it will not be made
-- reachable.
, rootInstances :: Set String
-- ^ All instances with types matching these regular expressions will
-- be added to the root set.
}

instance TOML.DecodeTOML Config where
tomlDecoder = do
rootPatterns <- TOML.getField "roots"
typeClassRoots <- TOML.getField "type-class-roots"
rootClasses <- TOML.getFieldOr mempty "root-classes"
rootInstances <- TOML.getFieldOr mempty "root-instances"

return Config{..}
47 changes: 34 additions & 13 deletions src/Weeder/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
{-# language FlexibleContexts #-}
{-# language NamedFieldPuns #-}
{-# language OverloadedStrings #-}
{-# language LambdaCase #-}

-- | This module provides an entry point to the Weeder executable.

module Weeder.Main ( main, mainWithConfig ) where

-- base
import Control.Exception ( throwIO )
import Control.Monad ( guard, when )
import Control.Monad.IO.Class ( liftIO )
import Data.Bool
import Control.Monad ( guard )
import Data.Foldable
import Data.List ( isSuffixOf, sortOn )
import Data.Version ( showVersion )
Expand Down Expand Up @@ -108,7 +107,7 @@ main = do
-- 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
mainWithConfig hieExt hieDirectories requireHsFiles weederConfig@Config{ rootPatterns, typeClassRoots, rootInstances, rootClasses } = do
hieFilePaths <-
concat <$>
traverse ( getFilesIn hieExt )
Expand All @@ -125,13 +124,16 @@ mainWithConfig hieExt hieDirectories requireHsFiles Config{ rootPatterns, typeCl
nameCache <-
initNameCache 'z' []

hieFileResults <-
mapM ( readCompatibleHieFileOrExit nameCache ) hieFilePaths

let
hieFileResults' = flip filter hieFileResults \hieFileResult ->
let hsFileExists = any ( hie_hs_file hieFileResult `isSuffixOf` ) hsFilePaths
in requireHsFiles ==> hsFileExists

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
execStateT ( analyseHieFiles weederConfig hieFileResults' ) emptyAnalysis

let
roots =
Expand All @@ -146,7 +148,7 @@ mainWithConfig hieExt hieDirectories requireHsFiles Config{ rootPatterns, typeCl
reachableSet =
reachable
analysis
( Set.map DeclarationRoot roots <> bool mempty ( Set.map DeclarationRoot ( implicitRoots analysis ) ) typeClassRoots )
( Set.map DeclarationRoot roots <> filterImplicitRoots (prettyPrintedType analysis) ( implicitRoots analysis ) )

dead =
allDeclarations analysis Set.\\ reachableSet
Expand All @@ -166,18 +168,37 @@ mainWithConfig hieExt hieDirectories requireHsFiles Config{ rootPatterns, typeCl

for_ ( Map.toList warnings ) \( path, declarations ) ->
for_ (sortOn (srcLocLine . fst) declarations) \( start, d ) ->
putStrLn $ showWeed path start d
case Map.lookup d (prettyPrintedType analysis) of
Nothing -> putStrLn $ showWeed path start d
Just t -> putStrLn $ showPath path start <> "(Instance) :: " <> t

let exitCode = if null warnings then ExitSuccess else ExitFailure 1

pure (exitCode, analysis)

where

filterImplicitRoots printedTypeMap = Set.filter $ \case
DeclarationRoot _ -> True -- keep implicit roots for rewrite rules
ModuleRoot _ -> True
InstanceRoot d c -> typeClassRoots || any (occNameString c =~) rootClasses || matchingType
where
matchingType = case Map.lookup d printedTypeMap of
Just t -> any (t =~) rootInstances
Nothing -> False


showWeed :: FilePath -> RealSrcLoc -> Declaration -> String
showWeed path start d =
path <> ":" <> show ( srcLocLine start ) <> ": "
showPath path start
<> occNameString ( declOccName d)


showPath :: FilePath -> RealSrcLoc -> String
showPath path start =
path <> ":" <> show ( srcLocLine start ) <> ": "


-- | Recursively search for files with the given extension in given directory
getFilesIn
:: String
Expand Down
52 changes: 38 additions & 14 deletions test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,62 @@ import System.IO (stdout, 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
zipWithM_ (uncurry integrationTestSpec) testOutputFiles hieDirectories
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
Expand Down
2 changes: 2 additions & 0 deletions test/Spec/ApplicativeDo.failing
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test/Spec/ApplicativeDo/ApplicativeDo.hs:6: (Instance) :: Functor Foo
test/Spec/ApplicativeDo/ApplicativeDo.hs:9: (Instance) :: Applicative Foo
Empty file added test/Spec/ApplicativeDo.stdout
Empty file.
3 changes: 3 additions & 0 deletions test/Spec/ApplicativeDo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
roots = [ "Spec.ApplicativeDo.ApplicativeDo.root" ]

type-class-roots = false
17 changes: 17 additions & 0 deletions test/Spec/ApplicativeDo/ApplicativeDo.hs
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions test/Spec/DeriveGeneric.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/Spec/DeriveGeneric/DeriveGeneric.hs:12: (Instance) :: FromJSON T
3 changes: 3 additions & 0 deletions test/Spec/DeriveGeneric.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
roots = [ "Spec.DeriveGeneric.DeriveGeneric.t" ]

type-class-roots = false
15 changes: 15 additions & 0 deletions test/Spec/DeriveGeneric/DeriveGeneric.hs
Original file line number Diff line number Diff line change
@@ -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
Empty file.
7 changes: 7 additions & 0 deletions test/Spec/InstanceRootConstraint.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
roots = []

type-class-roots = false

root-classes = []

root-instances = [ 'Foo a => Foo \[a\]' ]
13 changes: 13 additions & 0 deletions test/Spec/InstanceRootConstraint/InstanceRootConstraint.hs
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 2 additions & 0 deletions test/Spec/InstanceTypeclass.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test/Spec/InstanceTypeclass/InstanceTypeclass.hs:4: Foo
test/Spec/InstanceTypeclass/InstanceTypeclass.hs:10: (Instance) :: Foo Char
5 changes: 5 additions & 0 deletions test/Spec/InstanceTypeclass.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
roots = []

type-class-roots = false

root-instances = [ "RootClass Char" ]
20 changes: 20 additions & 0 deletions test/Spec/InstanceTypeclass/InstanceTypeclass.hs
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions test/Spec/Monads.failing
Original file line number Diff line number Diff line change
@@ -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'
Empty file added test/Spec/Monads.stdout
Empty file.
3 changes: 3 additions & 0 deletions test/Spec/Monads.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
roots = [ "Spec.Monads.Monads.foo", "Spec.Monads.Monads.bar" ]

type-class-roots = false
38 changes: 38 additions & 0 deletions test/Spec/Monads/Monads.hs
Original file line number Diff line number Diff line change
@@ -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
Empty file added test/Spec/NumInstance.stdout
Empty file.
3 changes: 3 additions & 0 deletions test/Spec/NumInstance.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
roots = [ "Spec.NumInstance.NumInstance.two" ]

type-class-roots = false
17 changes: 17 additions & 0 deletions test/Spec/NumInstance/NumInstance.hs
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions test/Spec/NumInstanceLiteral.failing
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs:7: (Instance) :: Num Modulo1
Empty file.
3 changes: 3 additions & 0 deletions test/Spec/NumInstanceLiteral.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
roots = [ "Spec.NumInstanceLiteral.NumInstanceLiteral.zero" ]

type-class-roots = false
12 changes: 12 additions & 0 deletions test/Spec/NumInstanceLiteral/NumInstanceLiteral.hs
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions test/Spec/OverloadedLabels.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test/Spec/OverloadedLabels/OverloadedLabels.hs:17: (Instance) :: Has Point "y" Int
3 changes: 3 additions & 0 deletions test/Spec/OverloadedLabels.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
roots = [ "Spec.OverloadedLabels.OverloadedLabels.root" ]

type-class-roots = false
Loading