dfithian / prune-juice

Prune unused Haskell dependencies.
MIT License
48 stars 4 forks source link

Exposed modules parsed for `relude` are incorrect #9

Closed elldritch closed 2 years ago

elldritch commented 3 years ago

I was using this tool on one of my projects that uses Relude, an alternative Prelude. Interestingly, this tool appears to always report that relude is an unused dependency. I dug into the source code to see why.

I think the issue is in getDependencyByModule and parsePkg in Data.Prune.Dependency, where this tool appears to believe that relude only exports Data.ByteString. I think the root cause of this is a very, very weird exposed-modules entry when running ghc-pkg dump. Check out the dump for relude:

---
name:                 relude
version:              1.0.0.1
visibility:           public
id:
    relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808

key:
    relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808

license:              MIT
copyright:            2016 Stephen Diehl, 2016-2018 Serokell, 2018-2021 Kowainik
maintainer:           Kowainik <xrom.xkov@gmail.com>
author:
    Dmitrii Kovanikov, Veronika Romashkina, Stephen Diehl, Serokell

stability:            stable
homepage:             https://github.com/kowainik/relude
synopsis:
    Safe, performant, user-friendly and lightweight Haskell Standard Library

description:
    @__relude__@ is an alternative prelude library. If you find the default
    @Prelude@ unsatisfying, despite its advantages, consider using @relude@
    instead.

    == Relude goals and design principles
    * __Productivity.__ You can be more productive with a "non-standard" standard
      library, and @relude@ helps you with writing safer and more
      efficient code faster.

    * __Total programming__. Usage of [/partial functions/](https://www.reddit.com/r/haskell/comments/5n51u3/why_are_partial_functions_as_in_head_tail_bad/)
      can lead to unexpected bugs and runtime exceptions in pure
      code. The types of partial functions lie about their behaviour. And
      even if it is not always possible to rely only on total functions,
      @relude@ strives to encourage best-practices and reduce the
      chances of introducing a bug.

        +---------------------------------+--------------------------------------------+
        | __Partial__                     | __Total__                                  |
        +=================================+============================================+
        | @head :: [a] -> a@              | @head :: NonEmpty a -> a@                  |
        +---------------------------------+--------------------------------------------+
        | @tail :: [a] -> [a]@            | @tail :: NonEmpty a -> [a]@                |
        +---------------------------------+--------------------------------------------+
        | @read :: Read a => String -> a@ | @readMaybe :: Read a => String -> Maybe a@ |
        +---------------------------------+--------------------------------------------+
        | @fromJust :: Maybe a -> a@      | @fromMaybe :: a -> Maybe a -> a@           |
        +---------------------------------+--------------------------------------------+

    * __Type-safety__. We use the /"make invalid states unrepresentable"/ motto as one
      of our guiding principles. If it is possible, we express this concept through the
      types.

        /Example:/ @ whenNotNull :: Applicative f => [a] -> (NonEmpty a -> f ()) -> f () @

    * __Performance.__ We prefer @Text@ over @[String](https://www.reddit.com/r/haskell/comments/29jw0s/whats_wrong_with_string/)@,
      use space-leaks-free functions (e.g. our custom performant @sum@ and @product@),
      introduce @\{\-\# INLINE \#\-\}@ and @\{\-\# SPECIALIZE \#\-\}@ pragmas where
      appropriate, and make efficient container types
      (e.g. @Map@, @HashMap@, @Set@) more accesible.

    * __Minimalism__ (low number of dependencies). We do not force users of
      @relude@ to stick to any specific lens or text formatting or logging
      library. Where possible, @relude@ depends only on boot libraries.
      The [Dependency graph](https://raw.githubusercontent.com/kowainik/relude/main/relude-dependency-graph.png)
      of @relude@ can give you a clearer picture.

    * __Convenience__. Despite minimalism, we want to bring commonly used
       types and functions into scope, and make available functions easier
       to use. Some examples of conveniences:

        1. No need to add @containers@, @unordered-containers@, @text@
           and @bytestring@ to dependencies in your @.cabal@ file to
           use the main API of these libraries
        2. No need to import types like @NonEmpty@, @Text@, @Set@, @Reader[T]@, @MVar@, @STM@
        3. Functions like @liftIO@, @fromMaybe@, @sortWith@ are avaiable by default as well
        4. @IO@ actions are lifted to @MonadIO@

    * __Excellent documentation.__

        1. Tutorial
        2. Migration guide from @Prelude@
        3. Haddock for every function with examples tested by
           [doctest](http://hackage.haskell.org/package/doctest).
        4. Documentation regarding [internal module structure](http://hackage.haskell.org/package/relude/docs/Relude.html)
        5. @relude@-specific [HLint](http://hackage.haskell.org/package/hlint) rules: @[.hlint.yaml](https://github.com/kowainik/relude/blob/main/.hlint.yaml)@

    * __User-friendliness.__ Anyone should be able to quickly migrate to @relude@. Only
      some basic familiarity with the common libraries like @text@ and @containers@
      should be enough (but not necessary).

    * __Exploration.__ We have space to experiment with new ideas and proposals
      without introducing breaking changes. @relude@ uses the approach with
      @Extra.*@ modules which are not exported by default. The chosen approach makes it quite
      easy for us to provide new functionality without breaking anything and let
      the users decide to use it or not.

category:             Prelude
abi:                  5bbba46f609ef2239cb4d5e62e650110
exposed:              True
exposed-modules:
    Data.ByteString from bytestring-0.10.12.0:Data.ByteString,
    Data.ByteString.Builder from bytestring-0.10.12.0:Data.ByteString.Builder,
    Data.ByteString.Lazy from bytestring-0.10.12.0:Data.ByteString.Lazy,
    Data.ByteString.Short from bytestring-0.10.12.0:Data.ByteString.Short,
    Data.HashMap.Lazy from unordered-containers-0.2.14.0-6d16ff55bf3bc9699b38585b32ecd851874676643cb894fbd00961f32a8ba9df:Data.HashMap.Lazy,
    Data.HashMap.Strict from unordered-containers-0.2.14.0-6d16ff55bf3bc9699b38585b32ecd851874676643cb894fbd00961f32a8ba9df:Data.HashMap.Strict,
    Data.HashSet from unordered-containers-0.2.14.0-6d16ff55bf3bc9699b38585b32ecd851874676643cb894fbd00961f32a8ba9df:Data.HashSet,
    Data.IntMap.Lazy from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.IntMap.Lazy,
    Data.IntMap.Strict from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.IntMap.Strict,
    Data.IntSet from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.IntSet,
    Data.Map.Lazy from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.Map.Lazy,
    Data.Map.Strict from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.Map.Strict,
    Data.Sequence from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.Sequence,
    Data.Set from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.Set,
    Data.Text from text-1.2.5.0-a82e88cd69980d546206b725d4749750622040954f01b27d63d35acf234e85fb:Data.Text,
    Data.Text.IO from text-1.2.5.0-a82e88cd69980d546206b725d4749750622040954f01b27d63d35acf234e85fb:Data.Text.IO,
    Data.Text.Lazy from text-1.2.5.0-a82e88cd69980d546206b725d4749750622040954f01b27d63d35acf234e85fb:Data.Text.Lazy,
    Data.Text.Lazy.IO from text-1.2.5.0-a82e88cd69980d546206b725d4749750622040954f01b27d63d35acf234e85fb:Data.Text.Lazy.IO,
    Data.Text.Read from text-1.2.5.0-a82e88cd69980d546206b725d4749750622040954f01b27d63d35acf234e85fb:Data.Text.Read,
    Data.Tree from containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb:Data.Tree,
    Relude, Relude.Applicative, Relude.Base, Relude.Bool,
    Relude.Bool.Guard, Relude.Bool.Reexport, Relude.Container,
    Relude.Container.One, Relude.Container.Reexport, Relude.Debug,
    Relude.DeepSeq, Relude.Enum, Relude.Exception, Relude.Extra,
    Relude.Extra.Bifunctor, Relude.Extra.CallStack, Relude.Extra.Enum,
    Relude.Extra.Foldable, Relude.Extra.Foldable1, Relude.Extra.Group,
    Relude.Extra.Lens, Relude.Extra.Map, Relude.Extra.Newtype,
    Relude.Extra.Tuple, Relude.Extra.Type, Relude.File,
    Relude.Foldable, Relude.Foldable.Fold, Relude.Foldable.Reexport,
    Relude.Function, Relude.Functor, Relude.Functor.Fmap,
    Relude.Functor.Reexport, Relude.Lifted, Relude.Lifted.Concurrent,
    Relude.Lifted.Env, Relude.Lifted.Exit, Relude.Lifted.File,
    Relude.Lifted.Handle, Relude.Lifted.IORef, Relude.Lifted.Terminal,
    Relude.List, Relude.List.NonEmpty, Relude.List.Reexport,
    Relude.Monad, Relude.Monad.Either, Relude.Monad.Maybe,
    Relude.Monad.Reexport, Relude.Monad.Trans, Relude.Monoid,
    Relude.Nub, Relude.Numeric, Relude.Print, Relude.String,
    Relude.String.Conversion, Relude.String.Reexport, Relude.Unsafe

import-dirs:
    /home/leo/.cabal/store/ghc-8.10.7/relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808/lib

library-dirs:
    /home/leo/.cabal/store/ghc-8.10.7/relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808/lib

dynamic-library-dirs:
    /home/leo/.cabal/store/ghc-8.10.7/relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808/lib

data-dir:
    /home/leo/.cabal/store/ghc-8.10.7/relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808/share

hs-libraries:
    HSrelude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808

depends:
    base-4.14.3.0 bytestring-0.10.12.0
    containers-0.6.4.1-8ebbcbb1a51aafd2419e5d1d25685687393d645807ecea808f2b72a1091fa5bb
    deepseq-1.4.4.0 ghc-prim-0.6.1
    hashable-1.3.3.0-f4fcf519b5f7f43af478c3853bfa8ec857729a143712b9e59eb05024f55381a0
    mtl-2.2.2 stm-2.5.0.1
    text-1.2.5.0-a82e88cd69980d546206b725d4749750622040954f01b27d63d35acf234e85fb
    transformers-0.5.6.2
    unordered-containers-0.2.14.0-6d16ff55bf3bc9699b38585b32ecd851874676643cb894fbd00961f32a8ba9df

haddock-interfaces:
    /home/leo/.cabal/store/ghc-8.10.7/relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808/share/doc/html/relude.haddock

haddock-html:
    /home/leo/.cabal/store/ghc-8.10.7/relude-1.0.0.1-25d8bac889ac411e1750206040db62fdb129150d83a0024e016d9310fd690808/share/doc/html
pkgroot: "/home/leo/.cabal/store/ghc-8.10.7"
---

Notice the output format in exposed-modules using comma-delimited names and MODULE from PACKAGE entries. I've never seen this format before, and I couldn't find this format being used to describe the exposed-modules of any other package in my projects. Have you seen this before? Is this due to a module re-export?

This output seems to break the exposedModules parser, which does not support this format. I'm happy to contribute a PR to add this format to the parser, but I'm curious about whether you've seen this output format before. I can't find any documentation on it.

Given that parseDependencyName and parseExposedModules both seem to only ever be used in ghc-pkg output parsing, I'm tempted to try to toss the whole parser and just use the Read instance for InstalledPackageInfo. Any objections to that?

elldritch commented 3 years ago

just use the Read instance for InstalledPackageInfo

I'm an idiot. I thought this was a parser, but it's just a derived instance. :disappointed:

dfithian commented 2 years ago

Nice find! I wondered if something like that was happening, but I think as is often the case with alternative preludes, the point is moot as you're going to be including it in every module. I do think this is a good idea though, and to answer your question I haven't seen it before but it would appear it's related to re-exports, as you suggest.