From 43edf288e251a060d7a5835086b2c3d8e63fe41a Mon Sep 17 00:00:00 2001 From: Joshua Clayton Date: Sat, 21 May 2016 10:20:25 -0400 Subject: [PATCH] Attempt to find and load tags automatically Why? ==== Frequency of a tool's usage is determined by how easy it is to use the tool. By having to pipe in ctags files all the time, and not provide any guidance to the user, this program is merely a toy, since it's hard to get right, and harder to explore. This modifies the default behavior to look for a ctags file in a few common locations, and lets the user choose a custom location if she so chooses. Resolves #35 --- README.md | 22 ++++++++---- app/Main.hs | 43 +++++++++++++++-------- src/Unused/CLI.hs | 1 + src/Unused/CLI/MissingTagsFileError.hs | 48 ++++++++++++++++++++++++++ src/Unused/TagsSource.hs | 41 ++++++++++++++++++++++ unused.cabal | 2 ++ 6 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 src/Unused/CLI/MissingTagsFileError.hs create mode 100644 src/Unused/TagsSource.hs 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