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

View File

@ -1,4 +1,9 @@
- name: Rails
aliases:
- from: "%s?"
to: "be_%s"
- from: "has_%s?"
to: "have_%s"
allowedTerms:
# serialization
- 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
import Data.Bifunctor (second)
import Control.Arrow ((&&&))
import qualified Data.Map.Strict as Map
import Unused.Util (groupBy)
import Data.List (intercalate, sort, nub)
import Unused.TermSearch (SearchResults, fromResults)
import Unused.Types (TermMatchSet, resultsFromMatches, tmTerm)
import Unused.Types (TermMatchSet, TermMatch, resultsFromMatches, tmTerm)
import Unused.LikelihoodCalculator
import Unused.ResultsClassifier.Types
import Unused.Aliases
parseResults :: [LanguageConfiguration] -> SearchResults -> TermMatchSet
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
( LanguageConfiguration(..)
, LowLikelihoodMatch(..)
, TermAlias(..)
, Position(..)
, Matcher(..)
, loadConfig

View File

@ -4,6 +4,7 @@
module Unused.ResultsClassifier.Types
( LanguageConfiguration(..)
, LowLikelihoodMatch(..)
, TermAlias(..)
, Position(..)
, Matcher(..)
) where
@ -20,6 +21,7 @@ data LanguageConfiguration = LanguageConfiguration
{ lcName :: String
, lcAllowedTerms :: [String]
, lcAutoLowLikelihood :: [LowLikelihoodMatch]
, lcTermAliases :: [TermAlias]
} deriving Show
data LowLikelihoodMatch = LowLikelihoodMatch
@ -28,6 +30,11 @@ data LowLikelihoodMatch = LowLikelihoodMatch
, smClassOrModule :: Bool
} deriving Show
data TermAlias = TermAlias
{ taFrom :: String
, taTo :: String
} deriving Show
data Position = StartsWith | EndsWith | Equals 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 .: "allowedTerms"
<*> o .: "autoLowLikelihood"
<*> o .:? "aliases" .!= []
parseJSON _ = mzero
instance FromJSON LowLikelihoodMatch where
@ -45,6 +53,12 @@ instance FromJSON LowLikelihoodMatch where
<*> o .:? "classOrModule" .!= False
parseJSON _ = mzero
instance FromJSON TermAlias where
parseJSON (Y.Object o) = TermAlias
<$> o .: "from"
<*> o .: "to"
parseJSON _ = mzero
data MatchHandler a = MatchHandler
{ mhKeys :: [String]
, 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 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
let searchResults = r1Matches ++ r2Matches
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`
Map.fromList [ ("method_name", r1Results), ("other", r2Results) ]
Map.fromList [ ("admin?|be_admin", results) ]
it "handles empty input" $ do
(Right config) <- loadConfig

View File

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