packages feed

hfmt (empty) → 0.0.1.0

raw patch · 17 files changed

+653/−0 lines, 17 filesdep +Cabaldep +Diffdep +HUnitsetup-changed

Dependencies added: Cabal, Diff, HUnit, ansi-wl-pprint, base, directory, filepath, haskell-src-exts, hfmt, hindent, hlint, optparse-applicative, pipes, pretty, stylish-haskell, test-framework, test-framework-hunit, text

Files

+ LICENSE view
@@ -0,0 +1,22 @@+The MIT License (MIT)
+
+Copyright (c) 2014 Daniel Stiner
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple
+main = defaultMain
+ app/Actions.hs view
@@ -0,0 +1,46 @@+module Actions (act) where
+
+import Language.Haskell.Format
+import OptionsParser
+import Types
+
+import Control.Monad
+import Data.Algorithm.Diff
+import Data.Algorithm.DiffContext
+import Data.Algorithm.DiffOutput
+import Text.PrettyPrint
+
+act :: Options -> ReformatResult -> IO ()
+act options (InvalidReformat input errorString) =
+  putStrLn ("Error reformatting " ++ show input ++ ": " ++ errorString)
+act options (Reformat input source result) = act' (optAction options)
+  where
+    act' PrintDiffs = when wasReformatted (printDiff input source result)
+    act' PrintSources = undefined
+    act' PrintFilePaths = when wasReformatted (print input)
+    act' WriteSources = when wasReformatted (writeSource input (reformattedSource result))
+    wasReformatted = sourceChangedOrHasSuggestions source result
+
+sourceChangedOrHasSuggestions :: HaskellSource -> Reformatted -> Bool
+sourceChangedOrHasSuggestions source reformatted =
+  not (null (suggestions reformatted)) || source /= reformattedSource reformatted
+
+printDiff :: InputFile -> HaskellSource -> Reformatted -> IO ()
+printDiff (InputFilePath path) source reformatted = do
+  putStrLn (path ++ ":")
+  mapM_ (putStr . show) (suggestions reformatted)
+  putStr (showDiff source (reformattedSource reformatted))
+printDiff (InputFromStdIn) source reformatted = do
+  mapM_ (putStr . show) (suggestions reformatted)
+  putStr (showDiff source (reformattedSource reformatted))
+
+showDiff :: HaskellSource -> HaskellSource -> String
+showDiff (HaskellSource a) (HaskellSource b) = render (toDoc diff)
+  where
+    toDoc = prettyContextDiff (text "Original") (text "Reformatted") text
+    diff = getContextDiff linesOfContext (lines a) (lines b)
+    linesOfContext = 1
+
+writeSource :: InputFile -> HaskellSource -> IO ()
+writeSource (InputFilePath path) (HaskellSource source) = writeFile path source
+writeSource InputFromStdIn (HaskellSource source) = putStr source
+ app/Main.hs view
@@ -0,0 +1,48 @@+module Main (main) where
+
+import           Actions
+import           Language.Haskell.Format
+import           Language.Haskell.Format.Utilities  hiding (wasReformatted)
+import           Language.Haskell.Source.Enumerator
+import           OptionsParser                      as Options
+import           Types
+
+import           Control.Applicative
+import           Control.Monad
+import           Data.List
+import           Data.Maybe
+import           Data.Monoid
+import           Options.Applicative.Extra          as OptApp
+import           Pipes
+import qualified Pipes.Prelude                      as P
+
+main :: IO ()
+main = do
+  options <- execParser Options.parser
+  formatter <- defaultFormatter
+  runEffect $ inputFiles options >-> P.map (reformat formatter) >-> P.mapM_ (Actions.act options)
+
+inputFiles :: Options -> Producer InputFileWithSource IO ()
+inputFiles options = determineInputFilePaths (optPaths options) >-> P.mapM readInputFile
+  where
+    determineInputFilePaths :: [FilePath] -> Producer InputFile IO ()
+    determineInputFilePaths [] = enumeratePath "." >-> P.map InputFilePath
+    determineInputFilePaths ["-"] = yield InputFromStdIn
+    determineInputFilePaths paths = for (each paths) enumeratePath >-> P.map InputFilePath
+
+    readInputFile :: InputFile -> IO InputFileWithSource
+    readInputFile (InputFilePath path) = InputFileWithSource (InputFilePath path) <$> readSource
+                                                                                        path
+    readInputFile (InputFromStdIn) = InputFileWithSource InputFromStdIn <$> readStdin
+
+reformat :: Formatter -> InputFileWithSource -> ReformatResult
+reformat (Formatter format) (InputFileWithSource input source) =
+  case format source of
+    Left error        -> InvalidReformat input error
+    Right reformatted -> Reformat input source reformatted
+
+readSource :: HaskellSourceFilePath -> IO HaskellSource
+readSource path = HaskellSource <$> readFile path
+
+readStdin :: IO HaskellSource
+readStdin = HaskellSource <$> getContents
+ app/OptionsParser.hs view
@@ -0,0 +1,77 @@+module OptionsParser (Options, optAction, optPaths, parser) where
+
+import Types
+
+import Control.Applicative
+import Data.List
+import Data.Monoid
+import Options.Applicative.Builder
+import Options.Applicative.Common
+import Options.Applicative.Extra
+import Text.PrettyPrint.ANSI.Leijen hiding (empty, (<$>), (<>))
+
+data Options =
+       Options
+         { optPrintDiffs     :: Bool
+         , optPrintSources   :: Bool
+         , optPrintFilePaths :: Bool
+         , optWriteSources   :: Bool
+         , optPaths          :: [FilePath]
+         }
+
+optAction :: Options -> Action
+optAction options
+  | optPrintDiffs options = PrintDiffs
+  | optPrintFilePaths options = PrintFilePaths
+  | optPrintSources options = PrintSources
+  | optWriteSources options = WriteSources
+  | optPaths options == ["-"] = PrintSources
+  | otherwise = PrintDiffs
+
+optionParser :: Parser Options
+optionParser = Options <$> switch
+                             (long "print-diffs" <>
+                              short 'd' <>
+                              help "If a file's formatting is different, print a diff.")
+                       <*> switch
+                             (long "print-sources" <>
+                              short 's' <>
+                              help "If a file's formatting is different, print its source.")
+                       <*> switch
+                             (long "print-paths" <>
+                              short 'l' <>
+                              help "If a file's formatting is different, print its path.")
+                       <*> switch
+                             (long "write-sources" <>
+                              short 'w' <>
+                              help "If a file's formatting is different, overwrite it.")
+                       <*> many pathOption
+  where
+    pathOption = strArgument (metavar "PATH" <> pathOptionHelp)
+    pathOptionHelp = helpDoc $ Just $
+      paragraph "Explicit paths to process." <> hardline
+      <> unorderdedList " - "
+           [ paragraph "A single '-' will process standard input."
+           , paragraph "Files will be processed directly."
+           , paragraph "Directories will be recursively searched for source files to process."
+           , paragraph
+               ".cabal files will be parsed and all specified source directories and files processed."
+           , paragraph
+               "If no paths are given, the current directory will be searched for .cabal files to process, if none are found the currend directory will be recursively searched for source files to process."
+           ]
+
+paragraph :: String -> Doc
+paragraph = mconcat . intersperse softline . map text . words
+
+unorderdedList :: String -> [Doc] -> Doc
+unorderdedList prefix = encloseSep (text prefix) mempty (text prefix) . map (hang 0)
+
+optionParserInfo :: ParserInfo Options
+optionParserInfo = info (helper <*> optionParser)
+                     (fullDesc
+                      <> header "hfmt - formats Haskell programs"
+                      <> progDesc
+                           "Operates on Haskell source files, reformatting them by applying suggestions from hlint, hindent, and stylish-haskell. Inspired by the gofmt utility.")
+
+parser :: ParserInfo Options
+parser = optionParserInfo
+ app/Types.hs view
@@ -0,0 +1,31 @@+module Types (
+    Action(..),
+    InputFile(..),
+    InputFileWithSource(..),
+    ErrorString,
+    ReformatResult(..),
+    HaskellSourceFilePath,
+    HaskellSource(..),
+    ) where
+
+import Language.Haskell.Format
+import Language.Haskell.Source.Enumerator (HaskellSourceFilePath)
+
+data Action = PrintDiffs
+            | PrintSources
+            | PrintFilePaths
+            | WriteSources
+
+data InputFile = InputFilePath HaskellSourceFilePath
+               | InputFromStdIn
+
+instance Show InputFile where
+  show (InputFilePath path) = path
+  show InputFromStdIn = "-"
+
+data InputFileWithSource = InputFileWithSource InputFile HaskellSource
+
+type ErrorString = String
+
+data ReformatResult = InvalidReformat InputFile ErrorString
+                    | Reformat InputFile HaskellSource Reformatted
+ hfmt.cabal view
@@ -0,0 +1,79 @@+name:                hfmt
+version:             0.0.1.0
+synopsis:            Haskell source code formatter
+description:         Inspired by gofmt. Built using hlint, hindent, and stylish-haskell.
+license:             MIT
+license-file:        LICENSE
+author:              Daniel Stiner
+maintainer:          Daniel Stiner <daniel.stiner@gmail.com>
+stability:           Experimental
+homepage:            http://github.com/danstiner/hfmt
+bug-reports:         http://github.com/danstiner/hfmt/issues
+category:            Language
+build-type:          Simple
+cabal-version:       >=1.9.2
+tested-with:         GHC==7.8.4, GHC==7.10.2, GHC == 7.11.*
+
+library
+  Hs-Source-Dirs:    src
+  Exposed-Modules:   Language.Haskell.Format
+                     , Language.Haskell.Format.Utilities
+                     , Language.Haskell.Source.Enumerator
+  Other-Modules:     Language.Haskell.Format.Internal
+                     , Language.Haskell.Format.Definitions
+                     , Language.Haskell.Format.HIndent
+                     , Language.Haskell.Format.HLint
+                     , Language.Haskell.Format.Stylish
+  GHC-Options:       -Wall
+  Build-Depends:     base == 4.*
+                     , Cabal
+                     , Diff
+                     , directory
+                     , filepath
+                     , haskell-src-exts
+                     , hindent >= 4.5
+                     , hlint >= 1.9.13 , hlint < 2
+                     , HUnit
+                     , pipes
+                     , pretty
+                     , stylish-haskell
+                     , text
+
+executable hfmt
+  Hs-Source-Dirs:    app
+  Main-Is:           Main.hs
+  Other-Modules:     Actions
+                   , OptionsParser
+                   , Types
+  GHC-Options:       -Wall
+  Build-Depends:     base == 4.*
+                   , hfmt
+                   , ansi-wl-pprint
+                   , Diff
+                   , optparse-applicative
+                   , pipes
+                   , pretty
+
+test-suite self-formatting-test
+  Type:              exitcode-stdio-1.0
+  Hs-Source-Dirs:    test/self-formatting
+  Main-Is:           Spec.hs
+  Build-Depends:     base == 4.*
+                     , hfmt
+                     , HUnit
+                     , test-framework
+                     , test-framework-hunit
+
+test-suite pure-test
+  Type:              exitcode-stdio-1.0
+  Hs-Source-Dirs:    test/pure
+  Main-Is:           Spec.hs
+  Build-Depends:     base == 4.*
+                     , hfmt
+                     , HUnit
+                     , test-framework
+                     , test-framework-hunit
+
+source-repository head
+  type: git
+  location: git://github.com:danstiner/hfmt.git
+ src/Language/Haskell/Format.hs view
@@ -0,0 +1,42 @@+module Language.Haskell.Format (
+    autoSettings,
+    format,
+    formatters,
+    hlint,
+    hindent,
+    stylish,
+    Settings,
+    Formatter(..),
+    Suggestion(..),
+    HaskellSource(..),
+    Reformatted(..),
+    ) where
+
+import           Language.Haskell.Format.Definitions
+import qualified Language.Haskell.Format.HIndent     as HIndent
+import qualified Language.Haskell.Format.HLint       as HLint
+import qualified Language.Haskell.Format.Stylish     as Stylish
+
+import           Control.Applicative
+import           Data.Maybe
+import           Language.Haskell.HLint3             (Idea)
+
+data Settings = Settings
+
+autoSettings :: IO Settings
+autoSettings = return Settings
+
+hlint :: Settings -> IO Formatter
+hlint _ = HLint.suggester <$> HLint.autoSettings
+
+hindent :: Settings -> IO Formatter
+hindent _ = HIndent.formatter <$> HIndent.autoSettings
+
+stylish :: Settings -> IO Formatter
+stylish _ = Stylish.formatter <$> Stylish.autoSettings
+
+formatters :: Settings -> IO [Formatter]
+formatters s = sequence [hlint s, hindent s, stylish s]
+
+format :: Formatter -> HaskellSource -> Either String Reformatted
+format = unFormatter
+ src/Language/Haskell/Format/Definitions.hs view
@@ -0,0 +1,31 @@+module Language.Haskell.Format.Definitions (HaskellSource(..), Reformatted(..), Formatter(..), Suggestion(..)) where
+
+import Control.Applicative
+import Control.Monad
+import Data.Monoid
+
+type ErrorString = String
+
+newtype HaskellSource = HaskellSource String
+  deriving Eq
+
+newtype Suggestion = Suggestion String
+
+instance Show Suggestion where
+  show (Suggestion text) = text
+
+data Reformatted = Reformatted { reformattedSource :: HaskellSource, suggestions :: [Suggestion] }
+
+instance Monoid Reformatted where
+  mempty = Reformatted undefined []
+  (Reformatted _ suggestionsA) `mappend` (Reformatted sourceB suggestionsB) = Reformatted sourceB
+                                                                                (suggestionsA <> suggestionsB)
+
+data Formatter = Formatter { unFormatter :: HaskellSource -> Either ErrorString Reformatted }
+
+instance Monoid Formatter where
+  mempty = Formatter (\source -> Right (Reformatted source []))
+  (Formatter f) `mappend` (Formatter g) = Formatter (asReformatter g <=< f)
+
+asReformatter :: (HaskellSource -> Either ErrorString Reformatted) -> Reformatted -> Either ErrorString Reformatted
+asReformatter formatter r = (r <>) <$> formatter (reformattedSource r)
+ src/Language/Haskell/Format/HIndent.hs view
@@ -0,0 +1,28 @@+module Language.Haskell.Format.HIndent (autoSettings, formatter, defaultFormatter) where
+
+import Control.Applicative
+import Data.Maybe
+import Data.Text.Lazy                      as L
+import Data.Text.Lazy.Builder
+import HIndent
+import Language.Haskell.Exts.Extension     (Extension)
+
+import Language.Haskell.Format.Definitions
+import Language.Haskell.Format.Internal
+
+data Settings = Settings Style (Maybe [Extension])
+
+defaultFormatter :: IO Formatter
+defaultFormatter = formatter <$> autoSettings
+
+autoSettings :: IO Settings
+autoSettings = return (Settings gibiansky Nothing)
+
+formatter :: Settings -> Formatter
+formatter = mkFormatter . hindent
+
+hindent :: Settings -> HaskellSource -> Either String HaskellSource
+hindent (Settings style extensions) (HaskellSource source) =
+  HaskellSource . L.unpack . toLazyText <$> reformat style extensions sourceText
+  where
+    sourceText = L.pack source
+ src/Language/Haskell/Format/HLint.hs view
@@ -0,0 +1,41 @@+module Language.Haskell.Format.HLint (autoSettings, formatter, suggester) where
+
+import Language.Haskell.Format.Definitions
+import Language.Haskell.Format.Internal
+
+import Control.Applicative
+import Data.Maybe
+import Language.Haskell.Exts.Annotated     as Hse
+import Language.Haskell.HLint3             hiding (autoSettings)
+
+type Settings = (ParseMode, [Classify], Hint)
+
+formatter = undefined
+
+suggester :: (ParseMode, [Classify], Hint) -> Formatter
+suggester = mkSuggester . hlint
+
+hlint :: (ParseMode, [Classify], Hint) -> HaskellSource -> Either String [Suggestion]
+hlint (parseMode, classifications, hint) (HaskellSource source) =
+  getSuggestions <$> parseResultAsEither (parse source)
+  where
+    parse = Hse.parseFileContentsWithComments parseMode
+    getSuggestions moduleSource = map ideaToSuggestion $ applyHints classifications hint
+                                                           [moduleSource]
+
+autoSettings :: IO (ParseMode, [Classify], Hint)
+autoSettings = do
+  (fixities, classify, hints) <- findSettings (readSettingsFile Nothing) Nothing
+  return (hseFlags (parseFlagsAddFixities fixities defaultParseFlags), classify, resolveHints hints)
+
+parseResultAsEither :: ParseResult a -> Either String a
+parseResultAsEither (ParseOk a) = Right a
+parseResultAsEither (ParseFailed loc error) =
+  Left ("Parse failed at " ++ location ++ ": " ++ error)
+  where
+    location = filename ++ " " ++ linecol
+    filename = "[" ++ srcFilename loc ++ "]"
+    linecol = "(" ++ show (srcLine loc) ++ ":" ++ show (srcColumn loc) ++ ")"
+
+ideaToSuggestion :: Idea -> Suggestion
+ideaToSuggestion = Suggestion . show
+ src/Language/Haskell/Format/Internal.hs view
@@ -0,0 +1,11 @@+module Language.Haskell.Format.Internal (mkFormatter, mkSuggester) where
+
+import Language.Haskell.Format.Definitions
+
+import Control.Applicative
+
+mkFormatter :: (HaskellSource -> Either String HaskellSource) -> Formatter
+mkFormatter f = Formatter (fmap (\source -> Reformatted source []) . f)
+
+mkSuggester :: (HaskellSource -> Either String [Suggestion]) -> Formatter
+mkSuggester f = Formatter $ \source -> Reformatted source <$> f source
+ src/Language/Haskell/Format/Stylish.hs view
@@ -0,0 +1,23 @@+module Language.Haskell.Format.Stylish (autoSettings, formatter) where
+
+import Control.Applicative
+import Language.Haskell.Stylish            as Stylish
+
+import Language.Haskell.Format.Definitions
+import Language.Haskell.Format.Internal
+
+data Settings = Settings Stylish.Config
+
+autoSettings :: IO Settings
+autoSettings = Settings <$> Stylish.loadConfig (Stylish.makeVerbose False) Nothing
+
+formatter :: Settings -> Formatter
+formatter = mkFormatter . stylish
+
+stylish :: Settings -> HaskellSource -> Either String HaskellSource
+stylish (Settings config) (HaskellSource source) =
+  HaskellSource . unlines <$> Stylish.runSteps extensions Nothing steps sourceLines
+  where
+    sourceLines = lines source
+    extensions = Stylish.configLanguageExtensions config
+    steps = Stylish.configSteps config
+ src/Language/Haskell/Format/Utilities.hs view
@@ -0,0 +1,60 @@+module Language.Haskell.Format.Utilities (wasReformatted, hunitTest, defaultFormatter) where
+
+import           Language.Haskell.Format
+import           Language.Haskell.Format.Definitions
+import           Language.Haskell.Source.Enumerator
+
+import           Control.Applicative
+import           Control.Monad
+import           Data.Monoid
+import           Pipes
+import qualified Pipes.Prelude                       as P
+import           Test.HUnit
+
+type ErrorString = String
+
+data CheckResult = InvalidCheckResult ErrorString
+                 | CheckResult HaskellSource Reformatted
+
+hunitTest :: FilePath -> Test
+hunitTest filepath = TestLabel filepath $ makeTestCase filepath
+
+makeTestCase :: FilePath -> Test
+makeTestCase filepath = TestCase $ do
+  formatter <- defaultFormatter
+  runEffect $ check formatter filepath >-> assertResults
+
+assertResults :: Consumer CheckResult IO ()
+assertResults = forever $ do
+  result <- await
+  case result of
+    (InvalidCheckResult errorString) -> lift $ assertFailure ("Error: " ++ errorString)
+    (CheckResult source reformatted) -> when (wasReformatted source reformatted) $ lift $ assertFailure
+                                                                                            ("Incorrect formatting: " ++ concatMap
+                                                                                                                           show
+                                                                                                                           (suggestions
+                                                                                                                              reformatted))
+
+check :: Formatter -> FilePath -> Producer CheckResult IO ()
+check formatter path = enumeratePath path >->
+                       P.mapM readSource >->
+                       P.map (checkFormatting formatter)
+
+readSource :: HaskellSourceFilePath -> IO HaskellSource
+readSource path = HaskellSource <$> readFile path
+
+checkFormatting :: Formatter -> HaskellSource -> CheckResult
+checkFormatting (Formatter format) source =
+  case format source of
+    Left error        -> InvalidCheckResult error
+    Right reformatted -> CheckResult source reformatted
+
+fix :: Formatter -> FilePath -> IO ()
+fix = undefined
+
+defaultFormatter :: IO Formatter
+defaultFormatter = mconcat <$> (autoSettings >>= formatters)
+
+wasReformatted :: HaskellSource -> Reformatted -> Bool
+wasReformatted source reformatted =
+  not (null (suggestions reformatted)) || source /= reformattedSource reformatted
+ src/Language/Haskell/Source/Enumerator.hs view
@@ -0,0 +1,91 @@+module Language.Haskell.Source.Enumerator (enumeratePath, HaskellSourceFilePath) where
+
+import           Control.Applicative
+import           Control.Monad
+import           Data.List
+import           Distribution.PackageDescription
+import           Distribution.PackageDescription.Parse
+import qualified Distribution.Verbosity                as Verbosity
+import           Pipes
+import           System.Directory
+import           System.FilePath
+
+type HaskellSourceFilePath = FilePath
+
+enumeratePath :: FilePath -> Producer HaskellSourceFilePath IO ()
+enumeratePath = filePath
+
+filePath :: FilePath -> Producer HaskellSourceFilePath IO ()
+filePath path = do
+  isCabal <- lift $ isCabalFile path
+  if isCabal
+    then package' path
+    else directoryOrFile path
+
+simpleFilePath :: FilePath -> Producer HaskellSourceFilePath IO ()
+simpleFilePath path = do
+  isHaskellSource <- lift $ isHaskellSourceFile path
+  when isHaskellSource $ yield path
+
+simpleDirectory :: FilePath -> Producer HaskellSourceFilePath IO ()
+simpleDirectory path = do
+  contents <- lift $ getDirectoryContentsFullPaths path
+  mapM_ simpleFileOrDirectory contents
+
+simpleFileOrDirectory :: FilePath -> Producer HaskellSourceFilePath IO ()
+simpleFileOrDirectory filepath = do
+  dir <- lift $ doesDirectoryExist filepath
+  if dir
+    then simpleDirectory filepath
+    else simpleFilePath filepath
+
+package' :: FilePath -> Producer HaskellSourceFilePath IO ()
+package' path = readPackage path >>= expandPaths
+  where
+    readPackage = lift . readPackageDescription Verbosity.silent
+    expandPaths = mapM_ (simpleFileOrDirectory . mkFull) . sourcePaths
+    dir = dropFileName path
+    mkFull = (dir </>)
+
+directory :: FilePath -> Producer HaskellSourceFilePath IO ()
+directory path = do
+  contents <- lift $ getDirectoryContentsFullPaths path
+  cabalFiles <- lift $ filterM isCabalFile contents
+  if null cabalFiles
+    then mapM_ directoryOrFile contents
+    else mapM_ package' cabalFiles
+
+directoryOrFile :: FilePath -> Producer HaskellSourceFilePath IO ()
+directoryOrFile path = do
+  isFile <- lift $ doesFileExist path
+  if isFile
+    then simpleFilePath path
+    else directory path
+
+getDirectoryContentsFullPaths :: FilePath -> IO [FilePath]
+getDirectoryContentsFullPaths path = mkFull . notHidden . notMeta <$> getDirectoryContents path
+  where
+    mkFull = map (path </>)
+    notHidden = filter (not . isPrefixOf ".")
+    notMeta = (\\ [".", ".."])
+
+isCabalFile :: FilePath -> IO Bool
+isCabalFile path = (hasCabalExtension &&) <$> isFile
+  where
+    isFile = doesFileExist path
+    hasCabalExtension = ".cabal" `isSuffixOf` path
+
+isHaskellSourceFile :: FilePath -> IO Bool
+isHaskellSourceFile path = (hasHaskellExtension &&) <$> isFile
+  where
+    isFile = doesFileExist path
+    hasHaskellExtension = ".hs" `isSuffixOf` path || ".lhs" `isSuffixOf` path
+
+sourcePaths :: GenericPackageDescription -> [FilePath]
+sourcePaths pkg = nub $ concatMap ($ pkg) pathExtractors
+  where
+    pathExtractors = [ maybe [] (hsSourceDirs . libBuildInfo . condTreeData) . condLibrary
+                     , concatMap (hsSourceDirs . buildInfo . condTreeData . snd) . condExecutables
+                     , concatMap (hsSourceDirs . testBuildInfo . condTreeData . snd) . condTestSuites
+                     , concatMap (hsSourceDirs . benchmarkBuildInfo . condTreeData . snd) . condBenchmarks
+                     ]
+ test/pure/Spec.hs view
@@ -0,0 +1,8 @@+module Main where
+
+import Test.Framework                 (defaultMain, testGroup)
+import Test.Framework.Providers.HUnit
+import Test.HUnit                     hiding (Test)
+
+main :: IO ()
+main = defaultMain []
+ test/self-formatting/Spec.hs view
@@ -0,0 +1,13 @@+module Main where
+
+import Language.Haskell.Format.Utilities
+
+import Test.Framework                    (defaultMain, testGroup)
+import Test.Framework.Providers.HUnit
+import Test.HUnit                        hiding (Test)
+
+main :: IO ()
+main = defaultMain
+         [ testGroup "Check formatting of package sources"
+             (hUnitTestToTests $ hunitTest "hfmt.cabal")
+         ]