Initial support of aliases based on wildcard matching

Why?
====

Dynamic languages, and Rails in particular, support some fun method
creation. One common pattern is, within RSpec, to create matchers
dynamically based on predicate methods. Two common examples are:

* `#admin?` gets converted to the matcher `#be_admin`
* `#has_active_todos?` gets converted to the matcher `#have_active_todos`

This especially comes into play when writing page objects with predicate
methods.

This change introduces the concept of aliases, a way to describe the
before/after for these transformations. This introduces a direct swap
with a wildcard value (%s), although this may change in the future to
support other transformations for pluralization, camel-casing, etc.

Externally, aliases are not grouped together by term; however, the
underlying counts are summed together, increasing the total occurrences
and likely pushing the individual method out of "high" likelihood into
"medium" or "low" likelihood.

Closes #19.
This commit is contained in:
Joshua Clayton 2016-05-29 07:49:22 -04:00
parent 0dcb06fe70
commit 6ffb098b20
8 changed files with 132 additions and 7 deletions

View File

@ -12,6 +12,7 @@ import Unused.Grouping (CurrentGrouping(..), groupedResponses)
import Unused.CLI (SearchRunner(..), withoutCursor, renderHeader, executeSearch, resetScreen, withInterruptHandler) import Unused.CLI (SearchRunner(..), withoutCursor, renderHeader, executeSearch, resetScreen, withInterruptHandler)
import qualified Unused.CLI.Views as V import qualified Unused.CLI.Views as V
import Unused.Cache import Unused.Cache
import Unused.Aliases (termsAndAliases)
import Unused.TagsSource import Unused.TagsSource
data Options = Options data Options = Options
@ -44,11 +45,12 @@ run options = withoutCursor $ do
case terms' of case terms' of
(Left e) -> V.missingTagsFileError e (Left e) -> V.missingTagsFileError e
(Right terms) -> do (Right terms'') -> do
renderHeader terms
languageConfig <- loadLanguageConfig languageConfig <- loadLanguageConfig
let terms = termsWithAlternatesFromConfig languageConfig terms''
renderHeader terms
results <- withCache options $ executeSearch (oSearchRunner options) terms results <- withCache options $ executeSearch (oSearchRunner options) terms
let response = parseResults languageConfig results let response = parseResults languageConfig results
@ -59,6 +61,12 @@ run options = withoutCursor $ do
return () return ()
termsWithAlternatesFromConfig :: [LanguageConfiguration] -> [String] -> [String]
termsWithAlternatesFromConfig lcs =
termsAndAliases aliases
where
aliases = concatMap lcTermAliases lcs
printResults :: Options -> TermMatchSet -> IO () printResults :: Options -> TermMatchSet -> IO ()
printResults options = V.searchResults . groupedResponses (oGrouping options) . optionFilters options printResults options = V.searchResults . groupedResponses (oGrouping options) . optionFilters options

View File

@ -1,4 +1,9 @@
- name: Rails - name: Rails
aliases:
- from: "%s?"
to: "be_%s"
- from: "has_%s?"
to: "have_%s"
allowedTerms: allowedTerms:
# serialization # serialization
- as_json - as_json

68
src/Unused/Aliases.hs Normal file
View File

