From 6ffb098b203a3807dc79e0776062edb08819829e Mon Sep 17 00:00:00 2001 From: Joshua Clayton Date: Sun, 29 May 2016 07:49:22 -0400 Subject: [PATCH] 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. --- app/Main.hs | 14 ++++-- data/config.yml | 5 ++ src/Unused/Aliases.hs | 68 +++++++++++++++++++++++++++ src/Unused/Parser.hs | 18 +++++-- src/Unused/ResultsClassifier.hs | 1 + src/Unused/ResultsClassifier/Types.hs | 14 ++++++ test/Unused/ParserSpec.hs | 18 ++++++- unused.cabal | 1 + 8 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 src/Unused/Aliases.hs diff --git a/app/Main.hs b/app/Main.hs index b967e74..d3950dd 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -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 diff --git a/data/config.yml b/data/config.yml index 5435dfa..1a875de 100644 --- a/data/config.yml +++ b/data/config.yml @@ -1,4 +1,9 @@ - name: Rails + aliases: + - from: "%s?" + to: "be_%s" + - from: "has_%s?" + to: "have_%s" allowedTerms: # serialization - as_json diff --git a/src/Unused/Aliases.hs b/src/Unused/Aliases.hs new file mode 100644 index 0000000..257ce9e --- /dev/null +++ b/src/Unused/Aliases.hs @@ -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" diff --git a/src/Unused/Parser.hs b/src/Unused/Parser.hs index 8581869..0d22d26 100644 --- a/src/Unused/Parser.hs +++ b/src/Unused/Parser.hs @@ -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 diff --git a/src/Unused/ResultsClassifier.hs b/src/Unused/ResultsClassifier.hs index 9dab4fc..ddef4ee 100644 --- a/src/Unused/ResultsClassifier.hs +++ b/src/Unused/ResultsClassifier.hs @@ -1,6 +1,7 @@ module Unused.ResultsClassifier ( LanguageConfiguration(..) , LowLikelihoodMatch(..) + , TermAlias(..) , Position(..) , Matcher(..) , loadConfig diff --git a/src/Unused/ResultsClassifier/Types.hs b/src/Unused/ResultsClassifier/Types.hs index 17d759a..0ccc26b 100644 --- a/src/Unused/ResultsClassifier/Types.hs +++ b/src/Unused/ResultsClassifier/Types.hs @@ -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) diff --git a/test/Unused/ParserSpec.hs b/test/Unused/ParserSpec.hs index f9f8941..a61411c 100644 --- a/test/Unused/ParserSpec.hs +++ b/test/Unused/ParserSpec.hs @@ -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 diff --git a/unused.cabal b/unused.cabal index 2988e6d..e87d228 100644 --- a/unused.cabal +++ b/unused.cabal @@ -23,6 +23,7 @@ library , Unused.Types , Unused.Util , Unused.Regex + , Unused.Aliases , Unused.ResponseFilter , Unused.ResultsClassifier , Unused.ResultsClassifier.Types