From 7fe32edc4d46ad5ff6639e01292c65fbf29390ce Mon Sep 17 00:00:00 2001 From: Joshua Clayton Date: Sat, 16 Jul 2016 07:45:21 -0400 Subject: [PATCH] Support projections-style transformations to reduce false-positives Basic aliases (e.g. `admin?`/`be_admin`) can be represented easily with simple wildcards, but more complex transformations require a different mechanism. Instead of using `%s` to represent strings that can be replaced 1:1, this introduces a syntax inspired by https://github.com/tpope/vim-projectionist, as such: - name: Rails aliases: - from: "*Validator" to: "{snakecase}" This would find `AbsoluteUriValidator` and also match `absolute_uri`, which would be found if the validation was in use. This currently supports the `camelcase` and `snakecase` transformations, as well as no transformation. Closes #18 --- data/config.yml | 10 +++--- src/Unused/Aliases.hs | 6 ++-- src/Unused/Projection.hs | 52 +++++++++++++++++++++++++++ src/Unused/Projection/Transform.hs | 37 +++++++++++++++++++ src/Unused/ResultsClassifier/Types.hs | 4 +++ test/Unused/AliasesSpec.hs | 7 ++-- test/Unused/ProjectionSpec.hs | 30 ++++++++++++++++ unused.cabal | 5 +++ 8 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 src/Unused/Projection.hs create mode 100644 src/Unused/Projection/Transform.hs create mode 100644 test/Unused/ProjectionSpec.hs diff --git a/data/config.yml b/data/config.yml index 1a875de..3773d2a 100644 --- a/data/config.yml +++ b/data/config.yml @@ -1,9 +1,11 @@ - name: Rails aliases: - - from: "%s?" - to: "be_%s" - - from: "has_%s?" - to: "have_%s" + - from: "*?" + to: "be_{}" + - from: "has_*?" + to: "have_{}" + - from: "*Validator" + to: "{snakecase}" allowedTerms: # serialization - as_json diff --git a/src/Unused/Aliases.hs b/src/Unused/Aliases.hs index 01ed8b0..c616726 100644 --- a/src/Unused/Aliases.hs +++ b/src/Unused/Aliases.hs @@ -20,13 +20,13 @@ termsAndAliases [] = map OriginalTerm termsAndAliases as = L.nub . concatMap ((as >>=) . generateSearchTerms . T.pack) generateSearchTerms :: Text -> TermAlias -> [SearchTerm] -generateSearchTerms term TermAlias{taFrom = from, taTo = to} = +generateSearchTerms term TermAlias{taFrom = from, taTransform = transform} = toTermWithAlias $ parsePatternForMatch (T.pack from) term where toTermWithAlias (Right (Just match)) = [OriginalTerm unpackedTerm, AliasTerm unpackedTerm (aliasedResult match)] toTermWithAlias _ = [OriginalTerm unpackedTerm] unpackedTerm = T.unpack term - aliasedResult match = T.unpack $ T.replace wildcard match (T.pack to) + aliasedResult = T.unpack . transform parsePatternForMatch :: Text -> Text -> Either Text (Maybe Text) parsePatternForMatch aliasPattern term = @@ -36,4 +36,4 @@ parsePatternForMatch aliasPattern term = findMatch _ = Left $ T.pack $ "There was a problem with the pattern: " ++ show aliasPattern wildcard :: Text -wildcard = "%s" +wildcard = "*" diff --git a/src/Unused/Projection.hs b/src/Unused/Projection.hs new file mode 100644 index 0000000..272f9ef --- /dev/null +++ b/src/Unused/Projection.hs @@ -0,0 +1,52 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Unused.Projection where + +import Data.Monoid ((<>)) +import Data.Text (Text) +import qualified Data.Text as T +import Text.Megaparsec +import Text.Megaparsec.Text +import Unused.Projection.Transform + +data ParsedTransform = ParsedTransform + { ptPre :: Text + , ptTransforms :: [Transform] + , ptPost :: Text + } + +translate :: Text -> Either ParseError (Text -> Text) +translate template = applyTransform <$> parseTransform template + +applyTransform :: ParsedTransform -> Text -> Text +applyTransform pt t = + ptPre pt + <> runTransformations t (ptTransforms pt) + <> ptPost pt + +parseTransform :: Text -> Either ParseError ParsedTransform +parseTransform = parse parsedTransformParser "" + +parsedTransformParser :: Parser ParsedTransform +parsedTransformParser = + ParsedTransform + <$> preTransformsParser + <*> transformsParser + <*> postTransformsParser + +preTransformsParser :: Parser Text +preTransformsParser = T.pack <$> manyTill anyChar (char '{') + +transformsParser :: Parser [Transform] +transformsParser = transformParser `sepBy` char '|' <* char '}' + +postTransformsParser :: Parser Text +postTransformsParser = T.pack <$> many anyChar + +transformParser :: Parser Transform +transformParser = do + result <- string "camelcase" <|> string "snakecase" + return $ case result of + "camelcase" -> Camelcase + "snakecase" -> Snakecase + _ -> Noop diff --git a/src/Unused/Projection/Transform.hs b/src/Unused/Projection/Transform.hs new file mode 100644 index 0000000..61dea8b --- /dev/null +++ b/src/Unused/Projection/Transform.hs @@ -0,0 +1,37 @@ +module Unused.Projection.Transform + ( Transform(..) + , runTransformations + ) where + +import Data.Either (rights) +import Data.Text (Text) +import qualified Data.Text as T +import qualified Text.Inflections as I +import qualified Text.Inflections.Parse.Types as I +import qualified Unused.Util as U + +data Transform + = Camelcase + | Snakecase + | Noop + +runTransformations :: Text -> [Transform] -> Text +runTransformations = foldl (flip runTransformation) + +runTransformation :: Transform -> Text -> Text +runTransformation Camelcase = toCamelcase +runTransformation Snakecase = toSnakecase +runTransformation Noop = id + +toCamelcase :: Text -> Text +toCamelcase t = maybe t (T.pack . I.camelize) $ toMaybeWords t + +toSnakecase :: Text -> Text +toSnakecase t = maybe t (T.pack . I.underscore) $ toMaybeWords t + +toMaybeWords :: Text -> Maybe [I.Word] +toMaybeWords t = + U.safeHead $ rights [asCamel, asSnake] + where + asCamel = I.parseCamelCase [] $ T.unpack t + asSnake = I.parseSnakeCase [] $ T.unpack t diff --git a/src/Unused/ResultsClassifier/Types.hs b/src/Unused/ResultsClassifier/Types.hs index 6c2e2a7..38a73c6 100644 --- a/src/Unused/ResultsClassifier/Types.hs +++ b/src/Unused/ResultsClassifier/Types.hs @@ -15,8 +15,10 @@ import qualified Control.Monad as M import qualified Data.HashMap.Strict as HM import qualified Data.List as L import qualified Data.Text as T +import Data.Text (Text) import Data.Yaml (FromJSON(..), (.:), (.:?), (.!=)) import qualified Data.Yaml as Y +import Unused.Projection data LanguageConfiguration = LanguageConfiguration { lcName :: String @@ -34,6 +36,7 @@ data LowLikelihoodMatch = LowLikelihoodMatch data TermAlias = TermAlias { taFrom :: String , taTo :: String + , taTransform :: Text -> Text } data ParseConfigError = ParseConfigError @@ -63,6 +66,7 @@ instance FromJSON TermAlias where parseJSON (Y.Object o) = TermAlias <$> o .: "from" <*> o .: "to" + <*> (either (fail . show) return =<< (translate . T.pack <$> (o .: "to"))) parseJSON _ = M.mzero data MatchHandler a = MatchHandler diff --git a/test/Unused/AliasesSpec.hs b/test/Unused/AliasesSpec.hs index 658f9da..9006dfa 100644 --- a/test/Unused/AliasesSpec.hs +++ b/test/Unused/AliasesSpec.hs @@ -1,5 +1,8 @@ +{-# LANGUAGE OverloadedStrings #-} + module Unused.AliasesSpec where +import Data.Monoid ((<>)) import Test.Hspec import Unused.Aliases import Unused.ResultsClassifier.Types (TermAlias(..)) @@ -15,8 +18,8 @@ spec = parallel $ termsAndAliases [] ["method_1", "method_2"] `shouldBe` [OriginalTerm "method_1", OriginalTerm "method_2"] it "adds aliases to the list of terms" $ do - let predicateAlias = TermAlias "%s?" "be_%s" - let pluralizeAlias = TermAlias "really_%s" "very_%s" + let predicateAlias = TermAlias "*?" "be_{}" ("be_" <>) + let pluralizeAlias = TermAlias "really_*" "very_{}" ("very_" <>) termsAndAliases [predicateAlias, pluralizeAlias] ["awesome?", "really_cool"] `shouldBe` [OriginalTerm "awesome?", AliasTerm "awesome?" "be_awesome", OriginalTerm "really_cool", AliasTerm "really_cool" "very_cool"] diff --git a/test/Unused/ProjectionSpec.hs b/test/Unused/ProjectionSpec.hs new file mode 100644 index 0000000..f415994 --- /dev/null +++ b/test/Unused/ProjectionSpec.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Unused.ProjectionSpec where + +import Data.Text (Text) +import Test.Hspec +import Unused.Projection + +main :: IO () +main = hspec spec + +spec :: Spec +spec = parallel $ + describe "translate" $ do + it "replaces the text without transforms" $ + translate' "foo_{}" "bar" `shouldBe` "foo_bar" + + it "handles text transformations" $ do + translate' "{camelcase}Validator" "proper_email" `shouldBe` "ProperEmailValidator" + translate' "{snakecase}" "ProperEmail" `shouldBe` "proper_email" + translate' "{camelcase}Validator" "AlreadyCamelcase" `shouldBe` "AlreadyCamelcaseValidator" + + it "handles unknown transformations" $ + translate' "{unknown}Validator" "proper_email" `shouldBe` "proper_email" + +translate' :: Text -> Text -> Text +translate' template v = + case translate template of + Right f -> f v + Left _ -> v diff --git a/unused.cabal b/unused.cabal index 4117335..a471a37 100644 --- a/unused.cabal +++ b/unused.cabal @@ -25,6 +25,8 @@ library , Unused.Util , Unused.Regex , Unused.Aliases + , Unused.Projection + , Unused.Projection.Transform , Unused.ResponseFilter , Unused.ResultsClassifier , Unused.ResultsClassifier.Types @@ -77,6 +79,8 @@ library , vector , mtl , transformers + , megaparsec + , inflections ghc-options: -Wall default-language: Haskell2010 @@ -100,6 +104,7 @@ test-suite unused-test , unused , hspec , containers + , text other-modules: Unused.ParserSpec , Unused.ResponseFilterSpec , Unused.TypesSpec