{-# LANGUAGE CPP #-} {- | Copyright: (c) 2020 Kowainik SPDX-License-Identifier: MPL-2.0 Maintainer: Kowainik Contains all 'Inspection's for known anti-patterns. The __anti-pattern__ inspections are in ranges: * @STAN-0201 .. STAN-0300@ -} module Stan.Inspection.AntiPattern ( -- * Anti-pattern inspections -- *** Anti-pattern @[0 .. length xs]@ stan0201 -- *** Anti-pattern 'foldl' , stan0202 -- *** Anti-pattern 'Data.ByteString.Char8.pack' , stan0203 -- *** Anti-pattern slow 'size' for 'HashMap' , stan0204 -- *** Anti-pattern slow 'size' for 'HashSet' , stan0205 -- *** Anti-pattern: Lazy fields , stan0206 -- *** Anti-pattern: Foldable methods on tuples, 'Maybe', 'Either' , stan0207 -- *** Anti-pattern: slow 'length' for 'Text' , stan0208 -- *** Anti-pattern: Slow 'nub' for lists , stan0209 -- *** Anti-pattern: Slow 'for_' on ranges , stan0210 -- *** Anti-pattern: '' for URLs , stan0211 -- *** Anti-pattern: unsafe functions , stan0212 -- *** Anti-pattern: Pattern-matching on @_@ , stan0213 -- *** Anti-pattern: use 'compare' , stan0214 -- *** Anti-pattern: Slashes in paths , stan0215 -- * All inspections , antiPatternInspectionsMap ) where import Relude.Extra.Lens ((%~), (.~)) import Relude.Extra.Tuple (fmapToFst) import Stan.Core.Id (Id (..)) import Stan.Inspection (Inspection (..), InspectionAnalysis (..), InspectionsMap, categoryL, descriptionL, severityL, solutionL) import Stan.NameMeta (NameMeta (..), baseNameFrom, mkBaseFoldableMeta, mkBaseOldListMeta, primTypeMeta, textNameFrom, unorderedNameFrom, _nameFrom) import Stan.Pattern.Ast (Literal (..), PatternAst (..), anyNamesToPatternAst, app, namesToPatternAst, opApp, range) import Stan.Pattern.Edsl (PatternBool (..)) import Stan.Pattern.Type (PatternType, charPattern, foldableMethodsPatterns, foldableTypesPatterns, listPattern, stringPattern, textPattern, (|->), (|::)) import Stan.Severity (Severity (..)) import qualified Data.List.NonEmpty as NE import qualified Stan.Category as Category -- | All anti-pattern 'Inspection's map from 'Id's. antiPatternInspectionsMap :: InspectionsMap antiPatternInspectionsMap = fromList $ fmapToFst inspectionId [ stan0201 , stan0202 , stan0203 , stan0204 , stan0205 , stan0206 , stan0207 , stan0208 , stan0209 , stan0210 , stan0211 , stan0212 , stan0213 , stan0214 , stan0215 ] -- | Smart constructor to create anti-pattern 'Inspection'. mkAntiPatternInspection :: Id Inspection -> Text -> InspectionAnalysis -> Inspection mkAntiPatternInspection insId name inspectionAnalysis = Inspection { inspectionId = insId , inspectionName = "Anti-pattern: " <> name , inspectionDescription = "" , inspectionSolution = [] , inspectionCategory = Category.antiPattern :| [] , inspectionSeverity = PotentialBug , .. } -- | 'Inspection' — @[0 .. length xs]@ @STAN-0201@. stan0201 :: Inspection stan0201 = mkAntiPatternInspection (Id "STAN-0201") "[0 .. length xs]" (FindAst lenPatAst) & descriptionL .~ "Creating a list with wrong number of indices" & solutionL .~ [ "Replace '[0 .. length xs]' with '[0 .. length xs - 1]'" , "Use 'zip [0 ..] xs` to work with list of pairs: index and element" ] where lenPatAst :: PatternAst lenPatAst = range (PatternAstConstant $ ExactNum 0) (app (PatternAstName (mkBaseFoldableMeta "length") (?)) (?) ) -- | 'Inspection' — 'foldl' @STAN-0202@. stan0202 :: Inspection stan0202 = mkAntiPatternInspection (Id "STAN-0202") "foldl" (FindAst $ PatternAstName (mkBaseFoldableMeta "foldl") (?)) & descriptionL .~ "Usage of space-leaking function 'foldl'" & solutionL .~ [ "Replace 'foldl' with 'foldl''" , "Use 'foldr (flip . f)` instead of 'foldl f'" ] & severityL .~ Error & categoryL %~ (Category.spaceLeak `NE.cons`) -- | 'Inspection' — 'Data.ByteString.Char8.pack' @STAN-0203@. stan0203 :: Inspection stan0203 = mkAntiPatternInspection (Id "STAN-0203") "Data.ByteString.Char8.pack" (FindAst $ PatternAstName packNameMeta (?)) & descriptionL .~ "Usage of 'pack' function that doesn't handle Unicode characters" & solutionL .~ [ "Convert to 'Text' and use 'encodeUtf8' from 'Data.Text.Encoding'" , "{Extra dependency} Use 'encodeUtf8' from 'relude'" , "{Extra dependency} Use the 'utf8-string' package" ] & severityL .~ Error where packNameMeta :: NameMeta packNameMeta = NameMeta { nameMetaPackage = "bytestring" , nameMetaModuleName = "Data.ByteString.Char8" , nameMetaName = "pack" } -- | 'Inspection' — slow 'Data.HashMap.Strict.size' and 'length' @STAN-0204@. stan0204 :: Inspection stan0204 = mkAntiPatternInspection (Id "STAN-0204") "HashMap size" (FindAst $ namesToPatternAst pats) & descriptionL .~ "Usage of 'size' or 'length' for 'HashMap' that runs in linear time" & solutionL .~ [ "{Extra dependency} Switch to 'Map' from 'containers'" ] & severityL .~ Performance where pats :: NonEmpty (NameMeta, PatternType) pats = (sizeNameMeta, (?)) :| [(mkBaseFoldableMeta "length", hmPat)] sizeNameMeta :: NameMeta sizeNameMeta = "size" `unorderedNameFrom` "Data.HashMap.Internal" hm :: NameMeta hm = "HashMap" `unorderedNameFrom` "Data.HashMap.Internal" hmPat :: PatternType hmPat = (hm |:: [(?), (?)]) |-> (?) -- | 'Inspection' — slow 'Data.HashSet.size' @STAN-0205@. stan0205 :: Inspection stan0205 = mkAntiPatternInspection (Id "STAN-0205") "HashSet size" (FindAst $ namesToPatternAst pats) & descriptionL .~ "Usage of 'size' or 'length' for 'HashSet' that runs in linear time" & solutionL .~ [ "{Extra dependency} Switch to 'Set' from 'containers'" ] & severityL .~ Performance where pats :: NonEmpty (NameMeta, PatternType) pats = (sizeNameMeta, (?)) :| [(mkBaseFoldableMeta "length", hsPat)] sizeNameMeta :: NameMeta sizeNameMeta = "size" `unorderedNameFrom` "Data.HashSet.Internal" hs :: NameMeta hs = "HashSet" `unorderedNameFrom` "Data.HashSet.Internal" hsPat :: PatternType hsPat = (hs |:: [(?)]) |-> (?) -- | 'Inspection' — missing strictness declaration @STAN-0206@. stan0206 :: Inspection stan0206 = Inspection { inspectionId = Id "STAN-0206" , inspectionName = "Data types with non-strict fields" , inspectionDescription = "Defining lazy fields in data types can lead to unexpected space leaks" , inspectionSolution = [ "Add '!' before the type, e.g. !Int or !(Maybe Bool)" , "Enable the 'StrictData' extension: {-# LANGUAGE StrictData #-}" ] , inspectionCategory = Category.spaceLeak :| [Category.syntax] , inspectionSeverity = Performance , inspectionAnalysis = LazyField } -- | 'Inspection' — 'Foldable' methods on possibly error-prone structures @STAN-0207@. stan0207 :: Inspection stan0207 = mkAntiPatternInspection (Id "STAN-0207") "Foldable methods on possibly error-prone structures" (FindAst $ namesToPatternAst allPatterns) & descriptionL .~ "Usage of Foldable methods on (,), Maybe, Either" & solutionL .~ [ "Use more explicit functions with specific monomorphic types" ] where allPatterns :: NonEmpty (NameMeta, PatternType) allPatterns = do -- Monad for NonEmpty t <- foldableTypesPatterns (method, mkType) <- foldableMethodsPatterns pure (method, mkType t) -- | 'Inspection' — slow 'length' for 'Data.Text' @STAN-0208@. stan0208 :: Inspection stan0208 = mkAntiPatternInspection (Id "STAN-0208") "Slow 'length' for Text" (FindAst $ PatternAstName lenNameMeta (textPattern |-> (?))) & descriptionL .~ "Usage of 'length' for 'Text' that runs in linear time" & solutionL .~ [ "{Extra dependency} Switch to 'ByteString' from 'bytestring'" ] & severityL .~ Performance where lenNameMeta :: NameMeta lenNameMeta = "length" `textNameFrom` "Data.Text" -- | 'Inspection' — slow 'nub' for lists @STAN-0209@. stan0209 :: Inspection stan0209 = mkAntiPatternInspection (Id "STAN-0209") "Slow 'nub' for lists" (FindAst $ PatternAstName (mkBaseOldListMeta "nub") $ listPattern |-> listPattern) & descriptionL .~ "Usage of 'nub' on lists that runs in quadratic time" & solutionL .~ [ "{Extra dependency} Switch list to 'Set' from 'containers'" , "{Extra dependency} Use 'ordNub/hashNub/sortNub/unstableNub' from 'relude'" , "{Extra dependency} Use 'nubOrd' from 'containers'" , "{Extra dependency} Use 'nubOrd' from 'extra'" ] & severityL .~ Performance -- | 'Inspection' — slow 'for_' and 'forM_' for ranges @STAN-0210@. stan0210 :: Inspection stan0210 = mkAntiPatternInspection (Id "STAN-0210") "Slow 'for_' on ranges" (FindAst pat) & descriptionL .~ "Usage of 'for_' or 'forM_' on numerical ranges is slow" & solutionL .~ [ "{Extra dependency} Use 'loop' library for fast monadic looping" ] & severityL .~ Performance where pat :: PatternAst pat = app forPattern (range (?) (?)) forPattern :: PatternAst forPattern = PatternAstOr (PatternAstName (mkBaseFoldableMeta "for_") forType) (PatternAstName (mkBaseFoldableMeta "forM_") forType) forType :: PatternType forType = listPattern |-> ((?) |-> (?)) |-> (?) -- | 'Inspection' — @@ on URLs @STAN-0211@. stan0211 :: Inspection stan0211 = mkAntiPatternInspection (Id "STAN-0211") "'' for URLs" (FindAst pat) & descriptionL .~ "Usage of '' for URLs results in the errors on Windows" & solutionL .~ [ "{Extra dependency} Use type-safe library for URLs" , "Concatenate URLs with slashes '/'" ] & severityL .~ Error where pat :: PatternAst pat = opApp (httpLit ||| urlName) filepathOperator (?) ||| opApp (?) filepathOperator urlName httpLit :: PatternAst httpLit = startWith "\"http:" ||| startWith "\"https:" ||| startWith "\"ftp:" ||| startWith "\"mailto:" ||| startWith "\"file:" ||| startWith "\"data:" ||| startWith "\"irc:" where startWith :: ByteString -> PatternAst startWith = PatternAstConstant . PrefixStr urlName :: PatternAst urlName = PatternAstVarName "url" filepathOperator :: PatternAst filepathOperator = PatternAstName operatorPosix fun ||| PatternAstName operatorWindows fun where operatorPosix :: NameMeta operatorPosix = NameMeta { nameMetaName = "" , nameMetaModuleName = "System.FilePath.Posix" , nameMetaPackage = "filepath" } operatorWindows :: NameMeta operatorWindows = NameMeta { nameMetaName = "" , nameMetaModuleName = "System.FilePath.Windows" , nameMetaPackage = "filepath" } fun :: PatternType fun = (?) |-> filePathType |-> (?) {- TODO: Note, that at the moment hie somehow thinks that '' works with 'String's even when I specify type of vars to 'FilePath' explicitly. This is odd and needs more investigation. -} filePathType :: PatternType filePathType = #if __GLASGOW_HASKELL__ < 910 "FilePath" `_nameFrom` "GHC.IO" #else "FilePath" `_nameFrom` "GHC.Internal.IO" #endif |:: [] ||| stringPattern ||| primTypeMeta "[]" |:: [ charPattern ] -- | 'Inspection' — usage of @unsafe*@ functions @STAN-0212@. stan0212 :: Inspection stan0212 = mkAntiPatternInspection (Id "STAN-0212") "unsafe functions" (FindAst pat) & descriptionL .~ "Usage of unsafe functions breaks referential transparency" & solutionL .~ [ "Remove 'undefined' or at least replace with 'error' to give better error messages" , "Replace 'unsafeCoerce' with 'coerce'" , "Rewrite the code to avoid using 'unsafePerformIO' and other unsafe IO functions" ] & severityL .~ Error & categoryL %~ (Category.unsafe `NE.cons`) where pat :: PatternAst pat = anyNamesToPatternAst #if __GLASGOW_HASKELL__ < 910 $ "undefined" `_nameFrom` "GHC.Err" :| [ "unsafeCoerce" `_nameFrom` "Unsafe.Coerce" , "unsafePerformIO" `_nameFrom` "GHC.IO.Unsafe" , "unsafeInterleaveIO" `_nameFrom` "GHC.IO.Unsafe" , "unsafeDupablePerformIO" `_nameFrom` "GHC.IO.Unsafe" #else $ "undefined" `_nameFrom` "GHC.Internal.Err" :| [ "unsafeCoerce" `_nameFrom` "GHC.Internal.Unsafe.Coerce" , "unsafePerformIO" `_nameFrom` "GHC.Internal.IO.Unsafe" , "unsafeInterleaveIO" `_nameFrom` "GHC.Internal.IO.Unsafe" , "unsafeDupablePerformIO" `_nameFrom` "GHC.Internal.IO.Unsafe" #endif , "unsafeFixIO" `baseNameFrom` "System.IO.Unsafe" ] -- | 'Inspection' — Pattent matching on @_@ for sum types — @STAN-0213@. stan0213 :: Inspection stan0213 = mkAntiPatternInspection (Id "STAN-0213") "Pattern matching on '_'" PatternMatchOn_ & descriptionL .~ "Pattern matching on '_' for sum types can create maintainability issues" & solutionL .~ [ "Pattern match on each constructor explicitly" , "Add meaningful names to holes, e.g. '_anyOtherFailure'" ] & severityL .~ Warning -- | 'Inspection' — use 'compare' @STAN-0214@. stan0214 :: Inspection stan0214 = mkAntiPatternInspection (Id "STAN-0214") "use 'compare'" UseCompare & descriptionL .~ "Usage of multiple comparison operators instead of single 'compare'" & solutionL .~ [ "Rewrite code to use single 'compare' instead of many comparison operators" ] & severityL .~ Performance -- | 'Inspection' — Slashes in paths @STAN-0215@. stan0215 :: Inspection stan0215 = mkAntiPatternInspection (Id "STAN-0215") "Slashes in paths" (FindAst pat) & descriptionL .~ "Usage of '/' or '\\' in paths results in the errors on different operation systems" & solutionL .~ [ "{Extra dependency} Use '' operator from 'filepath'" ] & severityL .~ Error where pat :: PatternAst pat = opApp pathLit filepathOperator (?) ||| opApp (?) filepathOperator pathLit pathLit :: PatternAst pathLit = PatternAstConstant (ContainStr "/") ||| PatternAstConstant (ContainStr "\\\\")