stan-0.1.3.0: src/Stan/Inspection/AntiPattern.hs
{-# LANGUAGE CPP #-}
{- |
Copyright: (c) 2020 Kowainik
SPDX-License-Identifier: MPL-2.0
Maintainer: Kowainik <xrom.xkov@gmail.com>
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 "\\\\")