oglyad-0.1.1.0: src/Oglyad/Ignore.hs
-- | silencing findings that are known and intentional.
--
-- two sources, both matched by check name:
--
-- * an ignore file (--ignore-file), where every entry names a package by
-- monorepo path or package name and must give a reason
-- * an `oglyad_ignore = "check, other-check"` label in a package's
-- anda.hcl, so the exception lives next to the package
--
-- ignored findings don't count toward the exit code. ignore-file entries that
-- no longer match anything are reported, so stale exceptions get cleaned up
module Oglyad.Ignore
( IgnoreRule (..),
Target (..),
IgnoreFile (..),
parseIgnoreFile,
Applied (..),
applyIgnores,
)
where
import Data.Aeson (FromJSON (..), Value, eitherDecodeStrict, withObject, (.:), (.:?))
import Data.Aeson.Types (Parser)
import Data.ByteString (ByteString)
import Data.List (partition)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Oglyad.Types
-- | what an ignore rule points at
data Target
= -- | a monorepo directory, e.g. anda/langs/zig/master-bootstrap
ByPath Text
| -- | every directory building this package name
ByName Text
deriving stock (Eq, Show)
data IgnoreRule = IgnoreRule
{ irCheck :: Text,
irTarget :: Target,
irReason :: Text
}
deriving stock (Eq, Show)
newtype IgnoreFile = IgnoreFile [IgnoreRule]
deriving stock (Eq, Show)
instance FromJSON IgnoreFile where
parseJSON = withObject "ignore file" $ \o -> IgnoreFile <$> (o .: "ignore" >>= traverse rule)
where
rule :: Value -> Parser IgnoreRule
rule = withObject "ignore entry" $ \o -> do
check <- o .: "check"
path <- o .:? "path"
name <- o .:? "package"
reason <- o .: "reason"
target <- case (path, name) of
(Just p, Nothing) -> pure (ByPath p)
(Nothing, Just n) -> pure (ByName n)
_ -> fail ("entry for " <> T.unpack check <> " needs exactly one of \"path\" or \"package\"")
if T.null (T.strip reason)
then fail ("entry for " <> T.unpack check <> " has an empty reason. say why it's ok")
else pure (IgnoreRule check target reason)
-- | parse and validate an ignore file against the known check names
parseIgnoreFile :: [Text] -> ByteString -> Either String IgnoreFile
parseIgnoreFile known raw = do
f@(IgnoreFile rules) <- eitherDecodeStrict raw
case [irCheck r | r <- rules, irCheck r `notElem` known] of
[] -> Right f
bad -> Left ("unknown checks in ignore file: " <> T.unpack (T.intercalate ", " bad))
data Applied = Applied
{ -- | findings that still count
apKept :: [Finding],
-- | findings an ignore matched
apIgnored :: [Finding],
-- | problems with the ignores themselves: unused file entries, unknown
-- check names in anda.hcl labels
apProblems :: [Finding]
}
-- | split findings into kept and ignored. known is every check name, ran is
-- the ones that ran this time (an entry for a check that didn't run isn't
-- unused, just idle). labels maps a monorepo path to the check names its
-- anda.hcl ignores
applyIgnores :: [Text] -> [Text] -> IgnoreFile -> Map FilePath [Text] -> [Finding] -> Applied
applyIgnores known ran (IgnoreFile rules) labels findings =
Applied
{ apKept = kept,
apIgnored = ignored,
apProblems = unused <> badLabels
}
where
(ignored, kept) = partition isIgnored findings
isIgnored f = byLabel f || any (`matches` f) rules
byLabel f = case fPath f of
Just path -> fCheck f `elem` Map.findWithDefault [] (T.unpack path) labels
Nothing -> False
matches r f =
irCheck r == fCheck f && case irTarget r of
ByPath p -> fPath f == Just p
ByName n -> fPackage f == n
unused =
[ Finding
{ fCheck = "unused-ignore",
fPackage = describe (irTarget r),
fPath = case irTarget r of ByPath p -> Just p; ByName _ -> Nothing,
fSeverity = Info,
fMessage = "ignore file entry for " <> irCheck r <> " matches nothing anymore, remove it"
}
| r <- rules,
irCheck r `elem` ran,
not (any (matches r) findings)
]
badLabels =
[ Finding
{ fCheck = "unused-ignore",
fPackage = T.pack path,
fPath = Just (T.pack path),
fSeverity = Warn,
fMessage = "oglyad_ignore in anda.hcl names unknown check " <> c
}
| (path, cs) <- Map.toList labels,
c <- cs,
c `notElem` known
]
describe = \case
ByPath p -> p
ByName n -> n