Files
pkh/src/lint/emit.rs
T
vhaudiquet a0e74073bf
CI / build (push) Successful in 2m55s
CI / test (push) Skipped
CI / snap (push) Successful in 4m32s
lint: add pkh lint, wrapping lintian for parity plus pkh-native checks
pkh covered the package lifecycle but never validated the packaging
itself: broken control stanzas, unparsable changelog versions or
uncommitted debian/ edits only surfaced at build or upload time. pkh
lint lints a source tree with day-one lintian parity plus a native Rust
engine for the checks lintian cannot have.

The wrapper reuses the pkh build output next to the tree when it matches
the current changelog entry and no tree content is newer (mtime walk,
skipping .git/.pc), else packs fresh with dpkg-source -b using weak gzip
compression (the artifact is ephemeral; xz dominated the run at 9.8 s
versus 2.7 s on a 111 MB tree) and symlinks quilt orig tarballs from the
tree's parent, which dpkg-source searches in cwd. Findings are parsed
from the installed lintian into a unified report, deduplicated by tag
name against the native engine, and rendered lintian-shaped
(<L>: <pkg> <type>: <tag> <details>) as text or JSON, colorized at
render time (--color auto/always/never). Exit codes follow lintian's
contract (0 clean, 1 findings at/above --fail-on, 2 runtime error);
lintian's own exit code is ignored because it uses 2 both for findings
and for runtime errors. -d/--dist maps to lintian --profile so the
target distro's rules apply even on a foreign host.

The native engine hosts the first workflow check lintian cannot know:
pkh-debian-changes-not-committed flags debian/ content that is not
committed to git, since the pkh flow builds and uploads the tree as-is.
Checks register in a static registry validated by a unit test, and the
wrapper's parser is pinned by golden tests captured from lintian 2.129
output. Strategies for lintian's Ubuntu blind spots (its vendor data
there is one file plus 14 disabled tags) are specced in
plans/pkh-lint.md, deliberately not implemented yet.
2026-09-19 23:45:31 +02:00

119 lines
4.3 KiB
Rust

//! Findings, the run report, and the emitter checks report through.
//!
//! The emitter is deliberately thin: checks name a tag and give a message;
//! metadata, output letter and explanations come from the registry, so
//! findings from the native engine and findings parsed from the wrapped
//! lintian share one shape and one rendering path.
use crate::lint::check;
use crate::lint::tag::Tag;
/// Where a finding comes from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Origin {
/// Emitted by pkh's native Rust checks.
Native,
/// Parsed from the wrapped lintian's output.
Lintian,
}
/// One lint finding, whatever produced it.
#[derive(Debug, Clone)]
pub struct Finding {
/// Output letter (`E`, `W`, `I`, `P`, `X`, `O`, `C`): severity for
/// native findings, verbatim from the output line in wrapper mode.
pub letter: char,
/// Stable tag name (`missing-debian-copyright-file`).
pub tag_name: String,
/// Free-form details after the tag name; empty when the tag stands alone.
pub message: String,
/// Package the finding belongs to (`hello`).
pub package: String,
/// Processable type lintian displays after the package name (`source`,
/// `changes`, ...); None for binary findings, which lintian prints
/// without a type.
pub processable_type: Option<String>,
/// Explanation lines shown by `--info`: the `N:` blocks lintian attaches
/// to the finding in wrapper mode, or the native tag's description and
/// references.
pub explanation: Vec<String>,
/// Native or parsed-from-lintian.
pub origin: Origin,
}
impl Finding {
/// Lowercase severity/classification name for this finding's letter, as
/// used in `--fail-on` values and JSON output.
pub fn severity_name(&self) -> &'static str {
match self.letter {
'E' => "error",
'W' => "warning",
'I' => "info",
'P' => "pedantic",
'X' => "experimental",
'O' => "overridden",
_ => "classification",
}
}
}
/// The full result of one lint run: every finding, from every source.
pub struct LintReport {
/// All findings in emission order: the wrapper's first, then the native
/// ones (deduplicated by tag name against the wrapper's).
pub findings: Vec<Finding>,
/// Display name of the linted source package.
pub source_name: String,
/// The wrapper was skipped because lintian is not installed; the
/// renderer prints a notice and the run is native-only.
pub wrapper_unavailable: bool,
/// Run-level notes (`N:` lines): how the linted artifact was obtained,
/// and similar context that is not a finding.
pub notes: Vec<String>,
}
/// Sink checks report findings through. Resolves tag metadata from the
/// registry so checks only ever name the tag they mean.
pub struct Emitter<'a> {
package: String,
findings: &'a mut Vec<Finding>,
}
impl<'a> Emitter<'a> {
/// Emitter for findings of `package` (e.g. the source package name),
/// appending into `findings`.
pub fn new(package: String, findings: &'a mut Vec<Finding>) -> Emitter<'a> {
Emitter { package, findings }
}
/// Emit `tag_name` with `message` as its detail line. Native findings
/// always report on the source package; an unknown tag name (a registry
/// bug) still produces a finding rather than panicking — the registry
/// test makes that path unreachable in practice.
pub fn tag(&mut self, tag_name: &str, message: impl Into<String>) {
let (letter, explanation) = match check::find_tag(tag_name) {
Some(tag) => (tag.letter(), explanation_of(tag)),
None => ('E', Vec::new()),
};
self.findings.push(Finding {
letter,
tag_name: tag_name.to_string(),
message: message.into(),
package: self.package.clone(),
processable_type: Some("source".to_string()),
explanation,
origin: Origin::Native,
});
}
}
/// The explanation `--info` shows for a native tag: its description plus any
/// references, prefixed like lintian's "Please refer to" pointers.
fn explanation_of(tag: &Tag) -> Vec<String> {
let mut lines = vec![tag.description.to_string()];
for reference in tag.references {
lines.push(format!("Please refer to {}", reference));
}
lines
}