@ -0,0 +1,68 @@
{-# LANGUAGE OverloadedStrings #-}
module Unused.Aliases
( groupedTermsAndAliases
, termsAndAliases
) where
import Data.Tuple (swap)
import Data.List (nub, sort, find, (\\))
import Data.Text (Text)
import qualified Data.Text as T
import Unused.ResultsClassifier.Types
import Unused.Types (TermMatch, tmTerm)
import Unused.Util (groupBy)
type Alias = (Text, Text)
type GroupedResult = (String, [TermMatch])
groupedTermsAndAliases :: [TermAlias] -> [TermMatch] -> [[TermMatch]]
groupedTermsAndAliases as ms =
map snd $ foldl (processResultsWithAliases aliases) [] matchesGroupedByTerm
where
matchesGroupedByTerm = groupBy tmTerm ms
aliases = map toAlias as
termsAndAliases :: [TermAlias] -> [String] -> [String]
termsAndAliases as =
nub . map T.unpack . concatMap (allAliases aliases . T.pack)
where
aliases = map toAlias as
allAliases :: [Alias] -> Text -> [Text]
allAliases as' term = concatMap (`generateAliases` term) as'
processResultsWithAliases :: [Alias] -> [GroupedResult] -> GroupedResult -> [GroupedResult]
processResultsWithAliases as acc result@(term, matches) =
if noAliasesExist
then acc ++ [result]
else case closestAlias of
Nothing -> acc ++ [result]
Just alias@(aliasTerm, aliasMatches) -> (acc \\ [alias]) ++ [(aliasTerm, aliasMatches ++ matches)]
where
packedTerm = T.pack term
noAliasesExist = null listOfAliases
listOfAliases = nub (concatMap (`aliasesForTerm` packedTerm) as) \\ [packedTerm]
closestAlias = find ((`elem` listOfAliases) . T.pack . fst) acc
toAlias :: TermAlias -> Alias
toAlias TermAlias{taFrom = from, taTo = to} = (T.pack from, T.pack to)
generateAliases :: Alias -> Text -> [Text]
generateAliases (from, to) term =
toTermWithAlias $ parsePatternForMatch from term
where
toTermWithAlias (Right (Just match)) = [term, T.replace wildcard match to]
toTermWithAlias _ = [term]
parsePatternForMatch :: Text -> Text -> Either Text (Maybe Text)
parsePatternForMatch aliasPattern term =
findMatch $ T.splitOn wildcard aliasPattern
where
findMatch [prefix, suffix] = Right $ T.stripSuffix suffix =<< T.stripPrefix prefix term
findMatch _ = Left $ T.pack $ "There was a problem with the pattern: " ++ show aliasPattern
aliasesForTerm :: Alias -> Text -> [Text]
aliasesForTerm a t = nub $ sort $ generateAliases a t ++ generateAliases (swap a) t
wildcard :: Text
wildcard = "%s"

View File

@ -3,12 +3,24 @@ module Unused.Parser
) where ) where
import Data.Bifunctor (second) import Data.Bifunctor (second)
import Control.Arrow ((&&&))
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Unused.Util (groupBy) import Data.List (intercalate, sort, nub)
import Unused.TermSearch (SearchResults, fromResults) import Unused.TermSearch (SearchResults, fromResults)
import Unused.Types (TermMatchSet, resultsFromMatches, tmTerm) import Unused.Types (TermMatchSet, TermMatch, resultsFromMatches, tmTerm)
import Unused.LikelihoodCalculator import Unused.LikelihoodCalculator
import Unused.ResultsClassifier.Types
import Unused.Aliases
parseResults :: [LanguageConfiguration] -> SearchResults -> TermMatchSet parseResults :: [LanguageConfiguration] -> SearchResults -> TermMatchSet
parseResults lcs = parseResults lcs =
Map.fromList . map (second $ calculateLikelihood lcs . resultsFromMatches) . groupBy tmTerm . fromResults Map.fromList . map (second $ calculateLikelihood lcs . resultsFromMatches) . groupResults aliases . fromResults
where
aliases = concatMap lcTermAliases lcs
groupResults :: [TermAlias] -> [TermMatch] -> [(String, [TermMatch])]
groupResults aliases ms =
map (toKey &&& id) groupedMatches
where
toKey = intercalate "|" . nub . sort . map tmTerm
groupedMatches = groupedTermsAndAliases aliases ms

View File

