commit ec8c9831cdeff09ad6ea49c4a56b4ef3009ce026 Author: Ali Abrar <--help> Date: Tue Sep 14 21:56:10 2021 -0400 Init repository: backup from now-deleted evanrelf/sort-imports diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..5891416 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,32 @@ +version: 2 +jobs: + build: + branches: + only: + - master + docker: + - image: fpco/stack-build:lts-12 + steps: + - checkout + - restore_cache: + name: Restore cache + keys: + - sort-imports-{{ checksum "stack.yaml" }}-{{ checksum "package.yaml" }} + - sort-imports-{{ checksum "stack.yaml" }} + - sort-imports + - run: + name: Build dependencies + command: stack setup && stack build --fast --dependencies-only -j 1 + - run: + name: Build sort-imports + command: stack build --fast + - run: + name: Run tests + command: stack test + - save_cache: + name: Save cache + key: sort-imports-{{ checksum "package.yaml" }}-{{ checksum "stack.yaml" }} + when: always + paths: + - .stack + - .stack-work diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2994da --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ + +# Created by https://www.gitignore.io/api/haskell,macos +# Edit at https://www.gitignore.io/?templates=haskell,macos + +### Haskell ### +dist +dist-* +cabal-dev +*.o +*.hi +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ +cabal.project.local +cabal.project.local~ +.HTF/ +.ghc.environment.* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# End of https://www.gitignore.io/api/haskell,macos diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..28ca475 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2019, Evan Relf + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..99dae69 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# sort-imports + +Sort Haskell import statements + +## Install + +```bash +git clone https://github.com/evanrelf/sort-imports.git +cd sort-imports +stack install +``` + +## Usage + +Takes input on `stdin` or with `--file 'path/to/file.hs'`, and outputs to `stdout`. For example: + +```bash +sort-imports < input.hs > output.hs +``` + +`input.hs`: + +```haskell +module Main where + +import qualified ModuleC as C +import ModuleD ((>>=), function, Type(..)) +import ModuleA +import ModuleB hiding (aaa, ccc, bbb) + +import AnotherModuleB +import AnotherModuleC +import AnotherModuleA + +main :: IO () +main = putStrLn "Hello world" +``` + +`output.hs`: + +```haskell +module Main where + +import ModuleA +import ModuleB hiding (aaa, bbb, ccc) +import qualified ModuleC as C +import ModuleD (Type(..), function, (>>=)) + +import AnotherModuleA +import AnotherModuleB +import AnotherModuleC + +main :: IO () +main = putStrLn "Hello world" +``` + +Type `sort-imports --help` for more information. + +## Editor integration + +### Vim/Neovim +- [sbdchd/neoformat](https://github.com/sbdchd/neoformat) diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..58d046b --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE LambdaCase #-} + +module Main where + +import Options.Applicative +import SortImports (sortImports) +import System.Environment (getArgs) +import System.IO (hReady, stdin) + +versionNumber :: String +versionNumber = "v1.2.0" + +version :: Parser Options +version = flag' Version $ mconcat + [ long "version" + , short 'v' + , help "Version number" + ] + +data Options + = Version + | FileInput FilePath + | StdInput + +fileInput :: Parser Options +fileInput = FileInput <$> strOption (mconcat + [ long "file" + , short 'f' + , metavar "FILENAME" + , help "Input file" + ]) + +stdInput :: Parser Options +stdInput = flag' StdInput $ mconcat + [ long "stdin" + , help "Read input from stdin" + ] + +options :: Parser Options +options = version <|> fileInput <|> stdInput + +run :: Options -> IO () +run = \case + Version -> putStrLn versionNumber + FileInput path -> readFile path >>= putStr . sortImports + StdInput -> interact sortImports + +main :: IO () +main = do + noArgs <- null <$> getArgs + anyStdin <- hReady stdin + if noArgs && anyStdin + then run StdInput + else execParser opts >>= run + where opts = info (options <**> helper) $ mconcat + [ briefDesc + , header "sort-imports - Sort Haskell import statements" + , footer "Input is read from stdin by default if no arguments are provided." + ] diff --git a/package.yaml b/package.yaml new file mode 100644 index 0000000..94e132d --- /dev/null +++ b/package.yaml @@ -0,0 +1,49 @@ +name: sort-imports +version: 1.3.0 +synopsis: Sort Haskell import statements +description: Haskell source code formatter that sorts import statements +homepage: https://github.com/evanrelf/sort-imports +license: ISC +author: Evan Relf +maintainer: Evan Relf +copyright: 2019 Evan Relf +category: Development +extra-source-files: + - CHANGELOG.md + - LICENSE + - README.md + +dependencies: + - base >= 4.7 && < 5 + +ghc-options: + - -Wall + - -Werror + - -Wincomplete-patterns + +library: + source-dirs: src + dependencies: + - megaparsec + +executables: + sort-imports: + source-dirs: app + main: Main.hs + dependencies: + - optparse-applicative + - sort-imports + +tests: + test: + source-dirs: test + main: Spec.hs + dependencies: + - base + - sort-imports + - tasty + - tasty-hunit + ghc-options: + - -rtsopts + - -threaded + - -with-rtsopts=-N diff --git a/sort-imports.cabal b/sort-imports.cabal new file mode 100644 index 0000000..e12f534 --- /dev/null +++ b/sort-imports.cabal @@ -0,0 +1,66 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.31.2. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: ca81c461a1640c117d8e15cf0540286b67186f864d6de5d525def01045f04d05 + +name: sort-imports +version: 1.3.0 +synopsis: Sort Haskell import statements +description: Haskell source code formatter that sorts import statements +category: Development +homepage: https://github.com/evanrelf/sort-imports +author: Evan Relf +maintainer: Evan Relf +copyright: 2019 Evan Relf +license: ISC +license-file: LICENSE +build-type: Simple +extra-source-files: + CHANGELOG.md + LICENSE + README.md + +library + exposed-modules: + SortImports + SortImports.Types + other-modules: + Paths_sort_imports + hs-source-dirs: + src + ghc-options: -Wall -Werror -Wincomplete-patterns + build-depends: + base >=4.7 && <5 + , megaparsec + default-language: Haskell2010 + +executable sort-imports + main-is: Main.hs + other-modules: + Paths_sort_imports + hs-source-dirs: + app + ghc-options: -Wall -Werror -Wincomplete-patterns + build-depends: + base >=4.7 && <5 + , optparse-applicative + , sort-imports + default-language: Haskell2010 + +test-suite test + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Paths_sort_imports + hs-source-dirs: + test + ghc-options: -Wall -Werror -Wincomplete-patterns -rtsopts -threaded -with-rtsopts=-N + build-depends: + base + , sort-imports + , tasty + , tasty-hunit + default-language: Haskell2010 diff --git a/src/SortImports.hs b/src/SortImports.hs new file mode 100644 index 0000000..35db7ac --- /dev/null +++ b/src/SortImports.hs @@ -0,0 +1,135 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RecordWildCards #-} + +module SortImports where + +import Control.Monad (void) +import Data.List (groupBy, sort) +import Data.Maybe (isJust) + +import Text.Megaparsec +import Text.Megaparsec.Char + +import SortImports.Types + ( Constructors(..) + , Export(..) + , Exposure(..) + , LineType(..) + , Module(..) + , Parser + , View(..) + ) + +rword :: String -> Parser String +rword s = string s <* space1 + +identifierSuffix :: Parser String +identifierSuffix = many $ alphaNumChar <|> oneOf "_'" + +lowerIdentifier :: Parser String +lowerIdentifier = (:) <$> (lowerChar <|> char '_') <*> identifierSuffix + +upperIdentifier :: Parser String +upperIdentifier = (:) <$> upperChar <*> identifierSuffix + +moduleSuffix :: Parser String +moduleSuffix = (:) <$> char '.' <*> upperIdentifier + +moduleName :: Parser String +moduleName = concat <$> ((:) <$> upperIdentifier <*> many moduleSuffix <* space) + +alias :: Parser String +alias = rword "as" *> upperIdentifier <* space + +operatorChar :: Parser Char +operatorChar = oneOf "!#$%&*+-./:<=>?@\\^|~" + +constructors :: Parser Constructors +constructors = do + void $ char '(' + cs <- [] <$ string ".." <|> ((upperIdentifier <|> operator') `sepBy1` (space *> char ',' *> space)) + void $ char ')' + pure $ if null cs then All else Explicit cs + +operator' :: Parser String +operator' = do + void $ char '(' + n <- some operatorChar + void $ char ')' + pure $ "(" <> n <> ")" + +type' :: Parser Export +type' = do + typeName <- upperIdentifier <|> operator' + void $ skipMany (char ' ') + cs <- optional constructors + pure $ Type typeName cs + +operator :: Parser Export +operator = do + void $ char '(' + s <- some operatorChar + void $ char ')' + pure $ Operator s + +function :: Parser Export +function = Function <$> lowerIdentifier + +export :: Parser Export +export = function <|> type' <|> operator + +exports :: Parser [Export] +exports = do + void $ char '(' + space + es <- (space *> export <* space) `sepBy` char ',' + space + void $ char ')' + pure es + +exposure :: Parser (Exposure [Export]) +exposure = do + hiding <- isJust <$> optional (rword "hiding") + (if hiding then Hidden else Exposed) <$> exports + +module' :: Parser Module +module' = do + void $ rword "import" + _qualified <- isJust <$> optional (rword "qualified") + _name <- moduleName + _alias <- optional alias + _exports <- optional exposure + space + pure Module {..} + +sortExports :: Module -> Module +sortExports m = m { _exports = fmap sort <$> _exports m } + +lineType :: String -> LineType +lineType x = + case parse module' "" x of + Left _ -> CodeLine x + Right m -> ModuleLine m + +groupLines :: [LineType] -> [[LineType]] +groupLines = groupBy f + where f (CodeLine _) (CodeLine _) = True + f (ModuleLine _) (ModuleLine _) = True + f _ _ = False + +sortIfModules :: [LineType] -> [LineType] +sortIfModules [] = [] +sortIfModules xs@(CodeLine _:_) = xs +sortIfModules xs@(ModuleLine _:_) = sort . fmap f $ xs + where f (ModuleLine x) = ModuleLine $ sortExports x + f x = x + +sortImports :: String -> String +sortImports + = unlines + . fmap (\case CodeLine x -> x + ModuleLine x -> view x) + . concatMap sortIfModules + . groupLines + . map lineType + . lines diff --git a/src/SortImports/Types.hs b/src/SortImports/Types.hs new file mode 100644 index 0000000..fe391b0 --- /dev/null +++ b/src/SortImports/Types.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE FlexibleInstances #-} + +module SortImports.Types where + +import Data.Void (Void) +import Text.Megaparsec +import Data.List (intercalate) + +class View a where + view :: a -> String + +type Parser = Parsec Void String + +data Constructors + = All + | Explicit [String] + deriving (Eq, Ord, Show) + +data Export + = Type String (Maybe Constructors) + | Function String + | Operator String + deriving (Eq, Ord, Show) + +data Exposure a + = Exposed a + | Hidden a + deriving (Eq, Functor, Show) + +data Module = Module + { _qualified :: Bool + , _name :: String + , _alias :: Maybe String + , _exports :: Maybe (Exposure [Export]) + } deriving (Eq, Show) + +data LineType + = CodeLine String + | ModuleLine Module + deriving (Ord, Eq, Show) + +instance Ord Module where + (Module _ lhs _ _) `compare` (Module _ rhs _ _) = lhs `compare` rhs + +instance View Constructors where + view All = ".." + view (Explicit xs) = intercalate ", " xs + +instance View Export where + view (Type s mc) = + case mc of + Nothing -> s + Just cs -> s <> "(" <> view cs <> ")" + view (Function s) = s + view (Operator s) = "(" <> s <> ")" + +instance View (Exposure [Export]) where + view (Hidden xs) = "hiding " <> view (Exposed xs) + view (Exposed xs) = "(" <> (intercalate ", " . fmap view $ xs) <> ")" + +instance View Module where + view (Module qualified name alias exports) = + unwords . filter (/= "") $ + [ "import" + , if qualified then "qualified" else "" + , name + , maybe "" ("as " <>) alias + , maybe "" view exports + ] diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..d0a6794 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,64 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# resolver: ./custom-snapshot.yaml +# resolver: https://example.com/snapshots/2018-01-01.yaml +resolver: lts-13.19 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the resolver +# using the same syntax as the packages field. +# (e.g., acme-missiles-0.3) +# extra-deps: [] + +# Override default flag values for local packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.9" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor diff --git a/stack.yaml.lock b/stack.yaml.lock new file mode 100644 index 0000000..eed3ce0 --- /dev/null +++ b/stack.yaml.lock @@ -0,0 +1,12 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/lock_files + +packages: [] +snapshots: +- completed: + size: 498155 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/13/19.yaml + sha256: b9367a80d4393d02e58a46b8a9fdfbd7bc19f59c0c2bbf90034ba15cf52cf213 + original: lts-13.19 diff --git a/test/Spec.hs b/test/Spec.hs new file mode 100644 index 0000000..9318416 --- /dev/null +++ b/test/Spec.hs @@ -0,0 +1,17 @@ +import SortImports +import SortImports.Types +import Test.Tasty +import Test.Tasty.HUnit + +main :: IO () +main = defaultMain $ testGroup "Tests" [unitTests] + +unitTests :: TestTree +unitTests = testGroup "Unit tests" + [ testCase "" $ + lineType "not a module" @?= CodeLine "not a module" + , testCase "" $ + lineType "import ModuleName" @?= ModuleLine (Module False "ModuleName" Nothing Nothing) + , testCase "" $ + lineType "import ModuleName" @?= ModuleLine (Module False "ModuleName" Nothing Nothing) + ]