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
This commit is contained in:
Joshua Clayton 2016-05-21 10:20:25 -04:00
parent c5f2a38e80
commit 43edf288e2
6 changed files with 137 additions and 20 deletions

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

41
src/Unused/TagsSource.hs Normal file
View File

@ -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", "."]

View File

@ -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