//! `pkh lint`: lint a Debian source tree, with lintian feature parity. //! //! The strategy is *wrap first, port second*: the //! [wrapper](wrapper) runs the installed lintian over an ephemeral source //! package for day-one parity with every lintian check, while the native //! engine (check registry, collectors, emitter) hosts pkh-specific workflow //! checks (`pkh-*` tags) and grows ported lintian checks incrementally. //! Both findings merge into one report — deduplicated by tag name, which is //! why native checks mirror lintian's tag names for equivalent checks — //! rendered as lintian-shaped text or JSON, with lintian's exit-code //! contract (0 clean, 1 findings at/above `--fail-on`, 2 runtime error). pub mod check; pub mod checks; pub mod collect; pub mod emit; pub mod output; pub mod tag; pub mod wrapper; use std::collections::HashSet; use std::path::PathBuf; use crate::lint::check::Check; use crate::lint::collect::LintData; use crate::lint::emit::{Finding, LintReport}; /// Knobs of one `pkh lint` run, built from the CLI in `main.rs`. pub struct LintOptions { /// Source tree to lint. pub path: PathBuf, /// Run the native engine only; never invoke the lintian wrapper. pub native: bool, /// Levels that make the exit code 1 (`--fail-on`, default: error). pub fail_on: Vec, /// Show tag explanations under each finding (`--info`, lintian's `-i`). pub info: bool, /// Also display info-level (`I:`) findings (lintian's `-I`). pub display_info: bool, /// Also display pedantic (`P:`) findings. pub pedantic: bool, /// Also display experimental (`X:`) findings. pub experimental: bool, /// Also display overridden (`O:`) findings. pub show_overrides: bool, /// Tag names to ignore for this run. pub suppress_tags: Vec, /// Run only these native checks (`--check`, repeatable). pub only_checks: Vec, /// Ignore any existing `pkh build` output and pack the tree fresh. pub repack: bool, /// Emit JSON instead of text. pub json: bool, /// Text colorization mode (`--color`, default auto: TTY without NO_COLOR). pub color: output::ColorMode, /// Target distribution (debian/ubuntu), when known. pub dist: Option, /// Target series, when known. pub series: Option, } /// Run one lint: collect package information, run the native checks, wrap /// lintian (unless `--native`), and merge everything into one report. pub fn run(options: &LintOptions) -> Result { let data = LintData::collect( &options.path, options.dist.as_deref(), options.series.as_deref(), ); let mut report = LintReport { findings: Vec::new(), source_name: data.source_name.clone(), wrapper_unavailable: false, notes: Vec::new(), }; if !options.native { match wrapper::run(&data.root, options)? { Some(outcome) => { report.findings.extend(outcome.findings); report.notes = outcome.notes; } None => report.wrapper_unavailable = true, } } let selected: Vec<&'static dyn Check> = if options.only_checks.is_empty() { check::CHECKS.to_vec() } else { options .only_checks .iter() .map(|id| check::find_check(id).ok_or_else(|| format!("Unknown --check '{id}'"))) .collect::>()? }; let mut native = Vec::new(); { let mut emitter = emit::Emitter::new(report.source_name.clone(), &mut native); for check in selected { check.run(&data, &mut emitter); } } report.findings.extend(merge(&report.findings, native)); if !options.suppress_tags.is_empty() { report .findings .retain(|finding| !options.suppress_tags.contains(&finding.tag_name)); } Ok(report) } /// Append `native` findings to `lintian`'s, dropping native duplicates by /// tag name (the wrapper's verdict wins for tags both engines produce). fn merge(lintian: &[Finding], native: Vec) -> Vec { let known: HashSet<&str> = lintian.iter().map(|f| f.tag_name.as_str()).collect(); native .into_iter() .filter(|finding| !known.contains(finding.tag_name.as_str())) .collect() } /// The `--list-tags` catalog: one line per registered native tag. pub fn list_tags() -> String { let mut out = String::new(); for check in check::CHECKS { for tag in check.tags() { out.push_str(&format!( "{} [{}] {}\n {}\n", tag.letter(), check.id(), tag.name, tag.description )); } } out } #[cfg(test)] mod tests { use super::*; use crate::lint::emit::Origin; fn finding(tag: &str, origin: Origin) -> Finding { Finding { letter: 'W', tag_name: tag.to_string(), message: String::new(), package: "hello".to_string(), processable_type: Some("source".to_string()), explanation: Vec::new(), origin, } } #[test] fn merge_drops_native_duplicates_by_tag_name() { let lintian = vec![finding("no-debian-copyright-in-source", Origin::Lintian)]; let native = vec![ finding("no-debian-copyright-in-source", Origin::Native), finding("pkh-debian-changes-not-committed", Origin::Native), ]; let merged = merge(&lintian, native); assert_eq!(merged.len(), 1); assert_eq!(merged[0].tag_name, "pkh-debian-changes-not-committed"); assert_eq!(merged[0].origin, Origin::Native); } #[test] fn native_tags_resolve_from_the_registry() { let tag = check::find_tag("pkh-debian-changes-not-committed") .expect("pkh check tags must resolve"); assert_eq!(tag.letter(), 'W'); } #[test] fn unknown_native_tags_still_produce_findings() { let mut sink = Vec::new(); let mut emitter = emit::Emitter::new("hello".to_string(), &mut sink); emitter.tag("no-such-tag-anywhere", "boom"); assert_eq!(sink.len(), 1); assert_eq!(sink[0].tag_name, "no-such-tag-anywhere"); } }