stan-0.0.0.0: src/Stan/Inspection/AntiPattern.hs
{- |
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)
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.Base"
hm :: NameMeta
hm = "HashMap" `unorderedNameFrom` "Data.HashMap.Base"
hmPat :: PatternType
hmPat = (hm |:: [(?), (?)]) |-> (?)
-- | 'Inspection' — slow 'Data.HashMap.Strict.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.Base"
hs :: NameMeta
hs = "HashSet" `unorderedNameFrom` "Data.HashSet.Base"
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 'bytesting'"
]
& 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 = "FilePath" `baseNameFrom` "GHC.IO" |:: []
||| 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
$ "undefined" `baseNameFrom` "GHC.Err" :|
[ "unsafeCoerce" `baseNameFrom` "Unsafe.Coerce"
, "unsafePerformIO" `baseNameFrom` "GHC.IO.Unsafe"
, "unsafeInterleaveIO" `baseNameFrom` "GHC.IO.Unsafe"
, "unsafeDupablePerformIO" `baseNameFrom` "GHC.IO.Unsafe"
, "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 "\\\\")