packages feed

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 "\\\\")