mirror of
https://github.com/joshuaclayton/unused.git
synced 2024-09-11 04:45:28 +03:00
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
This commit is contained in:
parent
15cc48b0e4
commit
7fe32edc4d
@ -1,9 +1,11 @@
|
|||||||
- name: Rails
|
- name: Rails
|
||||||
aliases:
|
aliases:
|
||||||
- from: "%s?"
|
- from: "*?"
|
||||||
to: "be_%s"
|
to: "be_{}"
|
||||||
- from: "has_%s?"
|
- from: "has_*?"
|
||||||
to: "have_%s"
|
to: "have_{}"
|
||||||
|
- from: "*Validator"
|
||||||
|
to: "{snakecase}"
|
||||||
allowedTerms:
|
allowedTerms:
|
||||||
# serialization
|
# serialization
|
||||||
- as_json
|
- as_json
|
||||||
|
@ -20,13 +20,13 @@ termsAndAliases [] = map OriginalTerm
|
|||||||
termsAndAliases as = L.nub . concatMap ((as >>=) . generateSearchTerms . T.pack)
|
termsAndAliases as = L.nub . concatMap ((as >>=) . generateSearchTerms . T.pack)
|
||||||
|
|
||||||
generateSearchTerms :: Text -> TermAlias -> [SearchTerm]
|
generateSearchTerms :: Text -> TermAlias -> [SearchTerm]
|
||||||
generateSearchTerms term TermAlias{taFrom = from, taTo = to} =
|
generateSearchTerms term TermAlias{taFrom = from, taTransform = transform} =
|
||||||
toTermWithAlias $ parsePatternForMatch (T.pack from) term
|
toTermWithAlias $ parsePatternForMatch (T.pack from) term
|
||||||
where
|
where
|
||||||
toTermWithAlias (Right (Just match)) = [OriginalTerm unpackedTerm, AliasTerm unpackedTerm (aliasedResult match)]
|
toTermWithAlias (Right (Just match)) = [OriginalTerm unpackedTerm, AliasTerm unpackedTerm (aliasedResult match)]
|
||||||
toTermWithAlias _ = [OriginalTerm unpackedTerm]
|
toTermWithAlias _ = [OriginalTerm unpackedTerm]
|
||||||
unpackedTerm = T.unpack term
|
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 :: Text -> Text -> Either Text (Maybe Text)
|
||||||
parsePatternForMatch aliasPattern term =
|
parsePatternForMatch aliasPattern term =
|
||||||
@ -36,4 +36,4 @@ parsePatternForMatch aliasPattern term =
|
|||||||
findMatch _ = Left $ T.pack $ "There was a problem with the pattern: " ++ show aliasPattern
|
findMatch _ = Left $ T.pack $ "There was a problem with the pattern: " ++ show aliasPattern
|
||||||
|
|
||||||
wildcard :: Text
|
wildcard :: Text
|
||||||
wildcard = "%s"
|
wildcard = "*"
|
||||||
|
52
src/Unused/Projection.hs
Normal file
52
src/Unused/Projection.hs
Normal file
@ -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
|
37
src/Unused/Projection/Transform.hs
Normal file
37
src/Unused/Projection/Transform.hs
Normal file
@ -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
|
@ -15,8 +15,10 @@ import qualified Control.Monad as M
|
|||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import Data.Text (Text)
|
||||||
import Data.Yaml (FromJSON(..), (.:), (.:?), (.!=))
|
import Data.Yaml (FromJSON(..), (.:), (.:?), (.!=))
|
||||||
import qualified Data.Yaml as Y
|
import qualified Data.Yaml as Y
|
||||||
|
import Unused.Projection
|
||||||
|
|
||||||
data LanguageConfiguration = LanguageConfiguration
|
data LanguageConfiguration = LanguageConfiguration
|
||||||
{ lcName :: String
|
{ lcName :: String
|
||||||
@ -34,6 +36,7 @@ data LowLikelihoodMatch = LowLikelihoodMatch
|
|||||||
data TermAlias = TermAlias
|
data TermAlias = TermAlias
|
||||||
{ taFrom :: String
|
{ taFrom :: String
|
||||||
, taTo :: String
|
, taTo :: String
|
||||||
|
, taTransform :: Text -> Text
|
||||||
}
|
}
|
||||||
|
|
||||||
data ParseConfigError = ParseConfigError
|
data ParseConfigError = ParseConfigError
|
||||||
@ -63,6 +66,7 @@ instance FromJSON TermAlias where
|
|||||||
parseJSON (Y.Object o) = TermAlias
|
parseJSON (Y.Object o) = TermAlias
|
||||||
<$> o .: "from"
|
<$> o .: "from"
|
||||||
<*> o .: "to"
|
<*> o .: "to"
|
||||||
|
<*> (either (fail . show) return =<< (translate . T.pack <$> (o .: "to")))
|
||||||
parseJSON _ = M.mzero
|
parseJSON _ = M.mzero
|
||||||
|
|
||||||
data MatchHandler a = MatchHandler
|
data MatchHandler a = MatchHandler
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Unused.AliasesSpec where
|
module Unused.AliasesSpec where
|
||||||
|
|
||||||
|
import Data.Monoid ((<>))
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Unused.Aliases
|
import Unused.Aliases
|
||||||
import Unused.ResultsClassifier.Types (TermAlias(..))
|
import Unused.ResultsClassifier.Types (TermAlias(..))
|
||||||
@ -15,8 +18,8 @@ spec = parallel $
|
|||||||
termsAndAliases [] ["method_1", "method_2"] `shouldBe` [OriginalTerm "method_1", OriginalTerm "method_2"]
|
termsAndAliases [] ["method_1", "method_2"] `shouldBe` [OriginalTerm "method_1", OriginalTerm "method_2"]
|
||||||
|
|
||||||
it "adds aliases to the list of terms" $ do
|
it "adds aliases to the list of terms" $ do
|
||||||
let predicateAlias = TermAlias "%s?" "be_%s"
|
let predicateAlias = TermAlias "*?" "be_{}" ("be_" <>)
|
||||||
let pluralizeAlias = TermAlias "really_%s" "very_%s"
|
let pluralizeAlias = TermAlias "really_*" "very_{}" ("very_" <>)
|
||||||
|
|
||||||
termsAndAliases [predicateAlias, pluralizeAlias] ["awesome?", "really_cool"]
|
termsAndAliases [predicateAlias, pluralizeAlias] ["awesome?", "really_cool"]
|
||||||
`shouldBe` [OriginalTerm "awesome?", AliasTerm "awesome?" "be_awesome", OriginalTerm "really_cool", AliasTerm "really_cool" "very_cool"]
|
`shouldBe` [OriginalTerm "awesome?", AliasTerm "awesome?" "be_awesome", OriginalTerm "really_cool", AliasTerm "really_cool" "very_cool"]
|
||||||
|
30
test/Unused/ProjectionSpec.hs
Normal file
30
test/Unused/ProjectionSpec.hs
Normal file
@ -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
|
@ -25,6 +25,8 @@ library
|
|||||||
, Unused.Util
|
, Unused.Util
|
||||||
, Unused.Regex
|
, Unused.Regex
|
||||||
, Unused.Aliases
|
, Unused.Aliases
|
||||||
|
, Unused.Projection
|
||||||
|
, Unused.Projection.Transform
|
||||||
, Unused.ResponseFilter
|
, Unused.ResponseFilter
|
||||||
, Unused.ResultsClassifier
|
, Unused.ResultsClassifier
|
||||||
, Unused.ResultsClassifier.Types
|
, Unused.ResultsClassifier.Types
|
||||||
@ -77,6 +79,8 @@ library
|
|||||||
, vector
|
, vector
|
||||||
, mtl
|
, mtl
|
||||||
, transformers
|
, transformers
|
||||||
|
, megaparsec
|
||||||
|
, inflections
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
@ -100,6 +104,7 @@ test-suite unused-test
|
|||||||
, unused
|
, unused
|
||||||
, hspec
|
, hspec
|
||||||
, containers
|
, containers
|
||||||
|
, text
|
||||||
other-modules: Unused.ParserSpec
|
other-modules: Unused.ParserSpec
|
||||||
, Unused.ResponseFilterSpec
|
, Unused.ResponseFilterSpec
|
||||||
, Unused.TypesSpec
|
, Unused.TypesSpec
|
||||||
|
Loading…
Reference in New Issue
Block a user