packages feed

stan-0.0.1.0: src/Stan/Analysis/Pretty.hs

{- |
Copyright: (c) 2020 Kowainik
SPDX-License-Identifier: MPL-2.0
Maintainer: Kowainik <xrom.xkov@gmail.com>

Pretty printing of Stan's analysis.
-}

module Stan.Analysis.Pretty
    ( prettyShowAnalysis

      -- * Numbers
    , AnalysisNumbers (..)
    , ProjectHealth (..)
    , analysisToNumbers
    , prettyHealth
    , toProjectHealth
    ) where

import Colourista.Short (b, i)
import Extensions (ExtensionsError, ParsedExtensions)
import Text.Printf (printf)

import Stan.Analysis (Analysis (..))
import Stan.Core.ModuleName (ModuleName (..))
import Stan.FileInfo (FileInfo (..), extensionsToText)
import Stan.Observation (Observation (..), prettyShowObservation)
import Stan.Report.Settings (OutputSettings (..), Verbosity (..))

import qualified Data.HashSet as HS
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Slist as S


{- | Shows analysed output of Stan work.
This functions groups 'Observation's by 'FilePath' they are found in.
-}
prettyShowAnalysis :: Analysis -> OutputSettings -> Text
prettyShowAnalysis an rs@OutputSettings{..} = case outputSettingsVerbosity of
    Verbose    -> groupedObservations <> summary (analysisToNumbers an)
    NonVerbose -> unlines $ toList $ prettyShowObservation rs <$> analysisObservations an
  where
    groupedObservations :: Text
    groupedObservations =
        Text.intercalate "\n\n"
        $ filter (/= "")
        $ map (showByFile rs)
        $ Map.elems
        $ analysisFileMap an

data AnalysisNumbers = AnalysisNumbers
    { anModules    :: !Int
    , anLoc        :: !Int
    , anExts       :: !Int
    , anSafeExts   :: !Int
    , anIns        :: !Int
    , anFoundObs   :: !Int
    , anIgnoredObs :: !Int
    , anHealth     :: !Double
    }

analysisToNumbers :: Analysis -> AnalysisNumbers
analysisToNumbers Analysis{..} = AnalysisNumbers
    { anModules    = analysisModulesNum
    , anLoc        = analysisLinesOfCode
    , anExts       = Set.size $ fst analysisUsedExtensions
    , anSafeExts   = Set.size $ snd analysisUsedExtensions
    , anIns        = HS.size analysisInspections
    , anFoundObs   = length analysisObservations
    , anIgnoredObs = length analysisIgnoredObservations
    , anHealth     = calculatedHealth
    }
  where
    calculatedHealth :: Double
    calculatedHealth =
        -- all inspections ignored or no observations
        if null analysisInspections || null analysisObservations
        then 100
        else
            let totalInspections = fromIntegral $ HS.size analysisInspections
                triggeredInspections =
                    fromIntegral
                    $ Set.size
                    $ Set.fromList
                    $ map observationInspectionId
                    $ toList analysisObservations

            in 100 * (1 - triggeredInspections / totalInspections)

{- | Show project health as pretty text with 2 digits after dot.
-}
prettyHealth :: Double -> Text
prettyHealth health =
    if fromIntegral (floor health :: Int) == health  -- display without decimal part
    then toText (printf "%.0f" health :: String) <> "%"
    else toText (printf "%.2f" health :: String) <> "%"

{- | Enum to describe project health depending on the value of
'anHealth'.
-}
data ProjectHealth
    = Unhealthy
    | LowHealth
    | MediumHealth
    | Healthy

-- | Calculate 'ProjectHealth'.
toProjectHealth :: Double -> ProjectHealth
toProjectHealth health
    | health >= 100 = Healthy
    | health >= 80  = MediumHealth
    | health >= 40  = LowHealth
    | otherwise     = Unhealthy

summary :: AnalysisNumbers -> Text
summary AnalysisNumbers{..} = unlines
    [ ""
    , b "           Stan's Summary:"
    , top
    , alignText "Analysed modules" <> alignNum anModules
    , mid
    , alignText "Analysed Lines of Code" <> alignNum anLoc
    , mid
    , alignText "Total Haskell2010 extensions" <> alignNum anExts
    , mid
    , alignText "Total SafeHaskell extensions" <> alignNum anSafeExts
    , mid
    , alignText "Total checked inspections" <> alignNum anIns
    , mid
    , alignText "Total found observations" <> alignNum anFoundObs
    , mid
    , alignText "Total ignored observations" <> alignNum anIgnoredObs
    , mid
    , alignText "Project health" <> alignVal (prettyHealth anHealth)
    , bot
    ]
  where
    alignNum :: Int -> Text
    alignNum = alignVal . show

    alignVal :: Text -> Text
    alignVal x = " ┃ " <> Text.justifyLeft 6 ' ' x <> " ┃"

    alignText :: Text -> Text
    alignText txt ="┃ " <> Text.justifyLeft 28 ' ' txt

    separator :: Text -> Text -> Text -> Text
    separator l c r = l <> Text.replicate 30 "━" <> c <> Text.replicate 8 "━" <> r
    top, mid, bot :: Text
    top = separator "┏" "┳" "┓"
    mid = separator "┣" "╋" "┫"
    bot = separator "┗" "┻" "┛"

showByFile :: OutputSettings -> FileInfo -> Text
showByFile outputSettings FileInfo{..} = if len == 0 then "" else unlines
    [ i "  File:         " <> b (toText fileInfoPath)
    , i "  Module:       " <> b (unModuleName fileInfoModuleName)
    , i "  LoC:          " <> b (show fileInfoLoc)
    , i "  Observations: " <> b (show len)
    , i "  Extensions from .cabal: " <> b (showExts fileInfoCabalExtensions)
    , i "  Extensions from module: " <> b (showExts fileInfoExtensions)
    , " ┏" <> Text.replicate 78 "━"
    ]

    <> Text.intercalate (" ┃\n ┃" <> Text.replicate 78 "~" <> "\n ┃\n")
        (toList $ prettyShowObservation outputSettings <$> S.sortOn observationSrcSpan fileInfoObservations)
  where
    len :: Int
    len = length fileInfoObservations

    showExts :: Either ExtensionsError ParsedExtensions -> Text
    showExts = Text.intercalate ", " . extensionsToText