stan-0.1.0.0: src/Stan/Analysis.hs
{- |
Copyright: (c) 2020 Kowainik
SPDX-License-Identifier: MPL-2.0
Maintainer: Kowainik <xrom.xkov@gmail.com>
Static analysis of all HIE files.
-}
module Stan.Analysis
( Analysis (..)
, runAnalysis
) where
import Data.Aeson.Micro (ToJSON (..), object, (.=))
import Extensions (ExtensionsError (..), OnOffExtension, ParsedExtensions (..),
SafeHaskellExtension, parseSourceWithPath, showOnOffExtension)
import Relude.Extra.Lens (Lens', lens, over)
import Stan.Analysis.Analyser (analyseAst)
import Stan.Cabal (mergeParsedExtensions)
import Stan.Core.Id (Id)
import Stan.Core.ModuleName (fromGhcModule)
import Stan.FileInfo (FileInfo (..), FileMap)
import Stan.Hie (countLinesOfCode)
import Stan.Hie.Compat (HieFile (..))
import Stan.Inspection (Inspection)
import Stan.Inspection.All (lookupInspectionById)
import Stan.Observation (Observation (..), Observations)
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Slist as S
{- | This data type stores all information collected during static analysis.
-}
data Analysis = Analysis
{ analysisModulesNum :: !Int
, analysisLinesOfCode :: !Int
, analysisUsedExtensions :: !(Set OnOffExtension, Set SafeHaskellExtension)
, analysisInspections :: !(HashSet (Id Inspection))
, analysisObservations :: !Observations
, analysisIgnoredObservations :: !Observations
, analysisFileMap :: !FileMap
} deriving stock (Show)
instance ToJSON Analysis where
toJSON Analysis{..} = object
[ "modulesNum" .= analysisModulesNum
, "linesOfCode" .= analysisLinesOfCode
, "usedExtensions" .=
let (ext, safeExt) = analysisUsedExtensions
in map showOnOffExtension (toList ext)
<> map (show @Text) (toList safeExt)
, "inspections" .= toList analysisInspections
, "observations" .= toJsonObs analysisObservations
, "ignoredObservations" .= toJsonObs analysisIgnoredObservations
, "fileMap" .= map (first toText) (Map.toList analysisFileMap)
]
where
toJsonObs :: Observations -> [Observation]
toJsonObs = toList . S.sortOn observationSrcSpan
modulesNumL :: Lens' Analysis Int
modulesNumL = lens
analysisModulesNum
(\analysis new -> analysis { analysisModulesNum = new })
linesOfCodeL :: Lens' Analysis Int
linesOfCodeL = lens
analysisLinesOfCode
(\analysis new -> analysis { analysisLinesOfCode = new })
extensionsL :: Lens' Analysis (Set OnOffExtension, Set SafeHaskellExtension)
extensionsL = lens
analysisUsedExtensions
(\analysis new -> analysis { analysisUsedExtensions = new })
inspectionsL :: Lens' Analysis (HashSet (Id Inspection))
inspectionsL = lens
analysisInspections
(\analysis new -> analysis { analysisInspections = new })
observationsL :: Lens' Analysis Observations
observationsL = lens
analysisObservations
(\analysis new -> analysis { analysisObservations = new })
ignoredObservationsL :: Lens' Analysis Observations
ignoredObservationsL = lens
analysisIgnoredObservations
(\analysis new -> analysis { analysisIgnoredObservations = new })
fileMapL :: Lens' Analysis FileMap
fileMapL = lens
analysisFileMap
(\analysis new -> analysis { analysisFileMap = new })
initialAnalysis :: Analysis
initialAnalysis = Analysis
{ analysisModulesNum = 0
, analysisLinesOfCode = 0
, analysisUsedExtensions = mempty
, analysisInspections = mempty
, analysisObservations = mempty
, analysisIgnoredObservations = mempty
, analysisFileMap = mempty
}
incModulesNum :: State Analysis ()
incModulesNum = modify' $ over modulesNumL (+ 1)
{- | Increase the total loc ('analysisLinesOfCode') by the given number of
analised lines of code.
-}
incLinesOfCode :: Int -> State Analysis ()
incLinesOfCode num = modify' $ over linesOfCodeL (+ num)
-- | Add set of 'Inspection' 'Id's to the existing set.
addInspections :: HashSet (Id Inspection) -> State Analysis ()
addInspections ins = modify' $ over inspectionsL (ins <>)
-- | Add list of 'Observation's to the beginning of the existing list
addObservations :: Observations -> State Analysis ()
addObservations observations = modify' $ over observationsL (observations <>)
-- | Add list of 'Observation's to the beginning of the existing list
addIgnoredObservations :: Observations -> State Analysis ()
addIgnoredObservations obs = modify' $ over ignoredObservationsL (obs <>)
-- | Collect all unique used extensions.
addExtensions :: ParsedExtensions -> State Analysis ()
addExtensions ParsedExtensions{..} = modify' $ over extensionsL
(\(setExts, setSafeExts) ->
( Set.union (Set.fromList parsedExtensionsAll) setExts
, maybe setSafeExts (`Set.insert` setSafeExts) parsedExtensionsSafe
)
)
-- | Update 'FileInfo' for the given 'FilePath'.
updateFileMap :: FilePath -> FileInfo -> State Analysis ()
updateFileMap fp fi = modify' $ over fileMapL (Map.insert fp fi)
{- | Perform static analysis of given 'HieFile'.
-}
runAnalysis
:: Map FilePath (Either ExtensionsError ParsedExtensions)
-> HashMap FilePath (HashSet (Id Inspection))
-> [Id Observation] -- ^ List of to-be-ignored Observations.
-> [HieFile]
-> Analysis
runAnalysis cabalExtensionsMap checksMap obs = executingState initialAnalysis .
analyse cabalExtensionsMap checksMap obs
analyse
:: Map FilePath (Either ExtensionsError ParsedExtensions)
-> HashMap FilePath (HashSet (Id Inspection))
-> [Id Observation] -- ^ List of to-be-ignored Observations.
-> [HieFile]
-> State Analysis ()
analyse _extsMap _checksMap _observations [] = pass
analyse cabalExtensions checksMap observations (hieFile@HieFile{..}:hieFiles) = do
whenJust (HM.lookup hie_hs_file checksMap)
(analyseHieFile hieFile cabalExtensions observations)
analyse cabalExtensions checksMap observations hieFiles
analyseHieFile
:: HieFile
-> Map FilePath (Either ExtensionsError ParsedExtensions)
-> [Id Observation] -- ^ List of to-be-ignored Observations.
-> HashSet (Id Inspection)
-> State Analysis ()
analyseHieFile hieFile@HieFile{..} cabalExts obs insIds = do
-- traceM (hie_hs_file hieFile)
let fileInfoLoc = countLinesOfCode hieFile
let fileInfoCabalExtensions = fromMaybe
(Left $ NotCabalModule hie_hs_file)
(Map.lookup hie_hs_file cabalExts)
let fileInfoExtensions = first (ModuleParseError hie_hs_file) $
parseSourceWithPath hie_hs_file hie_hs_src
let fileInfoPath = hie_hs_file
let fileInfoModuleName = fromGhcModule hie_module
-- merge cabal and module extensions and update overall exts
let fileInfoMergedExtensions = mergeParsedExtensions fileInfoCabalExtensions fileInfoExtensions
-- get list of inspections for the file
let inss = mapMaybe lookupInspectionById (toList insIds)
-- get all observations by analysing ast
let allObservations = analyseAst hieFile fileInfoMergedExtensions inss
let (ignoredObs, fileInfoObservations) = S.partition ((`elem` obs) . observationId) allObservations
incModulesNum
incLinesOfCode fileInfoLoc
updateFileMap hie_hs_file FileInfo{..}
whenRight_ fileInfoExtensions addExtensions
whenRight_ fileInfoCabalExtensions addExtensions
addInspections insIds
addObservations fileInfoObservations
addIgnoredObservations ignoredObs