use std::cmp::Ordering;
use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::str::FromStr;
use std::sync::LazyLock;
use camino::{Utf8Path, Utf8PathBuf};
use indexmap::IndexSet;
use owo_colors::OwoColorize;
use pkgcraft::bash::Node;
use pkgcraft::cli::TriState;
use pkgcraft::dep::{Cpn, Cpv};
use pkgcraft::repo::{EbuildRepo, Repository};
use pkgcraft::restrict::{Restrict, Restriction, Scope};
use regex::Regex;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumIter, EnumString};
use crate::Error;
use crate::check::{Check, CheckKind, Context};
use crate::scan::ScannerRun;
#[derive(
Display, EnumIter, EnumString, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone,
)]
#[strum(serialize_all = "kebab-case")]
pub enum ReportLevel {
Critical,
Error,
Warning,
Style,
Info,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum RangeOrValue<T: Eq + Copy> {
Value(T),
RangeOp(RangeOp<T>),
}
impl<T: PartialEq + Eq + Copy> RangeOrValue<T>
where
T: PartialOrd,
{
fn contains(&self, value: &T) -> bool {
match self {
Self::Value(x) => x == value,
Self::RangeOp(range) => range.contains(value),
}
}
}
impl<T> FromStr for RangeOrValue<T>
where
T: FromStr + Eq + Copy,
T::Err: fmt::Display + fmt::Debug,
{
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(value) = s.parse() {
Ok(RangeOrValue::Value(value))
} else if let Ok(value) = s.parse() {
Ok(RangeOrValue::RangeOp(value))
} else {
Err(Error::InvalidValue(format!("invalid range or value: {s}")))
}
}
}
impl<T: fmt::Display + Eq + Copy> fmt::Display for RangeOrValue<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Value(value) => value.fmt(f),
Self::RangeOp(value) => value.fmt(f),
}
}
}
impl From<ReportLevel> for RangeOrValue<ReportLevel> {
fn from(value: ReportLevel) -> Self {
Self::Value(value)
}
}
impl From<Scope> for RangeOrValue<Scope> {
fn from(value: Scope) -> Self {
Self::Value(value)
}
}
static RANGE_OP_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(?<op>[<>]=?|!?=)(?<value>.+)$").unwrap());
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum RangeOp<T: Eq + Copy> {
Less(T),
LessOrEqual(T),
Equal(T),
NotEqual(T),
GreaterOrEqual(T),
Greater(T),
}
impl<T: PartialEq + Eq + Copy> RangeOp<T>
where
T: PartialOrd,
{
fn contains(&self, value: &T) -> bool {
match self {
Self::Less(x) => value < x,
Self::LessOrEqual(x) => value <= x,
Self::Equal(x) => value == x,
Self::NotEqual(x) => value != x,
Self::GreaterOrEqual(x) => value >= x,
Self::Greater(x) => value > x,
}
}
}
impl<T> FromStr for RangeOp<T>
where
T: FromStr + Eq + Copy,
T::Err: fmt::Display + fmt::Debug,
{
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(caps) = RANGE_OP_RE.captures(s) {
let op = caps.name("op").map_or("", |m| m.as_str());
let value = caps.name("value").map_or("", |m| m.as_str());
let value = value.parse().map_err(|e| {
Error::InvalidValue(format!("invalid range value: {value}: {e}"))
})?;
match op {
"<" => Ok(Self::Less(value)),
"<=" => Ok(Self::LessOrEqual(value)),
"=" => Ok(Self::Equal(value)),
"!=" => Ok(Self::NotEqual(value)),
">=" => Ok(Self::GreaterOrEqual(value)),
">" => Ok(Self::Greater(value)),
_ => unreachable!("invalid RangeOp regex"),
}
} else {
Err(Error::InvalidValue(format!("invalid range op: {s}")))
}
}
}
impl<T: fmt::Display + Eq + Copy> fmt::Display for RangeOp<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Less(value) => write!(f, "<{value}"),
Self::LessOrEqual(value) => write!(f, "<={value}"),
Self::Equal(value) => write!(f, "={value}"),
Self::NotEqual(value) => write!(f, "!={value}"),
Self::GreaterOrEqual(value) => write!(f, ">={value}"),
Self::Greater(value) => write!(f, ">{value}"),
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum ReportSet {
All,
Finalize,
Check(Check),
Context(Context),
Level(RangeOrValue<ReportLevel>),
Report(ReportKind),
Scope(RangeOrValue<Scope>),
}
impl From<Check> for ReportSet {
fn from(value: Check) -> Self {
Self::Check(value)
}
}
impl From<CheckKind> for ReportSet {
fn from(value: CheckKind) -> Self {
Self::Check(value.into())
}
}
impl From<Context> for ReportSet {
fn from(value: Context) -> Self {
Self::Context(value)
}
}
impl From<ReportLevel> for ReportSet {
fn from(value: ReportLevel) -> Self {
Self::Level(value.into())
}
}
impl From<ReportKind> for ReportSet {
fn from(value: ReportKind) -> Self {
Self::Report(value)
}
}
impl From<Scope> for ReportSet {
fn from(value: Scope) -> Self {
Self::Scope(value.into())
}
}
impl FromStr for ReportSet {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(val) = s.strip_prefix('@') {
match val {
"all" => Ok(Self::All),
"finalize" => Ok(Self::Finalize),
_ => val
.parse()
.map(Self::Check)
.or_else(|_| val.parse().map(Self::Context))
.or_else(|_| val.parse().map(Self::Level))
.or_else(|_| val.parse().map(Self::Scope))
.map_err(|_| Error::InvalidValue(format!("invalid report set: {val}"))),
}
} else {
s.parse()
.map(Self::Report)
.map_err(|_| Error::InvalidValue(format!("invalid report: {s}")))
}
}
}
impl fmt::Display for ReportSet {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::All => write!(f, "@all"),
Self::Finalize => write!(f, "@finalize"),
Self::Check(check) => write!(f, "@{check}"),
Self::Context(context) => write!(f, "@{context}"),
Self::Level(level) => write!(f, "@{level}"),
Self::Report(report) => write!(f, "{report}"),
Self::Scope(scope) => write!(f, "@{scope}"),
}
}
}
impl ReportSet {
fn selected(&self) -> bool {
matches!(self, Self::Report(_) | Self::Check(_))
}
pub(crate) fn expand<'a>(
self,
default: &'a IndexSet<ReportKind>,
supported: &'a IndexSet<ReportKind>,
) -> Box<dyn Iterator<Item = ReportKind> + 'a> {
match self {
Self::All => Box::new(supported.iter().copied()),
Self::Finalize => Box::new(
default
.iter()
.filter(|r| r.finish_check(Scope::Repo))
.copied(),
),
Self::Check(check) => Box::new(check.reports.iter().copied()),
Self::Context(context) => Box::new(
Check::iter_report(supported)
.filter(move |x| x.context.contains(&context))
.flat_map(|x| x.reports)
.copied(),
),
Self::Level(range) => Box::new(
default
.iter()
.filter(move |r| range.contains(&r.level()))
.copied(),
),
Self::Report(kind) => Box::new([kind].into_iter()),
Self::Scope(range) => Box::new(
default
.iter()
.filter(move |r| range.contains(&r.scope()))
.copied(),
),
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct ReportTarget(TriState<ReportSet>);
impl ReportTarget {
pub fn collapse<'a, I>(
targets: I,
defaults: &IndexSet<ReportKind>,
supported: &IndexSet<ReportKind>,
) -> crate::Result<(IndexSet<ReportKind>, IndexSet<ReportKind>)>
where
I: IntoIterator<Item = &'a Self>,
{
let mut targets: IndexSet<_> = targets.into_iter().copied().map(|x| x.0).collect();
targets.sort_unstable();
let mut enabled = if let Some(TriState::Set(_)) = targets.first() {
Default::default()
} else {
defaults.clone()
};
let mut selected = IndexSet::new();
for target in targets {
match target {
TriState::Set(set) | TriState::Add(set) => {
for r in set.expand(defaults, supported) {
enabled.insert(r);
if set.selected() || supported.contains(&r) {
selected.insert(r);
}
}
}
TriState::Remove(set) => {
for r in set.expand(defaults, supported) {
enabled.swap_remove(&r);
}
}
};
}
if enabled.is_empty() {
Err(Error::InvalidValue("no reports enabled".to_string()))
} else {
enabled.sort_unstable();
selected.sort_unstable();
Ok((enabled, selected))
}
}
}
impl<T: Into<ReportSet>> From<T> for ReportTarget {
fn from(value: T) -> Self {
Self(TriState::Set(value.into()))
}
}
impl FromStr for ReportTarget {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(Self)
}
}
#[derive(
Serialize,
Deserialize,
AsRefStr,
Display,
EnumIter,
EnumString,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Copy,
Clone,
)]
pub enum ReportKind {
ArchesUnused,
Builtin,
CommandDieUnneeded,
CommandScopeInvalid,
DependencyDeprecated,
DependencyInvalid,
DependencyRevisionMissing,
DependencySlotMissing,
EapiBanned,
EapiDeprecated,
EapiFormat,
EapiStale,
EapiUnstable,
EapiUnused,
EbuildNameInvalid,
EbuildVersionsEqual,
EclassUnused,
FileUnknown,
FilesUnused,
HeaderInvalid,
HomepageInvalid,
IgnoreInvalid,
IgnoreUnused,
IuseInvalid,
KeywordsDropped,
KeywordsLive,
KeywordsOverlapping,
KeywordsUnsorted,
LicenseDeprecated,
LicenseInvalid,
LicensesUnused,
LiveOnly,
ManifestCollide,
ManifestConflict,
ManifestInvalid,
MetadataError,
MirrorsUnused,
Optfeature,
PackageDeprecatedUnused,
PackageOverride,
PhaseCall,
PropertiesInvalid,
PythonUpdate,
RepoCategoriesUnused,
RepoCategoryEmpty,
RepoPackageEmpty,
RestrictInvalid,
RestrictMissing,
RubyUpdate,
UnstableOnly,
UriInvalid,
UseGlobalUnused,
UseLocalDescMissing,
UseLocalGlobal,
UseLocalUnsorted,
UseLocalUnused,
VariableOrder,
VariableScopeInvalid,
WhitespaceInvalid,
WhitespaceUnneeded,
}
impl ReportKind {
pub(crate) fn version<T: Into<Cpv>>(self, value: T) -> ReportBuilder {
ReportBuilder(Report {
kind: self,
scope: ReportScope::Version(value.into(), None),
message: Default::default(),
})
}
pub(crate) fn package<T>(self, value: T) -> ReportBuilder
where
T: TryInto<Cpn>,
T::Error: fmt::Display,
{
let cpn = value
.try_into()
.unwrap_or_else(|e| unreachable!("can't convert value to Cpn: {e}"));
ReportBuilder(Report {
kind: self,
scope: ReportScope::Package(cpn),
message: Default::default(),
})
}
pub(crate) fn category<S: fmt::Display>(self, value: S) -> ReportBuilder {
ReportBuilder(Report {
kind: self,
scope: ReportScope::Category(value.to_string()),
message: Default::default(),
})
}
pub(crate) fn repo<R: Repository>(self, repo: R) -> ReportBuilder {
ReportBuilder(Report {
kind: self,
scope: ReportScope::Repo(repo.name().to_string()),
message: Default::default(),
})
}
pub(crate) fn in_scope(self, scope: ReportScope) -> ReportBuilder {
ReportBuilder(Report {
kind: self,
scope,
message: Default::default(),
})
}
pub fn level(&self) -> ReportLevel {
use ReportLevel::*;
match self {
Self::ArchesUnused => Warning,
Self::Builtin => Error,
Self::CommandDieUnneeded => Warning,
Self::CommandScopeInvalid => Error,
Self::DependencyDeprecated => Warning,
Self::DependencyInvalid => Error,
Self::DependencyRevisionMissing => Warning,
Self::DependencySlotMissing => Warning,
Self::EapiBanned => Error,
Self::EapiDeprecated => Warning,
Self::EapiFormat => Style,
Self::EapiStale => Warning,
Self::EapiUnstable => Error,
Self::EapiUnused => Warning,
Self::EbuildNameInvalid => Error,
Self::EbuildVersionsEqual => Error,
Self::EclassUnused => Warning,
Self::FileUnknown => Error,
Self::FilesUnused => Warning,
Self::HeaderInvalid => Error,
Self::HomepageInvalid => Error,
Self::IgnoreInvalid => Warning,
Self::IgnoreUnused => Warning,
Self::IuseInvalid => Error,
Self::KeywordsDropped => Warning,
Self::KeywordsLive => Warning,
Self::KeywordsOverlapping => Error,
Self::KeywordsUnsorted => Style,
Self::LicenseDeprecated => Warning,
Self::LicenseInvalid => Error,
Self::LicensesUnused => Warning,
Self::LiveOnly => Warning,
Self::ManifestInvalid => Error,
Self::ManifestCollide => Warning,
Self::ManifestConflict => Error,
Self::MetadataError => Critical,
Self::MirrorsUnused => Warning,
Self::Optfeature => Warning,
Self::PackageDeprecatedUnused => Warning,
Self::PackageOverride => Warning,
Self::PhaseCall => Error,
Self::PropertiesInvalid => Error,
Self::PythonUpdate => Info,
Self::RepoCategoriesUnused => Warning,
Self::RepoCategoryEmpty => Warning,
Self::RepoPackageEmpty => Warning,
Self::RestrictInvalid => Error,
Self::RestrictMissing => Warning,
Self::RubyUpdate => Info,
Self::UnstableOnly => Info,
Self::UriInvalid => Error,
Self::UseGlobalUnused => Warning,
Self::UseLocalDescMissing => Error,
Self::UseLocalGlobal => Warning,
Self::UseLocalUnsorted => Style,
Self::UseLocalUnused => Warning,
Self::VariableOrder => Style,
Self::VariableScopeInvalid => Error,
Self::WhitespaceInvalid => Warning,
Self::WhitespaceUnneeded => Style,
}
}
pub fn colorize(&self) -> String {
let s = self.as_ref();
match self.level() {
ReportLevel::Critical => s.red().to_string(),
ReportLevel::Error => s.fg_rgb::<255, 140, 0>().to_string(),
ReportLevel::Warning => s.yellow().to_string(),
ReportLevel::Style => s.cyan().to_string(),
ReportLevel::Info => s.green().to_string(),
}
}
pub(crate) fn scope(&self) -> Scope {
match self {
Self::ArchesUnused => Scope::Repo,
Self::Builtin => Scope::Version,
Self::CommandDieUnneeded => Scope::Version,
Self::CommandScopeInvalid => Scope::Version,
Self::DependencyDeprecated => Scope::Version,
Self::DependencyInvalid => Scope::Version,
Self::DependencyRevisionMissing => Scope::Version,
Self::DependencySlotMissing => Scope::Version,
Self::EapiBanned => Scope::Version,
Self::EapiDeprecated => Scope::Version,
Self::EapiFormat => Scope::Version,
Self::EapiStale => Scope::Version,
Self::EapiUnstable => Scope::Version,
Self::EapiUnused => Scope::Repo,
Self::EbuildNameInvalid => Scope::Package,
Self::EbuildVersionsEqual => Scope::Package,
Self::EclassUnused => Scope::Repo,
Self::FileUnknown => Scope::Version,
Self::FilesUnused => Scope::Package,
Self::HeaderInvalid => Scope::Version,
Self::HomepageInvalid => Scope::Version,
Self::IgnoreInvalid => Scope::Version,
Self::IgnoreUnused => Scope::Version,
Self::IuseInvalid => Scope::Version,
Self::KeywordsDropped => Scope::Version,
Self::KeywordsLive => Scope::Version,
Self::KeywordsOverlapping => Scope::Version,
Self::KeywordsUnsorted => Scope::Version,
Self::LicenseDeprecated => Scope::Version,
Self::LicenseInvalid => Scope::Version,
Self::LicensesUnused => Scope::Repo,
Self::LiveOnly => Scope::Package,
Self::ManifestInvalid => Scope::Package,
Self::ManifestCollide => Scope::Package,
Self::ManifestConflict => Scope::Category,
Self::MetadataError => Scope::Version,
Self::MirrorsUnused => Scope::Repo,
Self::Optfeature => Scope::Version,
Self::PackageDeprecatedUnused => Scope::Repo,
Self::PackageOverride => Scope::Package,
Self::PhaseCall => Scope::Version,
Self::PropertiesInvalid => Scope::Version,
Self::PythonUpdate => Scope::Version,
Self::RepoCategoriesUnused => Scope::Repo,
Self::RepoCategoryEmpty => Scope::Repo,
Self::RepoPackageEmpty => Scope::Package,
Self::RestrictInvalid => Scope::Version,
Self::RestrictMissing => Scope::Version,
Self::RubyUpdate => Scope::Version,
Self::UnstableOnly => Scope::Package,
Self::UriInvalid => Scope::Version,
Self::UseGlobalUnused => Scope::Repo,
Self::UseLocalDescMissing => Scope::Package,
Self::UseLocalGlobal => Scope::Package,
Self::UseLocalUnsorted => Scope::Package,
Self::UseLocalUnused => Scope::Package,
Self::VariableOrder => Scope::Version,
Self::VariableScopeInvalid => Scope::Version,
Self::WhitespaceInvalid => Scope::Version,
Self::WhitespaceUnneeded => Scope::Version,
}
}
pub(crate) fn scoped(&self, scope: Scope) -> Option<Scope> {
if self.scope() > scope {
Some(self.scope())
} else {
None
}
}
pub(crate) fn finish_check(&self, scope: Scope) -> bool {
match self {
Self::ArchesUnused => scope == Scope::Repo,
Self::EapiUnused => scope == Scope::Repo,
Self::EclassUnused => scope == Scope::Repo,
Self::LicensesUnused => scope == Scope::Repo,
Self::IgnoreUnused => scope == Scope::Repo,
Self::ManifestCollide => scope == Scope::Repo,
Self::ManifestConflict => scope == Scope::Repo,
Self::MirrorsUnused => scope == Scope::Repo,
Self::PackageDeprecatedUnused => scope == Scope::Repo,
Self::RepoCategoryEmpty => scope == Scope::Repo,
Self::UseGlobalUnused => scope == Scope::Repo,
_ => false,
}
}
pub(crate) fn finish_target(&self) -> bool {
matches!(self, Self::IgnoreUnused)
}
pub fn defaults(repo: &EbuildRepo) -> IndexSet<Self> {
let mut set: IndexSet<_> = Check::iter_default(repo)
.flat_map(|x| x.reports)
.copied()
.collect();
set.sort_unstable();
set
}
pub fn supported<T: Into<Scope>>(repo: &EbuildRepo, value: T) -> IndexSet<Self> {
let scope = value.into();
let mut set: IndexSet<_> = Check::iter_supported(repo, scope)
.flat_map(|c| c.reports)
.filter(|r| scope >= r.scope())
.copied()
.collect();
set.sort_unstable();
set
}
}
pub(crate) struct ReportBuilder(Report);
impl ReportBuilder {
pub(crate) fn message<S>(mut self, value: S) -> Self
where
S: fmt::Display,
{
self.0.message = Some(value.to_string());
self
}
pub(crate) fn location<L>(mut self, value: L) -> Self
where
L: Into<Location>,
{
if let ReportScope::Version(_, location @ None) = &mut self.0.scope {
*location = Some(value.into());
} else {
panic!("invalid report scope: {:?}", self.0.scope);
}
self
}
pub(crate) fn report(self, run: &ScannerRun) {
run.report(self.0)
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
pub struct Location {
pub line: usize,
pub column: usize,
}
impl fmt::Debug for Location {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{self}")
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "line {}", self.line)?;
if self.column > 0 {
write!(f, ", column {}", self.column)?;
}
Ok(())
}
}
impl From<usize> for Location {
fn from(value: usize) -> Self {
Self { line: value, column: 0 }
}
}
impl From<(usize, usize)> for Location {
fn from(value: (usize, usize)) -> Self {
Self { line: value.0, column: value.1 }
}
}
impl From<&Node<'_>> for Location {
fn from(value: &Node<'_>) -> Self {
Self {
line: value.start_position().row + 1,
column: value.start_position().column + 1,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
pub enum ReportScope {
Version(Cpv, Option<Location>),
Package(Cpn),
Category(String),
Repo(String),
}
impl ReportScope {
fn scope(&self) -> Scope {
match self {
Self::Version(_, _) => Scope::Version,
Self::Package(_) => Scope::Package,
Self::Category(_) => Scope::Category,
Self::Repo(_) => Scope::Repo,
}
}
pub(crate) fn to_abspath<R: Repository>(&self, repo: R) -> Utf8PathBuf {
repo.path().join(self.to_relpath())
}
pub(crate) fn to_relpath(&self) -> Utf8PathBuf {
match self {
Self::Version(cpv, _) => cpv.relpath(),
Self::Package(cpn) => cpn.to_string().into(),
Self::Category(category) => category.into(),
Self::Repo(_) => Default::default(),
}
}
}
impl Ord for ReportScope {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(Self::Repo(v1), Self::Repo(v2)) => v1.cmp(v2),
(Self::Category(v1), Self::Category(v2)) => v1.cmp(v2),
(Self::Package(v1), Self::Package(v2)) => v1.cmp(v2),
(Self::Version(v1, l1), Self::Version(v2, l2)) => {
v1.cmp(v2).then_with(|| l1.cmp(l2))
}
(Self::Version(v1, _), Self::Package(v2)) => v1
.cpn()
.cmp(v2)
.then_with(|| self.scope().cmp(&other.scope())),
(Self::Package(v1), Self::Version(v2, _)) => v1
.cmp(v2.cpn())
.then_with(|| self.scope().cmp(&other.scope())),
_ => self.scope().cmp(&other.scope()),
}
}
}
impl PartialOrd for ReportScope {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq<Scope> for ReportScope {
fn eq(&self, other: &Scope) -> bool {
self.scope() == *other
}
}
impl PartialOrd<Scope> for ReportScope {
fn partial_cmp(&self, other: &Scope) -> Option<Ordering> {
Some(self.scope().cmp(other))
}
}
impl fmt::Debug for ReportScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Version(cpv, Some(location)) => {
write!(f, "Version( {cpv}, {location:?} )")
}
Self::Version(cpv, None) => write!(f, "Version( {cpv} )"),
Self::Package(cpn) => write!(f, "Package( {cpn} )"),
Self::Category(cat) => write!(f, "Category( {cat} )"),
Self::Repo(repo) => write!(f, "Repo( {repo} )"),
}
}
}
impl fmt::Display for ReportScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Version(cpv, Some(location)) => write!(f, "{cpv}, {location}"),
Self::Version(cpv, None) => write!(f, "{cpv}"),
Self::Package(cpn) => write!(f, "{cpn}"),
Self::Category(cat) => write!(f, "{cat}/*"),
Self::Repo(repo) => write!(f, "{repo}"),
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct Report {
scope: ReportScope,
pub kind: ReportKind,
message: Option<String>,
}
impl Report {
pub fn scope(&self) -> &ReportScope {
&self.scope
}
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
pub fn level(&self) -> ReportLevel {
self.kind.level()
}
pub fn to_json(&self) -> String {
serde_json::to_string(&self).expect("failed serializing report")
}
pub fn from_json(data: &str) -> crate::Result<Self> {
serde_json::from_str(data).map_err(|e| {
Error::InvalidValue(format!("failed deserializing report JSON: {data}: {e}"))
})
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.scope, self.kind)?;
if let Some(value) = self.message() {
write!(f, ": {value}")?;
}
Ok(())
}
}
impl Restriction<&Report> for Restrict {
fn matches(&self, report: &Report) -> bool {
match &report.scope {
ReportScope::Version(cpv, _) => self.matches(cpv),
ReportScope::Package(cpn) => self.matches(cpn),
_ => false,
}
}
}
pub struct Iter<'a, R: BufRead> {
reader: R,
line: String,
reports: Option<&'a IndexSet<ReportKind>>,
restrict: Option<&'a Restrict>,
scopes: Option<&'a IndexSet<Scope>>,
}
impl<'a> Iter<'a, BufReader<File>> {
pub fn try_from_file<P: AsRef<Utf8Path>>(
path: P,
reports: Option<&'a IndexSet<ReportKind>>,
restrict: Option<&'a Restrict>,
scopes: Option<&'a IndexSet<Scope>>,
) -> crate::Result<Iter<'a, BufReader<File>>> {
let path = path.as_ref();
let file = File::open(path)
.map_err(|e| Error::InvalidValue(format!("failed loading file: {path}: {e}")))?;
Ok(Iter {
reader: BufReader::new(file),
line: String::new(),
reports,
restrict,
scopes,
})
}
}
impl<'a, R: BufRead> Iter<'a, R> {
pub fn from_reader(
reader: R,
reports: Option<&'a IndexSet<ReportKind>>,
restrict: Option<&'a Restrict>,
scopes: Option<&'a IndexSet<Scope>>,
) -> Iter<'a, R> {
Iter {
reader,
line: String::new(),
reports,
restrict,
scopes,
}
}
fn filtered(&self, report: &Report) -> bool {
if let Some(reports) = self.reports
&& !reports.contains(&report.kind)
{
return true;
}
if let Some(scopes) = self.scopes
&& !scopes.contains(&report.scope().scope())
{
return true;
}
if let Some(filter) = self.restrict
&& !filter.matches(report)
{
return true;
}
false
}
}
impl<R: BufRead> Iterator for Iter<'_, R> {
type Item = crate::Result<Report>;
fn next(&mut self) -> Option<Self::Item> {
loop {
self.line.clear();
match self.reader.read_line(&mut self.line) {
Ok(0) => return None,
Ok(_) => match Report::from_json(&self.line) {
Ok(report) => {
if !self.filtered(&report) {
return Some(Ok(report));
}
}
err => return Some(err),
},
Err(e) => {
return Some(Err(Error::InvalidValue(format!(
"failed reading line: {e}"
))));
}
}
}
}
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use pkgcraft::restrict::Scope;
use pkgcraft::test::test_data;
use pretty_assertions::assert_eq;
use strum::IntoEnumIterator;
use super::*;
use crate::check::Check;
use crate::report::ReportLevel;
use crate::test::assert_ordered_reports;
static REPORTS: &str = indoc::indoc! {r#"
{"kind":"DependencyDeprecated","scope":{"Version":["cat/pkg1-2-r3",null]},"message":"BDEPEND: cat/deprecated"}
{"kind":"EapiDeprecated","scope":{"Version":["cat/pkg1-2-r3",null]},"message":"6"}
{"kind":"WhitespaceUnneeded","scope":{"Version":["cat/pkg1-2-r3",{"line":3,"column":0}]},"message":"empty line"}
{"kind":"WhitespaceInvalid","scope":{"Version":["cat/pkg1-2-r3",{"line":6,"column":0}]},"message":"missing ending newline"}
{"kind":"UnstableOnly","scope":{"Package":"cat/pkg1"},"message":"arch1"}
{"kind":"UnstableOnly","scope":{"Package":"cat/pkg1"},"message":"arch2"}
{"kind":"EapiDeprecated","scope":{"Version":["cat/pkg2-1-r2",null]},"message":"6"}
{"kind":"RepoCategoryEmpty","scope":{"Category":"cat1"},"message":null}
{"kind":"RepoCategoryEmpty","scope":{"Category":"cat2"},"message":null}
{"kind":"LicensesUnused","scope":{"Repo":"repo1"},"message":"unused"}
{"kind":"LicensesUnused","scope":{"Repo":"repo2"},"message":"unused"}
"#};
#[test]
fn kind() {
let kinds: Vec<_> = ReportKind::iter().collect();
let ordered: Vec<_> = ReportKind::iter().map(|x| x.to_string()).sorted().collect();
let ordered: Vec<_> = ordered.iter().map(|s| s.parse().unwrap()).collect();
assert_eq!(&kinds, &ordered, "unordered ReportKind variants");
}
#[test]
fn cmp() {
let expected: Vec<_> = REPORTS
.lines()
.map(Report::from_json)
.try_collect()
.unwrap();
for (a, b) in expected.iter().tuples() {
assert!(a < b);
assert!(a.scope() <= b.scope());
}
let mut reports = expected.clone();
reports.reverse();
reports.sort();
assert_ordered_reports!(expected, reports);
}
#[test]
fn display_and_debug() {
for report in REPORTS.lines().filter_map(|s| Report::from_json(s).ok()) {
let kind = report.kind.to_string();
let scope = report.scope().to_string();
let s = report.to_string();
assert!(s.contains(&kind));
assert!(s.contains(&scope));
let s = format!("{report:?}");
assert!(s.contains(&kind));
}
}
#[test]
fn builder() {
let result = std::panic::catch_unwind(|| {
let cpv = Cpv::try_new("cat/pkg-1").unwrap();
ReportKind::Builtin.version(cpv).location(1)
});
assert!(result.is_ok());
let result = std::panic::catch_unwind(|| {
let cpn = Cpn::try_new("cat/pkg").unwrap();
ReportKind::LiveOnly.package(cpn).location(1)
});
assert!(result.is_err());
}
#[test]
fn report_target() {
let data = test_data();
let repo = data.ebuild_repo("gentoo").unwrap();
let defaults = ReportKind::defaults(repo);
let supported = ReportKind::supported(repo, Scope::Repo);
let (enabled, selected) = ReportTarget::collapse([], &defaults, &supported).unwrap();
assert!(selected.is_empty());
let checks: IndexSet<_> = Check::iter_report(&enabled).collect();
assert!(checks.contains(&CheckKind::Header));
let repo = data.ebuild_repo("qa-primary").unwrap();
let defaults = ReportKind::defaults(repo);
let supported = ReportKind::supported(repo, Scope::Repo);
let (enabled, selected) = ReportTarget::collapse([], &defaults, &supported).unwrap();
assert!(selected.is_empty());
let checks: IndexSet<_> = Check::iter_report(&enabled).collect();
assert!(checks.contains(&CheckKind::Dependency));
assert!(!checks.contains(&CheckKind::UnstableOnly));
assert!(!checks.contains(&CheckKind::Header));
let report = ReportKind::HeaderInvalid;
assert_eq!(report.level(), ReportLevel::Error);
let target = ReportLevel::Error.into();
let (enabled, selected) =
ReportTarget::collapse([&target], &defaults, &supported).unwrap();
assert!(!enabled.contains(&report));
assert!(!enabled.is_empty());
assert!(selected.is_subset(&enabled));
assert!(!selected.is_empty());
}
}