use std::borrow::Borrow;
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::Not;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use std::time::Instant;
use camino::Utf8Path;
use indexmap::IndexSet;
use itertools::Itertools;
use pkgcraft::dep::{Cpn, Cpv};
use pkgcraft::pkg::ebuild::{EbuildPkg, EbuildRawPkg};
use pkgcraft::repo::{EbuildRepo, Repository};
use pkgcraft::restrict::Scope;
use pkgcraft::types::{OrderedMap, OrderedSet};
use strum::{AsRefStr, Display, EnumIter, EnumString};
use crate::Error;
use crate::report::ReportKind;
use crate::scan::ScannerRun;
use crate::source::SourceKind;
mod commands;
mod dependency;
mod dependency_slot_missing;
mod duplicates;
mod eapi_stale;
mod eapi_status;
mod ebuild_name;
mod eclass;
mod filesdir;
mod header;
mod homepage;
mod ignore;
mod iuse;
mod keywords;
mod keywords_dropped;
mod license;
mod live;
mod manifest;
mod metadata;
mod properties;
mod python_update;
mod repo_layout;
mod restrict;
mod restrict_test_missing;
mod ruby_update;
mod src_uri;
mod unstable_only;
mod use_local;
mod variable_order;
mod variables;
mod whitespace;
#[derive(
AsRefStr,
Display,
EnumIter,
EnumString,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Copy,
Clone,
)]
pub enum CheckKind {
Commands,
Dependency,
DependencySlotMissing,
Duplicates,
EapiStale,
EapiStatus,
EbuildName,
Eclass,
Filesdir,
Header,
Homepage,
Ignore,
Iuse,
Keywords,
KeywordsDropped,
License,
Live,
Manifest,
Metadata,
Properties,
PythonUpdate,
RepoLayout,
Restrict,
RestrictTestMissing,
RubyUpdate,
SrcUri,
UnstableOnly,
UseLocal,
VariableOrder,
Variables,
Whitespace,
}
impl From<CheckKind> for Check {
fn from(value: CheckKind) -> Self {
CHECKS
.get(&value)
.copied()
.unwrap_or_else(|| panic!("no registered check: {value}"))
}
}
#[derive(Debug, Copy, Clone)]
pub struct Check {
pub kind: CheckKind,
pub reports: &'static [ReportKind],
pub(crate) scope: Scope,
pub(crate) sources: &'static [SourceKind],
pub context: &'static [Context],
create: fn(&ScannerRun) -> Runner,
}
impl Check {
pub(crate) fn to_runner(self, run: &ScannerRun) -> CheckRunner {
CheckRunner {
check: self,
runner: Arc::new((self.create)(run)),
}
}
pub fn iter() -> impl Iterator<Item = Self> {
CHECKS.iter().copied()
}
pub fn iter_default(repo: &EbuildRepo) -> impl Iterator<Item = Check> {
let selected = Default::default();
Self::iter().filter(move |x| x.skipped(repo, &selected).is_none())
}
pub fn iter_supported<T: Into<Scope>>(
repo: &EbuildRepo,
value: T,
) -> impl Iterator<Item = Check> {
let scope = value.into();
let selected = Self::iter().collect();
Self::iter().filter(move |x| x.skipped(repo, &selected).is_none() && scope >= x.scope)
}
pub fn iter_report<'a, I>(reports: I) -> impl Iterator<Item = Check> + 'a
where
I: IntoIterator<Item = &'a ReportKind>,
I::IntoIter: 'a,
{
reports
.into_iter()
.filter_map(|x| REPORTS.get(x))
.flatten()
.copied()
}
pub fn iter_source(source: &SourceKind) -> impl Iterator<Item = Check> {
Self::iter().filter(move |c| c.sources.contains(source))
}
pub(crate) fn skipped(
&self,
repo: &EbuildRepo,
selected: &IndexSet<Self>,
) -> Option<Context> {
self.context.iter().copied().find(|context| {
match context {
Context::Gentoo => repo.name() == "gentoo" || selected.contains(self),
Context::GentooInherited => repo.trees().any(|x| x.name() == "gentoo"),
Context::Optional => selected.contains(self),
Context::Overlay => !repo.masters().is_empty(),
}
.not()
})
}
pub(crate) fn filtered(&self) -> bool {
self.scope != Scope::Version
|| (!self.sources.contains(&SourceKind::EbuildPkg)
&& !self.sources.contains(&SourceKind::EbuildRawPkg))
}
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 {
self.reports.iter().any(|r| r.finish_check(scope))
}
pub(crate) fn finish_target(&self) -> bool {
self.reports.iter().any(|r| r.finish_target())
}
}
impl PartialEq for Check {
fn eq(&self, other: &Self) -> bool {
self.kind == other.kind
}
}
impl Eq for Check {}
impl Hash for Check {
fn hash<H: Hasher>(&self, state: &mut H) {
self.kind.hash(state);
}
}
impl Borrow<CheckKind> for Check {
fn borrow(&self) -> &CheckKind {
&self.kind
}
}
impl Ord for Check {
fn cmp(&self, other: &Self) -> Ordering {
self.kind.cmp(&other.kind)
}
}
impl PartialOrd for Check {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Check {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.kind)
}
}
impl FromStr for Check {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let kind: CheckKind = s
.parse()
.map_err(|_| Error::InvalidValue(format!("unknown check: {s}")))?;
Ok(CHECKS.get(&kind).copied().unwrap())
}
}
impl AsRef<Utf8Path> for Check {
fn as_ref(&self) -> &Utf8Path {
Utf8Path::new(self.kind.as_ref())
}
}
inventory::collect!(Check);
static CHECKS: LazyLock<IndexSet<Check>> = LazyLock::new(|| {
let mut checks = IndexSet::new();
for check in inventory::iter::<Check>().copied().sorted() {
if !checks.insert(check) {
unreachable!("re-registering check: {check}");
}
}
checks
});
#[derive(
Debug, Display, EnumIter, EnumString, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone,
)]
#[strum(serialize_all = "kebab-case")]
pub enum Context {
Gentoo,
GentooInherited,
Optional,
Overlay,
}
macro_rules! register {
($($fields:tt)+) => {
static CHECK: $crate::check::Check = $crate::check::Check {
$($fields)+
};
inventory::submit! { CHECK }
impl std::fmt::Display for Check {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{CHECK}")
}
}
};
}
use register;
#[allow(unused_variables)]
pub(crate) trait CheckRun {
fn run_repo(&self, run: &ScannerRun) {}
fn run_category(&self, category: &str, run: &ScannerRun) {}
fn finish_category(&self, category: &str, run: &ScannerRun) {}
fn run_cpv(&self, cpv: &Cpv, run: &ScannerRun) {}
fn finish_cpv(&self, cpv: &Cpv, run: &ScannerRun) {}
fn run_cpn(&self, cpn: &Cpn, run: &ScannerRun) {}
fn finish_cpn(&self, cpn: &Cpn, run: &ScannerRun) {}
fn run_ebuild_pkg(&self, pkg: &EbuildPkg, run: &ScannerRun) {}
fn run_ebuild_pkg_set(&self, cpn: &Cpn, pkgs: &[EbuildPkg], run: &ScannerRun) {}
fn run_ebuild_raw_pkg(&self, pkg: &EbuildRawPkg, run: &ScannerRun) {}
fn run_ebuild_raw_pkg_set(&self, cpn: &Cpn, pkgs: &[EbuildRawPkg], run: &ScannerRun) {}
fn finish(&self, run: &ScannerRun) {}
}
type Runner = Box<dyn CheckRun + Send + Sync>;
#[derive(Clone)]
pub(crate) struct CheckRunner {
pub(crate) check: Check,
runner: Arc<Runner>,
}
fn time<F>(runner: &CheckRunner, run: &ScannerRun, func: F)
where
F: FnOnce(),
{
let now = Instant::now();
func();
*run.stats.entry(runner.check).or_default() += now.elapsed();
}
impl CheckRun for CheckRunner {
fn run_repo(&self, run: &ScannerRun) {
time(self, run, || self.runner.run_repo(run));
}
fn run_category(&self, category: &str, run: &ScannerRun) {
time(self, run, || self.runner.run_category(category, run));
}
fn finish_category(&self, category: &str, run: &ScannerRun) {
time(self, run, || self.runner.finish_category(category, run));
}
fn run_cpv(&self, cpv: &Cpv, run: &ScannerRun) {
time(self, run, || self.runner.run_cpv(cpv, run));
}
fn finish_cpv(&self, cpv: &Cpv, run: &ScannerRun) {
time(self, run, || self.runner.finish_cpv(cpv, run));
}
fn run_cpn(&self, cpn: &Cpn, run: &ScannerRun) {
time(self, run, || self.runner.run_cpn(cpn, run));
}
fn finish_cpn(&self, cpn: &Cpn, run: &ScannerRun) {
time(self, run, || self.runner.finish_cpn(cpn, run));
}
fn run_ebuild_pkg(&self, pkg: &EbuildPkg, run: &ScannerRun) {
time(self, run, || self.runner.run_ebuild_pkg(pkg, run));
}
fn run_ebuild_pkg_set(&self, cpn: &Cpn, pkgs: &[EbuildPkg], run: &ScannerRun) {
time(self, run, || self.runner.run_ebuild_pkg_set(cpn, pkgs, run));
}
fn run_ebuild_raw_pkg(&self, pkg: &EbuildRawPkg, run: &ScannerRun) {
time(self, run, || self.runner.run_ebuild_raw_pkg(pkg, run));
}
fn run_ebuild_raw_pkg_set(&self, cpn: &Cpn, pkgs: &[EbuildRawPkg], run: &ScannerRun) {
time(self, run, || self.runner.run_ebuild_raw_pkg_set(cpn, pkgs, run));
}
fn finish(&self, run: &ScannerRun) {
time(self, run, || self.runner.finish(run));
}
}
impl fmt::Display for CheckRunner {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.check)
}
}
impl PartialEq for CheckRunner {
fn eq(&self, other: &Self) -> bool {
self.check == other.check
}
}
impl Eq for CheckRunner {}
impl Hash for CheckRunner {
fn hash<H: Hasher>(&self, state: &mut H) {
self.check.hash(state);
}
}
static REPORTS: LazyLock<OrderedMap<ReportKind, OrderedSet<Check>>> = LazyLock::new(|| {
Check::iter()
.flat_map(|c| c.reports.iter().copied().map(move |r| (r, c)))
.collect()
});
#[cfg(test)]
mod tests {
use pkgcraft::test::assert_ordered_eq;
use strum::IntoEnumIterator;
use super::*;
#[test]
fn kind() {
let kinds: Vec<_> = CheckKind::iter().collect();
let ordered: Vec<_> = CheckKind::iter().map(|x| x.to_string()).sorted().collect();
let ordered: Vec<_> = ordered.iter().map(|s| s.parse().unwrap()).collect();
assert_ordered_eq!(&kinds, &ordered);
let checks: Vec<_> = Check::iter().map(|c| c.kind).collect();
assert_ordered_eq!(&kinds, &checks);
}
#[test]
fn report() {
let reports: Vec<_> = ReportKind::iter()
.filter(|x| REPORTS.get(x).is_none())
.collect();
assert!(reports.is_empty(), "no checks for reports: {}", reports.iter().join(", "));
}
}