diff --git a/README.md b/README.md index cf75acc..e295117 100644 --- a/README.md +++ b/README.md @@ -64,17 +64,27 @@ your `$PATH`. ## Using Unused -`unused` reads from a pipe expecting a series of tokens to search the codebase -for. +`unused` attempts to read from common tags file locations (`.git/tags`, +`tags`, and `tmp/tags`). -The recommended way to do this is to clean up your tags file and pipe it in: +In an application where the tags file exists, run: ```sh -cat .git/tags | cut -f1 | sort -u | unused +unused ``` -My end goal is to have the latter rolled up into unused itself, so you can -navigate to a directory, run `unused`, and everything works as expected. +If you want to specify a custom tags file, or load tokens from somewhere else, +run: + +```sh +cat .custom/tags | unused --stdin +``` + +To view more usage options, run: + +```sh +unused --help +``` ## Requirements diff --git a/app/Main.hs b/app/Main.hs index 4cc457d..3e672a9 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -8,8 +8,9 @@ import Unused.Types (ParseResponse, RemovalLikelihood(..)) import Unused.ResultsClassifier import Unused.ResponseFilter (withOneOccurrence, withLikelihoods, ignoringPaths) import Unused.Grouping (CurrentGrouping(..), groupedResponses) -import Unused.CLI (SearchRunner(..), withoutCursor, renderHeader, executeSearch, printParseError, printSearchResults, resetScreen, withInterruptHandler) +import Unused.CLI (SearchRunner(..), withoutCursor, renderHeader, executeSearch, printParseError, printMissingTagsFileError, printSearchResults, resetScreen, withInterruptHandler) import Unused.Cache +import Unused.TagsSource data Options = Options { oSearchRunner :: SearchRunner @@ -19,6 +20,7 @@ data Options = Options , oIgnoredPaths :: [String] , oGrouping :: CurrentGrouping , oWithCache :: Bool + , oFromStdIn :: Bool } main :: IO () @@ -27,35 +29,42 @@ main = withInterruptHandler $ (withInfo parseOptions pHeader pDescription pFooter) where pHeader = "Unused: Analyze potentially unused code" - pDescription = "Unused allows a developer to pipe in a list of tokens to\ - \ search through in directory to determine likelihood a\ - \ token can be removed. Requires tokens be piped into the\ - \ program seperated by newlines. See CLI USAGE below." - pFooter = "CLI USAGE: $ cat path/to/ctags | cut -f1 | sort -u | unused" + pDescription = "Unused allows a developer to leverage an existing tags file\ + \ (located at .git/tags, tags, or tmp/tags) to identify tokens\ + \ in a codebase that are unused." + pFooter = "CLI USAGE: $ unused" run :: Options -> IO () run options = withoutCursor $ do hSetBuffering stdout NoBuffering - terms <- pure . lines =<< getContents - renderHeader terms + terms' <- calculateTagInput options - languageConfig <- loadLanguageConfig + case terms' of + (Left e) -> printMissingTagsFileError e + (Right terms) -> do + renderHeader terms - results <- withCache options $ unlines <$> executeSearch (oSearchRunner options) terms + languageConfig <- loadLanguageConfig - let response = parseLines languageConfig results + results <- withCache options $ unlines <$> executeSearch (oSearchRunner options) terms - resetScreen + let response = parseLines languageConfig results - either printParseError (printSearchResults . groupedResponses (oGrouping options)) $ - optionFilters options response + resetScreen + + either printParseError (printSearchResults . groupedResponses (oGrouping options)) $ + optionFilters options response return () loadLanguageConfig :: IO [LanguageConfiguration] loadLanguageConfig = either (const []) id <$> loadConfig +calculateTagInput :: Options -> IO (Either TagSearchOutcome [String]) +calculateTagInput Options{ oFromStdIn = True } = loadTagsFromPipe +calculateTagInput Options{ oFromStdIn = False } = loadTagsFromFile + withCache :: Options -> IO String -> IO String withCache Options{ oWithCache = True } = cached withCache Options{ oWithCache = False } = id @@ -88,6 +97,7 @@ parseOptions = <*> parseIgnorePaths <*> parseGroupings <*> parseWithCache + <*> parseFromStdIn parseSearchRunner :: Parser SearchRunner parseSearchRunner = @@ -153,3 +163,8 @@ parseWithCache = switch $ short 'c' <> long "with-cache" <> help "Write to cache and read when available" + +parseFromStdIn :: Parser Bool +parseFromStdIn = switch $ + long "stdin" + <> help "Read tags from STDIN" diff --git a/src/Unused/CLI.hs b/src/Unused/CLI.hs index 4b6587b..5aa2bde 100644 --- a/src/Unused/CLI.hs +++ b/src/Unused/CLI.hs @@ -4,5 +4,6 @@ module Unused.CLI import Unused.CLI.Search as X import Unused.CLI.SearchError as X +import Unused.CLI.MissingTagsFileError as X import Unused.CLI.SearchResult as X import Unused.CLI.Util as X diff --git a/src/Unused/CLI/MissingTagsFileError.hs b/src/Unused/CLI/MissingTagsFileError.hs new file mode 100644 index 0000000..866714d --- /dev/null +++ b/src/Unused/CLI/MissingTagsFileError.hs @@ -0,0 +1,48 @@ +module Unused.CLI.MissingTagsFileError + ( printMissingTagsFileError + ) where + +import Unused.TagsSource +import Unused.CLI.Util + +printMissingTagsFileError :: TagSearchOutcome -> IO () +printMissingTagsFileError e = do + setSGR [SetColor Background Vivid Red] + setSGR [SetColor Foreground Vivid White] + setSGR [SetConsoleIntensity BoldIntensity] + + putStrLn "\nThere was a problem finding a tags file.\n" + + setSGR [Reset] + + printOutcomeMessage e + putStr "\n" + + setSGR [SetConsoleIntensity BoldIntensity] + putStr "If you're generating a ctags file to a custom location, " + putStrLn "you can pipe it into unused:" + setSGR [Reset] + + putStrLn " cat custom/ctags | unused --stdin" + + putStr "\n" + + setSGR [SetConsoleIntensity BoldIntensity] + putStrLn "You can find out more about Exuberant Ctags here:" + setSGR [Reset] + putStrLn " http://ctags.sourceforge.net/" + + putStr "\n" + + setSGR [SetConsoleIntensity BoldIntensity] + putStrLn "You can read about a good git-based Ctags workflow here:" + setSGR [Reset] + putStrLn " http://tbaggery.com/2011/08/08/effortless-ctags-with-git.html" + + putStr "\n" + +printOutcomeMessage :: TagSearchOutcome -> IO () +printOutcomeMessage (TagsFileNotFound directoriesSearched) = do + putStrLn "Looked for a 'tags' file in the following directories:\n" + mapM_ (\d -> putStrLn $ "* " ++ d) directoriesSearched + diff --git a/src/Unused/TagsSource.hs b/src/Unused/TagsSource.hs new file mode 100644 index 0000000..ee38977 --- /dev/null +++ b/src/Unused/TagsSource.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Unused.TagsSource + ( TagSearchOutcome(..) + , loadTagsFromFile + , loadTagsFromPipe + ) where + +import Data.List (nub) +import System.Directory (findFile) +import Unused.Regex (matchRegex) +import qualified Data.Text as T + +data TagSearchOutcome + = TagsFileNotFound [String] + +loadTagsFromPipe :: IO (Either TagSearchOutcome [String]) +loadTagsFromPipe = fmap (Right . tokensFromTags) getContents + +loadTagsFromFile :: IO (Either TagSearchOutcome [String]) +loadTagsFromFile = fmap (fmap tokensFromTags) tagsContent + +tokensFromTags :: String -> [String] +tokensFromTags = + filter tagRemovalRegex . nub . map token . tokenLocations + where + tokenLocations = map (T.splitOn "\t" . T.pack) . lines + token = T.unpack . head + +tagRemovalRegex :: String -> Bool +tagRemovalRegex = not . matchRegex "^!_TAG" + +tagsContent :: IO (Either TagSearchOutcome String) +tagsContent = findFile possibleTagsFileDirectories "tags" >>= eitherReadFile + +eitherReadFile :: Maybe String -> IO (Either TagSearchOutcome String) +eitherReadFile Nothing = return $ Left $ TagsFileNotFound possibleTagsFileDirectories +eitherReadFile (Just path) = Right <$> readFile path + +possibleTagsFileDirectories :: [String] +possibleTagsFileDirectories = [".git", "tmp", "."] diff --git a/unused.cabal b/unused.cabal index 6cd53af..897dd7c 100644 --- a/unused.cabal +++ b/unused.cabal @@ -33,9 +33,11 @@ library , Unused.LikelihoodCalculator , Unused.Cache , Unused.Cache.DirectoryFingerprint + , Unused.TagsSource , Unused.CLI , Unused.CLI.Search , Unused.CLI.SearchError + , Unused.CLI.MissingTagsFileError , Unused.CLI.SearchResult , Unused.CLI.SearchResult.ColumnFormatter , Unused.CLI.Util