{-# LANGUAGE ApplicativeDo        #-}
{-# LANGUAGE DataKinds            #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE OverloadedLabels     #-}
{-# LANGUAGE TypeOperators        #-}
{-# LANGUAGE UndecidableInstances #-}

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

@stan@ runtime configuration that allows customizing the set of
inspections to check the code against.
-}

module Stan.Config
    ( -- * Data types
      ConfigP (..)
    , Config
    , PartialConfig
    , Check (..)
    , CheckType (..)
    , CheckFilter (..)
    , Scope (..)

      -- * Default
    , defaultConfig
    , mkDefaultChecks

      -- * Final stage
    , finaliseConfig

      -- * Printing
    , configToCliCommand

      -- * Apply config
      -- $applyConfig
    , applyConfig
    , applyChecks
    , applyChecksFor
    ) where

import Trial ((::-), Phase (..), Trial, withTag)

import Stan.Category (Category (..))
import Stan.Core.Id (Id (..))
import Stan.Inspection (Inspection (..))
import Stan.Inspection.All (inspections, inspectionsIds, lookupInspectionById)
import Stan.Observation (Observation (..))
import Stan.Severity (Severity (..))

import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashSet as HashSet
import qualified Data.Text as T


{- | Main configuration type for the following purposes:

* Filtering inspections (including or ignoring) per scope (file,
  directory, all)
-}
data ConfigP (p :: Phase Text) = ConfigP
    { forall (p :: Phase Text). ConfigP p -> p ::- [Check]
configChecks  :: !(p ::- [Check])
    , forall (p :: Phase Text). ConfigP p -> p ::- [Scope]
configRemoved :: !(p ::- [Scope])
    , forall (p :: Phase Text). ConfigP p -> p ::- [Id Observation]
configIgnored :: !(p ::- [Id Observation])
    -- , configGroupBy :: !GroupBy
    }

deriving stock instance
    ( Show (p ::- [Check])
    , Show (p ::- [Scope])
    , Show (p ::- [Id Observation])
    ) => Show (ConfigP p)

deriving stock instance
    ( Eq (p ::- [Check])
    , Eq (p ::- [Scope])
    , Eq (p ::- [Id Observation])
    ) => Eq (ConfigP p)

type Config = ConfigP 'Final
type PartialConfig = ConfigP 'Partial

instance Semigroup PartialConfig where
    (<>) :: PartialConfig -> PartialConfig -> PartialConfig
    PartialConfig
x <> :: PartialConfig -> PartialConfig -> PartialConfig
<> PartialConfig
y = ConfigP
        { configChecks :: 'Partial ::- [Check]
configChecks  = PartialConfig -> 'Partial ::- [Check]
forall (p :: Phase Text). ConfigP p -> p ::- [Check]
configChecks PartialConfig
x Trial Text (Text, [Check])
-> Trial Text (Text, [Check]) -> Trial Text (Text, [Check])
forall a. Semigroup a => a -> a -> a
<> PartialConfig -> 'Partial ::- [Check]
forall (p :: Phase Text). ConfigP p -> p ::- [Check]
configChecks PartialConfig
y
        , configRemoved :: 'Partial ::- [Scope]
configRemoved = PartialConfig -> 'Partial ::- [Scope]
forall (p :: Phase Text). ConfigP p -> p ::- [Scope]
configRemoved PartialConfig
x Trial Text (Text, [Scope])
-> Trial Text (Text, [Scope]) -> Trial Text (Text, [Scope])
forall a. Semigroup a => a -> a -> a
<> PartialConfig -> 'Partial ::- [Scope]
forall (p :: Phase Text). ConfigP p -> p ::- [Scope]
configRemoved PartialConfig
y
        , configIgnored :: 'Partial ::- [Id Observation]
configIgnored = PartialConfig -> 'Partial ::- [Id Observation]
forall (p :: Phase Text). ConfigP p -> p ::- [Id Observation]
configIgnored PartialConfig
x Trial Text (Text, [Id Observation])
-> Trial Text (Text, [Id Observation])
-> Trial Text (Text, [Id Observation])
forall a. Semigroup a => a -> a -> a
<> PartialConfig -> 'Partial ::- [Id Observation]
forall (p :: Phase Text). ConfigP p -> p ::- [Id Observation]
configIgnored PartialConfig
y
        }

-- | Type of 'Check': 'Include' or 'Exclude' 'Inspection's.
data CheckType
    = Include
    | Exclude
    deriving stock (Int -> CheckType -> ShowS
[CheckType] -> ShowS
CheckType -> FilePath
(Int -> CheckType -> ShowS)
-> (CheckType -> FilePath)
-> ([CheckType] -> ShowS)
-> Show CheckType
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CheckType -> ShowS
showsPrec :: Int -> CheckType -> ShowS
$cshow :: CheckType -> FilePath
show :: CheckType -> FilePath
$cshowList :: [CheckType] -> ShowS
showList :: [CheckType] -> ShowS
Show, CheckType -> CheckType -> Bool
(CheckType -> CheckType -> Bool)
-> (CheckType -> CheckType -> Bool) -> Eq CheckType
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CheckType -> CheckType -> Bool
== :: CheckType -> CheckType -> Bool
$c/= :: CheckType -> CheckType -> Bool
/= :: CheckType -> CheckType -> Bool
Eq, Int -> CheckType
CheckType -> Int
CheckType -> [CheckType]
CheckType -> CheckType
CheckType -> CheckType -> [CheckType]
CheckType -> CheckType -> CheckType -> [CheckType]
(CheckType -> CheckType)
-> (CheckType -> CheckType)
-> (Int -> CheckType)
-> (CheckType -> Int)
-> (CheckType -> [CheckType])
-> (CheckType -> CheckType -> [CheckType])
-> (CheckType -> CheckType -> [CheckType])
-> (CheckType -> CheckType -> CheckType -> [CheckType])
-> Enum CheckType
forall a.
(a -> a)
-> (a -> a)
-> (Int -> a)
-> (a -> Int)
-> (a -> [a])
-> (a -> a -> [a])
-> (a -> a -> [a])
-> (a -> a -> a -> [a])
-> Enum a
$csucc :: CheckType -> CheckType
succ :: CheckType -> CheckType
$cpred :: CheckType -> CheckType
pred :: CheckType -> CheckType
$ctoEnum :: Int -> CheckType
toEnum :: Int -> CheckType
$cfromEnum :: CheckType -> Int
fromEnum :: CheckType -> Int
$cenumFrom :: CheckType -> [CheckType]
enumFrom :: CheckType -> [CheckType]
$cenumFromThen :: CheckType -> CheckType -> [CheckType]
enumFromThen :: CheckType -> CheckType -> [CheckType]
$cenumFromTo :: CheckType -> CheckType -> [CheckType]
enumFromTo :: CheckType -> CheckType -> [CheckType]
$cenumFromThenTo :: CheckType -> CheckType -> CheckType -> [CheckType]
enumFromThenTo :: CheckType -> CheckType -> CheckType -> [CheckType]
Enum, CheckType
CheckType -> CheckType -> Bounded CheckType
forall a. a -> a -> Bounded a
$cminBound :: CheckType
minBound :: CheckType
$cmaxBound :: CheckType
maxBound :: CheckType
Bounded)

-- | Rule to control the set of inspections per scope.
data Check = Check
    { Check -> CheckType
checkType   :: !CheckType
    , Check -> CheckFilter
checkFilter :: !CheckFilter
    , Check -> Scope
checkScope  :: !Scope
    } deriving stock (Int -> Check -> ShowS
[Check] -> ShowS
Check -> FilePath
(Int -> Check -> ShowS)
-> (Check -> FilePath) -> ([Check] -> ShowS) -> Show Check
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Check -> ShowS
showsPrec :: Int -> Check -> ShowS
$cshow :: Check -> FilePath
show :: Check -> FilePath
$cshowList :: [Check] -> ShowS
showList :: [Check] -> ShowS
Show, Check -> Check -> Bool
(Check -> Check -> Bool) -> (Check -> Check -> Bool) -> Eq Check
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Check -> Check -> Bool
== :: Check -> Check -> Bool
$c/= :: Check -> Check -> Bool
/= :: Check -> Check -> Bool
Eq)

-- | Criterion for inspections filtering.
data CheckFilter
    = CheckInspection !(Id Inspection)
    | CheckSeverity !Severity
    | CheckCategory !Category
    | CheckAll
    deriving stock (Int -> CheckFilter -> ShowS
[CheckFilter] -> ShowS
CheckFilter -> FilePath
(Int -> CheckFilter -> ShowS)
-> (CheckFilter -> FilePath)
-> ([CheckFilter] -> ShowS)
-> Show CheckFilter
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CheckFilter -> ShowS
showsPrec :: Int -> CheckFilter -> ShowS
$cshow :: CheckFilter -> FilePath
show :: CheckFilter -> FilePath
$cshowList :: [CheckFilter] -> ShowS
showList :: [CheckFilter] -> ShowS
Show, CheckFilter -> CheckFilter -> Bool
(CheckFilter -> CheckFilter -> Bool)
-> (CheckFilter -> CheckFilter -> Bool) -> Eq CheckFilter
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CheckFilter -> CheckFilter -> Bool
== :: CheckFilter -> CheckFilter -> Bool
$c/= :: CheckFilter -> CheckFilter -> Bool
/= :: CheckFilter -> CheckFilter -> Bool
Eq)

-- | Where to apply the rule for controlling inspection set.
data Scope
    = ScopeFile !FilePath
    | ScopeDirectory !FilePath
    | ScopeAll
    deriving stock (Int -> Scope -> ShowS
[Scope] -> ShowS
Scope -> FilePath
(Int -> Scope -> ShowS)
-> (Scope -> FilePath) -> ([Scope] -> ShowS) -> Show Scope
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Scope -> ShowS
showsPrec :: Int -> Scope -> ShowS
$cshow :: Scope -> FilePath
show :: Scope -> FilePath
$cshowList :: [Scope] -> ShowS
showList :: [Scope] -> ShowS
Show, Scope -> Scope -> Bool
(Scope -> Scope -> Bool) -> (Scope -> Scope -> Bool) -> Eq Scope
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Scope -> Scope -> Bool
== :: Scope -> Scope -> Bool
$c/= :: Scope -> Scope -> Bool
/= :: Scope -> Scope -> Bool
Eq)

defaultConfig :: PartialConfig
defaultConfig :: PartialConfig
defaultConfig = ConfigP
    { configChecks :: 'Partial ::- [Check]
configChecks  = Text -> Trial Text [Check] -> Trial Text (Text, [Check])
forall tag a. tag -> Trial tag a -> TaggedTrial tag a
withTag Text
"Default" (Trial Text [Check] -> Trial Text (Text, [Check]))
-> Trial Text [Check] -> Trial Text (Text, [Check])
forall a b. (a -> b) -> a -> b
$ [Check] -> Trial Text [Check]
forall a. a -> Trial Text a
forall (f :: * -> *) a. Applicative f => a -> f a
pure []
    , configRemoved :: 'Partial ::- [Scope]
configRemoved = Text -> Trial Text [Scope] -> Trial Text (Text, [Scope])
forall tag a. tag -> Trial tag a -> TaggedTrial tag a
withTag Text
"Default" (Trial Text [Scope] -> Trial Text (Text, [Scope]))
-> Trial Text [Scope] -> Trial Text (Text, [Scope])
forall a b. (a -> b) -> a -> b
$ [Scope] -> Trial Text [Scope]
forall a. a -> Trial Text a
forall (f :: * -> *) a. Applicative f => a -> f a
pure []
    , configIgnored :: 'Partial ::- [Id Observation]
configIgnored = Text
-> Trial Text [Id Observation]
-> Trial Text (Text, [Id Observation])
forall tag a. tag -> Trial tag a -> TaggedTrial tag a
withTag Text
"Default" (Trial Text [Id Observation]
 -> Trial Text (Text, [Id Observation]))
-> Trial Text [Id Observation]
-> Trial Text (Text, [Id Observation])
forall a b. (a -> b) -> a -> b
$ [Id Observation] -> Trial Text [Id Observation]
forall a. a -> Trial Text a
forall (f :: * -> *) a. Applicative f => a -> f a
pure []
    }

finaliseConfig :: PartialConfig -> Trial Text Config
finaliseConfig :: PartialConfig -> Trial Text Config
finaliseConfig PartialConfig
config = do
    [Check]
configChecks  <- PartialConfig -> Trial Text [Check]
forall (x :: Symbol) a. IsLabel x a => a
#configChecks PartialConfig
config
    [Scope]
configRemoved <- PartialConfig -> Trial Text [Scope]
forall (x :: Symbol) a. IsLabel x a => a
#configRemoved PartialConfig
config
    [Id Observation]
configIgnored <- PartialConfig -> Trial Text [Id Observation]
forall (x :: Symbol) a. IsLabel x a => a
#configIgnored PartialConfig
config
    pure ConfigP {[Id Observation]
[Scope]
[Check]
'Final ::- [Id Observation]
'Final ::- [Scope]
'Final ::- [Check]
configChecks :: 'Final ::- [Check]
configRemoved :: 'Final ::- [Scope]
configIgnored :: 'Final ::- [Id Observation]
configChecks :: [Check]
configRemoved :: [Scope]
configIgnored :: [Id Observation]
..}


{- | Convert TOML configuration to the equivalent CLI command that can
be copy-pasted to get the same results as using the TOML config.

@
  ⓘ Reading Configurations from \/home\/vrom911\/Kowainik\/stan\/.stan.toml ...
stan check --exclude --directory=test/ \\
     check --include \\
     check --exclude --inspectionId=STAN-0002 \\
     check --exclude --inspectionId=STAN-0001 --file=src/MyFile.hs
     remove --file=src/Secret.hs
     ignore --id="STAN0001-asdfgh42:42"
@
-}
configToCliCommand :: Config -> Text
configToCliCommand :: Config -> Text
configToCliCommand ConfigP{'Final ::- [Id Observation]
'Final ::- [Scope]
'Final ::- [Check]
configChecks :: forall (p :: Phase Text). ConfigP p -> p ::- [Check]
configRemoved :: forall (p :: Phase Text). ConfigP p -> p ::- [Scope]
configIgnored :: forall (p :: Phase Text). ConfigP p -> p ::- [Id Observation]
configChecks :: 'Final ::- [Check]
configRemoved :: 'Final ::- [Scope]
configIgnored :: 'Final ::- [Id Observation]
..} = Text
"stan " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> [Text] -> Text
T.intercalate Text
" \\\n     "
    (  (Check -> Text) -> [Check] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Check -> Text
checkToCli [Check]
'Final ::- [Check]
configChecks
    [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> (Scope -> Text) -> [Scope] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Scope -> Text
removedToCli [Scope]
'Final ::- [Scope]
configRemoved
    [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> (Id Observation -> Text) -> [Id Observation] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Id Observation -> Text
ignoredToCli [Id Observation]
'Final ::- [Id Observation]
configIgnored
    )
  where
    checkToCli :: Check -> Text
    checkToCli :: Check -> Text
checkToCli Check{Scope
CheckFilter
CheckType
checkType :: Check -> CheckType
checkFilter :: Check -> CheckFilter
checkScope :: Check -> Scope
checkType :: CheckType
checkFilter :: CheckFilter
checkScope :: Scope
..} = Text
"check"
        Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> CheckType -> Text
checkTypeToCli CheckType
checkType
        Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> CheckFilter -> Text
checkFilterToCli CheckFilter
checkFilter
        Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Scope -> Text
scopeToCli Scope
checkScope

    removedToCli :: Scope -> Text
    removedToCli :: Scope -> Text
removedToCli Scope
scope = Text
"remove"
        Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Scope -> Text
scopeToCli Scope
scope

    ignoredToCli :: Id Observation -> Text
    ignoredToCli :: Id Observation -> Text
ignoredToCli Id Observation
obsId = Text
"ignore"
        Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Id Observation -> Text
forall a. Id a -> Text
idToCli Id Observation
obsId

    idToCli :: Id a -> Text
    idToCli :: forall a. Id a -> Text
idToCli = Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
(<>) Text
" --id=" (Text -> Text) -> (Id a -> Text) -> Id a -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Id a -> Text
forall a. Id a -> Text
unId

    checkTypeToCli :: CheckType -> Text
    checkTypeToCli :: CheckType -> Text
checkTypeToCli = \case
        CheckType
Include -> Text
" --include"
        CheckType
Exclude -> Text
" --exclude"

    checkFilterToCli :: CheckFilter -> Text
    checkFilterToCli :: CheckFilter -> Text
checkFilterToCli = \case
        CheckInspection Id Inspection
insId -> Id Inspection -> Text
forall a. Id a -> Text
idToCli Id Inspection
insId
        CheckSeverity Severity
sev -> Text
" --severity=" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Severity -> Text
forall b a. (Show a, IsString b) => a -> b
show Severity
sev
        CheckCategory Category
cat -> Text
" --category=" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Category -> Text
unCategory Category
cat
        CheckFilter
CheckAll -> Text
" --filter-all"

    scopeToCli :: Scope -> Text
    scopeToCli :: Scope -> Text
scopeToCli = \case
        ScopeFile FilePath
file -> Text
" --file=" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> FilePath -> Text
forall a. ToText a => a -> Text
toText FilePath
file
        ScopeDirectory FilePath
dir -> Text
" --directory=" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> FilePath -> Text
forall a. ToText a => a -> Text
toText FilePath
dir
        Scope
ScopeAll -> Text
" --scope-all"

mkDefaultChecks :: [FilePath] -> HashMap FilePath (HashSet (Id Inspection))
mkDefaultChecks :: [FilePath] -> HashMap FilePath (HashSet (Id Inspection))
mkDefaultChecks = [(FilePath, HashSet (Id Inspection))]
-> HashMap FilePath (HashSet (Id Inspection))
forall k v. (Eq k, Hashable k) => [(k, v)] -> HashMap k v
HashMap.fromList ([(FilePath, HashSet (Id Inspection))]
 -> HashMap FilePath (HashSet (Id Inspection)))
-> ([FilePath] -> [(FilePath, HashSet (Id Inspection))])
-> [FilePath]
-> HashMap FilePath (HashSet (Id Inspection))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (FilePath -> (FilePath, HashSet (Id Inspection)))
-> [FilePath] -> [(FilePath, HashSet (Id Inspection))]
forall a b. (a -> b) -> [a] -> [b]
map (, HashSet (Id Inspection)
inspectionsIds)

{- | Apply configuration to the given list of files to get the set of
inspections for each file.

The algorithm:

1. Remove all files specified by the @remove@ option.
2. Run 'applyChecks' on the remaining files.
-}
applyConfig
    :: [FilePath]
    -- ^ Paths to project files
    -> Config
    -- ^ Stan runtime configuration
    -> HashMap FilePath (HashSet (Id Inspection))
    -- ^ Resulting set of inspections for each file
applyConfig :: [FilePath] -> Config -> HashMap FilePath (HashSet (Id Inspection))
applyConfig [FilePath]
paths ConfigP{'Final ::- [Id Observation]
'Final ::- [Scope]
'Final ::- [Check]
configChecks :: forall (p :: Phase Text). ConfigP p -> p ::- [Check]
configRemoved :: forall (p :: Phase Text). ConfigP p -> p ::- [Scope]
configIgnored :: forall (p :: Phase Text). ConfigP p -> p ::- [Id Observation]
configChecks :: 'Final ::- [Check]
configRemoved :: 'Final ::- [Scope]
configIgnored :: 'Final ::- [Id Observation]
..} =
    [FilePath] -> [Check] -> HashMap FilePath (HashSet (Id Inspection))
applyChecks ((FilePath -> Bool) -> [FilePath] -> [FilePath]
forall a. (a -> Bool) -> [a] -> [a]
filter FilePath -> Bool
notRemoved [FilePath]
paths) [Check]
'Final ::- [Check]
configChecks
  where
    -- TODO: can be implemented efficiently, but the more efficient
    -- implementation is required only if the @configRemoved@ can have
    -- >= 1K entries
    notRemoved :: FilePath -> Bool
    notRemoved :: FilePath -> Bool
notRemoved FilePath
path = (Scope -> Bool) -> [Scope] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all (FilePath -> Scope -> Bool
isNotInScope FilePath
path) [Scope]
'Final ::- [Scope]
configRemoved

    isNotInScope :: FilePath -> Scope -> Bool
    isNotInScope :: FilePath -> Scope -> Bool
isNotInScope FilePath
path = \case
        ScopeFile FilePath
file -> FilePath
path FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
/= FilePath
file
        ScopeDirectory FilePath
dir -> Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$ FilePath -> FilePath -> Bool
isInDir FilePath
dir FilePath
path
        Scope
ScopeAll -> Bool
False

{- | Convert the list of 'Check's from 'Config' to data structure that
allows filtering of 'Inspection's for given files.
-}
applyChecks
    :: [FilePath]
    -- ^ Paths to project files
    -> [Check]
    -- ^ List of rules
    -> HashMap FilePath (HashSet (Id Inspection))
    -- ^ Resulting set of inspections for each file
applyChecks :: [FilePath] -> [Check] -> HashMap FilePath (HashSet (Id Inspection))
applyChecks = HashMap FilePath (HashSet (Id Inspection))
-> [Check] -> HashMap FilePath (HashSet (Id Inspection))
applyChecksFor (HashMap FilePath (HashSet (Id Inspection))
 -> [Check] -> HashMap FilePath (HashSet (Id Inspection)))
-> ([FilePath] -> HashMap FilePath (HashSet (Id Inspection)))
-> [FilePath]
-> [Check]
-> HashMap FilePath (HashSet (Id Inspection))
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [FilePath] -> HashMap FilePath (HashSet (Id Inspection))
mkDefaultChecks

{- | Modify existing 'Check's for each file using the given list of
'Check's.
-}
applyChecksFor
    :: HashMap FilePath (HashSet (Id Inspection))
    -- ^ Initial set of inspections for each file
    -> [Check]
    -- ^ List of rules
    -> HashMap FilePath (HashSet (Id Inspection))
    -- ^ Resulting set of inspections for each file
applyChecksFor :: HashMap FilePath (HashSet (Id Inspection))
-> [Check] -> HashMap FilePath (HashSet (Id Inspection))
applyChecksFor = (HashMap FilePath (HashSet (Id Inspection))
 -> Check -> HashMap FilePath (HashSet (Id Inspection)))
-> HashMap FilePath (HashSet (Id Inspection))
-> [Check]
-> HashMap FilePath (HashSet (Id Inspection))
forall b a. (b -> a -> b) -> b -> [a] -> b
forall (t :: * -> *) b a.
Foldable t =>
(b -> a -> b) -> b -> t a -> b
foldl' HashMap FilePath (HashSet (Id Inspection))
-> Check -> HashMap FilePath (HashSet (Id Inspection))
useCheck
  where
    useCheck
        :: HashMap FilePath (HashSet (Id Inspection))
        -> Check
        -> HashMap FilePath (HashSet (Id Inspection))
    useCheck :: HashMap FilePath (HashSet (Id Inspection))
-> Check -> HashMap FilePath (HashSet (Id Inspection))
useCheck HashMap FilePath (HashSet (Id Inspection))
dict Check{Scope
CheckFilter
CheckType
checkType :: Check -> CheckType
checkFilter :: Check -> CheckFilter
checkScope :: Check -> Scope
checkType :: CheckType
checkFilter :: CheckFilter
checkScope :: Scope
..} =
        (HashSet (Id Inspection) -> HashSet (Id Inspection))
-> Scope
-> HashMap FilePath (HashSet (Id Inspection))
-> HashMap FilePath (HashSet (Id Inspection))
applyForScope (CheckType
-> CheckFilter
-> HashSet (Id Inspection)
-> HashSet (Id Inspection)
applyFilter CheckType
checkType CheckFilter
checkFilter) Scope
checkScope HashMap FilePath (HashSet (Id Inspection))
dict

    applyFilter
        :: CheckType
        -> CheckFilter
        -> HashSet (Id Inspection)
        -> HashSet (Id Inspection)
    applyFilter :: CheckType
-> CheckFilter
-> HashSet (Id Inspection)
-> HashSet (Id Inspection)
applyFilter = \case
        CheckType
Include -> CheckFilter -> HashSet (Id Inspection) -> HashSet (Id Inspection)
includeFilter
        CheckType
Exclude -> CheckFilter -> HashSet (Id Inspection) -> HashSet (Id Inspection)
excludeFilter

    excludeFilter :: CheckFilter -> HashSet (Id Inspection) -> HashSet (Id Inspection)
    excludeFilter :: CheckFilter -> HashSet (Id Inspection) -> HashSet (Id Inspection)
excludeFilter CheckFilter
cFilter = (Id Inspection -> Bool)
-> HashSet (Id Inspection) -> HashSet (Id Inspection)
forall a. (a -> Bool) -> HashSet a -> HashSet a
HashSet.filter (Bool -> Bool
not (Bool -> Bool) -> (Id Inspection -> Bool) -> Id Inspection -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CheckFilter -> Id Inspection -> Bool
satisfiesFilter CheckFilter
cFilter)

    includeFilter :: CheckFilter -> HashSet (Id Inspection) -> HashSet (Id Inspection)
    includeFilter :: CheckFilter -> HashSet (Id Inspection) -> HashSet (Id Inspection)
includeFilter CheckFilter
cFilter HashSet (Id Inspection)
ins = case CheckFilter
cFilter of
        CheckInspection Id Inspection
iId -> Id Inspection -> HashSet (Id Inspection) -> HashSet (Id Inspection)
forall a. (Eq a, Hashable a) => a -> HashSet a -> HashSet a
HashSet.insert Id Inspection
iId HashSet (Id Inspection)
ins
        CheckSeverity Severity
sev ->
            let sevInspections :: [Inspection]
sevInspections = (Inspection -> Bool) -> [Inspection] -> [Inspection]
forall a. (a -> Bool) -> [a] -> [a]
filter ((Severity -> Severity -> Bool
forall a. Eq a => a -> a -> Bool
== Severity
sev) (Severity -> Bool)
-> (Inspection -> Severity) -> Inspection -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Inspection -> Severity
inspectionSeverity) [Inspection]
inspections
            in [Id Inspection] -> HashSet (Id Inspection)
forall a. (Eq a, Hashable a) => [a] -> HashSet a
HashSet.fromList ((Inspection -> Id Inspection) -> [Inspection] -> [Id Inspection]
forall a b. (a -> b) -> [a] -> [b]
map Inspection -> Id Inspection
inspectionId [Inspection]
sevInspections) HashSet (Id Inspection)
-> HashSet (Id Inspection) -> HashSet (Id Inspection)
forall a. Semigroup a => a -> a -> a
<> HashSet (Id Inspection)
ins
        CheckCategory Category
cat ->
            let catInspections :: [Inspection]
catInspections = (Inspection -> Bool) -> [Inspection] -> [Inspection]
forall a. (a -> Bool) -> [a] -> [a]
filter (Category -> NonEmpty Category -> Bool
forall (f :: * -> *) a.
(Foldable f, DisallowElem f, Eq a) =>
a -> f a -> Bool
elem Category
cat (NonEmpty Category -> Bool)
-> (Inspection -> NonEmpty Category) -> Inspection -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Inspection -> NonEmpty Category
inspectionCategory) [Inspection]
inspections
            in [Id Inspection] -> HashSet (Id Inspection)
forall a. (Eq a, Hashable a) => [a] -> HashSet a
HashSet.fromList ((Inspection -> Id Inspection) -> [Inspection] -> [Id Inspection]
forall a b. (a -> b) -> [a] -> [b]
map Inspection -> Id Inspection
inspectionId [Inspection]
catInspections) HashSet (Id Inspection)
-> HashSet (Id Inspection) -> HashSet (Id Inspection)
forall a. Semigroup a => a -> a -> a
<> HashSet (Id Inspection)
ins
        CheckFilter
CheckAll -> HashSet (Id Inspection)
inspectionsIds HashSet (Id Inspection)
-> HashSet (Id Inspection) -> HashSet (Id Inspection)
forall a. Semigroup a => a -> a -> a
<> HashSet (Id Inspection)
ins

    -- Returns 'True' if the given inspection satisfies 'CheckFilter'
    satisfiesFilter :: CheckFilter -> Id Inspection -> Bool
    satisfiesFilter :: CheckFilter -> Id Inspection -> Bool
satisfiesFilter CheckFilter
cFilter Id Inspection
iId = case Id Inspection -> Maybe Inspection
lookupInspectionById Id Inspection
iId of
        -- TODO: rewrite more efficiently after using GHC-8.10
        Maybe Inspection
Nothing -> Bool
False  -- no such ID => doesn't satisfy
        Just Inspection{[Text]
NonEmpty Category
Text
Id Inspection
Severity
InspectionAnalysis
inspectionSeverity :: Inspection -> Severity
inspectionId :: Inspection -> Id Inspection
inspectionCategory :: Inspection -> NonEmpty Category
inspectionId :: Id Inspection
inspectionName :: Text
inspectionDescription :: Text
inspectionSolution :: [Text]
inspectionCategory :: NonEmpty Category
inspectionSeverity :: Severity
inspectionAnalysis :: InspectionAnalysis
inspectionName :: Inspection -> Text
inspectionDescription :: Inspection -> Text
inspectionSolution :: Inspection -> [Text]
inspectionAnalysis :: Inspection -> InspectionAnalysis
..} -> case CheckFilter
cFilter of
            CheckInspection Id Inspection
checkId -> Id Inspection
iId Id Inspection -> Id Inspection -> Bool
forall a. Eq a => a -> a -> Bool
== Id Inspection
checkId
            CheckSeverity Severity
sev       -> Severity
sev Severity -> Severity -> Bool
forall a. Eq a => a -> a -> Bool
== Severity
inspectionSeverity
            CheckCategory Category
cat       -> Category
cat Category -> NonEmpty Category -> Bool
forall (f :: * -> *) a.
(Foldable f, DisallowElem f, Eq a) =>
a -> f a -> Bool
`elem` NonEmpty Category
inspectionCategory
            CheckFilter
CheckAll                -> Bool
True

    applyForScope
        :: (HashSet (Id Inspection) -> HashSet (Id Inspection))
        -> Scope
        -> HashMap FilePath (HashSet (Id Inspection))
        -> HashMap FilePath (HashSet (Id Inspection))
    applyForScope :: (HashSet (Id Inspection) -> HashSet (Id Inspection))
-> Scope
-> HashMap FilePath (HashSet (Id Inspection))
-> HashMap FilePath (HashSet (Id Inspection))
applyForScope HashSet (Id Inspection) -> HashSet (Id Inspection)
f Scope
cScope HashMap FilePath (HashSet (Id Inspection))
hm = case Scope
cScope of
        ScopeFile FilePath
path -> (HashSet (Id Inspection) -> HashSet (Id Inspection))
-> FilePath
-> HashMap FilePath (HashSet (Id Inspection))
-> HashMap FilePath (HashSet (Id Inspection))
forall k v.
(Eq k, Hashable k) =>
(v -> v) -> k -> HashMap k v -> HashMap k v
HashMap.adjust HashSet (Id Inspection) -> HashSet (Id Inspection)
f FilePath
path HashMap FilePath (HashSet (Id Inspection))
hm
        ScopeDirectory FilePath
dir -> (FilePath -> HashSet (Id Inspection) -> HashSet (Id Inspection))
-> HashMap FilePath (HashSet (Id Inspection))
-> HashMap FilePath (HashSet (Id Inspection))
forall k v1 v2. (k -> v1 -> v2) -> HashMap k v1 -> HashMap k v2
HashMap.mapWithKey
            (\FilePath
path -> if FilePath -> FilePath -> Bool
isInDir FilePath
dir FilePath
path then HashSet (Id Inspection) -> HashSet (Id Inspection)
f else HashSet (Id Inspection) -> HashSet (Id Inspection)
forall a. a -> a
id)
            HashMap FilePath (HashSet (Id Inspection))
hm
        Scope
ScopeAll -> HashSet (Id Inspection) -> HashSet (Id Inspection)
f (HashSet (Id Inspection) -> HashSet (Id Inspection))
-> HashMap FilePath (HashSet (Id Inspection))
-> HashMap FilePath (HashSet (Id Inspection))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> HashMap FilePath (HashSet (Id Inspection))
hm

isInDir :: FilePath -> FilePath -> Bool
isInDir :: FilePath -> FilePath -> Bool
isInDir FilePath
dir FilePath
path = FilePath
dir FilePath -> FilePath -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isPrefixOf` FilePath
path

{- $applyConfig

The 'applyConfig' function transforms the list of rules defined in the
'Config' (either via TOML or CLI) to get the list of 'Inspection's for
each module.

By default, @stan@ runs all 'Inspection's for all modules in the
Haskell project and outputs all 'Observation's it finds. Using
'Config', you can adjust the default setting using your preferences.

=== Algorithm

The algorithm for figuring out the resulting set of 'Inspection's per
module applies each 'Check' one-by-one in order of their appearance.

When introducing a new 'Check' in the config, you must always specify
three key-value pairs:

1. 'CheckType' — control inclusion and exclusion criteria

    * 'Include'

        @
        type = \"Include\"
        @

    * 'Exclude'

        @
        type = \"Exclude\"
        @

2. 'CheckFilter' — how to filter inspections

    * 'CheckInspection': by specific 'Inspection' 'Id'

        @
        id = "STAN-0001"
        @

    * 'CheckSeverity': by specific 'Severity'

        @
        severity = \"Warning\"
        @

    * 'CheckCategory': by specific 'Category'

        @
        category = \"Partial\"
        @

    * 'CheckAll': applied to all 'Inspection's

        @
        filter = "all"
        @

3. 'Scope' — where to apply check

    * 'ScopeFile': only to the specific file

        @
        file = "src\/MyModule.hs"
        @

    * 'ScopeDirectory': to all files in the specified directory

        @
        directory = "text\/"
        @

    * 'ScopeAll': to all files

        @
        scope = "all"
        @

The algorithm doesn't remove any files or inspections from the
consideration completely. So, for example, if you exclude all
inspections in a specific file, new inspections can be added for this
file later by the follow up rules.

However, if you want to completely remove some files or directory from
analysis, you can use the @remove@ key:

@
[[remove]]
file = "src\/Autogenerated.hs"
@

=== Common examples

This section contains examples of custom configuration (in TOML) for
common cases.

1. Exclude all 'Inspection's.

    @
    [[check]]
    type   = \"Exclude\"
    filter = "all"
    scope  = "all"
    @

2. Exclude all 'Inspection's only for specific file.

    @
    [[check]]
    type = \"Exclude\"
    filter = "all"
    file = "src/MyModule.hs"
    @

3. Exclude a specific 'Inspection' in all files:

    @
    [[check]]
    type = \"Exclude\"
    id = "STAN-0001"
    scope = "all"
    @

4. Exclude all 'Inspection's for specific file except 'Inspection's
that have a category @Partial@.

    @
    # exclude all inspections for a file
    [[check]]
    type = \"Exclude\"
    filter = "all"
    file = "src/MyModule.hs"

    # return back only required inspections
    [[check]]
    type = \"Include\"
    category = \"Partial\"
    file = "src/MyModule.hs"
    @

5. Keep 'Inspection's only with the category @Partial@ for all files
except a single one.

    @
    # exclude all inspections
    [[check]]
    type   = \"Exclude\"
    filter = "all"
    scope  = "all"

    # return back inspections with the category Partial
    [[check]]
    type = \"Include\"
    category = \"Partial\"
    scope = "all"

    # finally, disable all inspections for a specific file
    [[check]]
    type = \"Exclude\"
    filter = "all"
    file = "src/MyModule.hs"
    @
-}