@ -1,6 +1,7 @@
module Unused.ResultsClassifier module Unused.ResultsClassifier
( LanguageConfiguration(..) ( LanguageConfiguration(..)
, LowLikelihoodMatch(..) , LowLikelihoodMatch(..)
, TermAlias(..)
, Position(..) , Position(..)
, Matcher(..) , Matcher(..)
, loadConfig , loadConfig

View File

@ -4,6 +4,7 @@
module Unused.ResultsClassifier.Types module Unused.ResultsClassifier.Types
( LanguageConfiguration(..) ( LanguageConfiguration(..)
, LowLikelihoodMatch(..) , LowLikelihoodMatch(..)
, TermAlias(..)
, Position(..) , Position(..)
, Matcher(..) , Matcher(..)
) where ) where
@ -20,6 +21,7 @@ data LanguageConfiguration = LanguageConfiguration
{ lcName :: String { lcName :: String
, lcAllowedTerms :: [String] , lcAllowedTerms :: [String]
, lcAutoLowLikelihood :: [LowLikelihoodMatch] , lcAutoLowLikelihood :: [LowLikelihoodMatch]
, lcTermAliases :: [TermAlias]
} deriving Show } deriving Show
data LowLikelihoodMatch = LowLikelihoodMatch data LowLikelihoodMatch = LowLikelihoodMatch
@ -28,6 +30,11 @@ data LowLikelihoodMatch = LowLikelihoodMatch
, smClassOrModule :: Bool , smClassOrModule :: Bool
} deriving Show } deriving Show
data TermAlias = TermAlias
{ taFrom :: String
, taTo :: String
} deriving Show
data Position = StartsWith | EndsWith | Equals deriving Show data Position = StartsWith | EndsWith | Equals deriving Show
data Matcher = Term Position String | Path Position String | AppOccurrences Int | AllowedTerms [String] deriving Show data Matcher = Term Position String | Path Position String | AppOccurrences Int | AllowedTerms [String] deriving Show
@ -36,6 +43,7 @@ instance FromJSON LanguageConfiguration where
<$> o .: "name" <$> o .: "name"
<*> o .: "allowedTerms" <*> o .: "allowedTerms"
<*> o .: "autoLowLikelihood" <*> o .: "autoLowLikelihood"
<*> o .:? "aliases" .!= []
parseJSON _ = mzero parseJSON _ = mzero
instance FromJSON LowLikelihoodMatch where instance FromJSON LowLikelihoodMatch where
@ -45,6 +53,12 @@ instance FromJSON LowLikelihoodMatch where
<*> o .:? "classOrModule" .!= False <*> o .:? "classOrModule" .!= False
parseJSON _ = mzero parseJSON _ = mzero
instance FromJSON TermAlias where
parseJSON (Y.Object o) = TermAlias
<$> o .: "from"
<*> o .: "to"
parseJSON _ = mzero
data MatchHandler a = MatchHandler data MatchHandler a = MatchHandler
{ mhKeys :: [String] { mhKeys :: [String]
, mhKeyToMatcher :: T.Text -> Either T.Text (a -> Matcher) , mhKeyToMatcher :: T.Text -> Either T.Text (a -> Matcher)

View File

@ -23,13 +23,29 @@ spec = parallel $
let r2Matches = [ TermMatch "other" "app/path/other.rb" 1 ] let r2Matches = [ TermMatch "other" "app/path/other.rb" 1 ]
let r2Results = TermResults "other" r2Matches (Occurrences 0 0) (Occurrences 1 1) (Occurrences 1 1) (Removal High "used once") let r2Results = TermResults "other" r2Matches (Occurrences 0 0) (Occurrences 1 1) (Occurrences 1 1) (Removal High "used once")
(Right config) <- loadConfig
let result = parseResults config $ SearchResults $ r1Matches ++ r2Matches
result `shouldBe`
Map.fromList [ ("method_name", r1Results), ("other", r2Results) ]
it "handles aliases correctly" $ do
let r1Matches = [ TermMatch "admin?" "app/path/user.rb" 3 ]
let r2Matches = [ TermMatch "be_admin" "spec/models/user_spec.rb" 2
, TermMatch "be_admin" "spec/features/user_promoted_to_admin_spec.rb" 2
]
(Right config) <- loadConfig (Right config) <- loadConfig
let searchResults = r1Matches ++ r2Matches let searchResults = r1Matches ++ r2Matches
let result = parseResults config $ SearchResults searchResults let result = parseResults config $ SearchResults searchResults
let results = TermResults "admin?" searchResults (Occurrences 2 4) (Occurrences 1 3) (Occurrences 3 7) (Removal Low "used frequently")
result `shouldBe` result `shouldBe`
Map.fromList [ ("method_name", r1Results), ("other", r2Results) ] Map.fromList [ ("admin?|be_admin", results) ]
it "handles empty input" $ do it "handles empty input" $ do
(Right config) <- loadConfig (Right config) <- loadConfig

View File

@ -23,6 +23,7 @@ library
, Unused.Types , Unused.Types
, Unused.Util , Unused.Util
, Unused.Regex , Unused.Regex
, Unused.Aliases
, Unused.ResponseFilter , Unused.ResponseFilter
, Unused.ResultsClassifier , Unused.ResultsClassifier
, Unused.ResultsClassifier.Types , Unused.ResultsClassifier.Types