use crate::declare_detector;
use crate::diagnostic::Diagnostic;
use crate::{FixerError, FixerPreferences, LintianIssue, Visibility};
use debian_workspace::Workspace;
fn is_asterisk_bullet(line: &str) -> bool {
let rest = line.trim_start();
let mut chars = rest.chars();
chars.next() == Some('*') && chars.next().is_some_and(|c| c.is_whitespace())
}
pub fn detect(
ws: &dyn Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
let package = ws.package().unwrap_or("").to_string();
let news = match ws.parsed_news() {
Ok(n) => n,
Err(debian_workspace::Error::NotFound) => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
let Some(entry) = news.iter().next() else {
return Ok(Vec::new());
};
let Some(first_line) = entry.change_lines().next() else {
return Ok(Vec::new());
};
if !is_asterisk_bullet(&first_line) {
return Ok(Vec::new());
}
let line_no = entry.line() + 1;
let issue = LintianIssue::source_with_info(
"debian-news-entry-uses-asterisk",
Visibility::Info,
vec![format!(
"[usr/share/doc/{}/NEWS.Debian.gz:{}]",
&package, line_no
)],
);
Ok(vec![Diagnostic {
issue: Some(issue),
message: "NEWS entry uses asterisks for a bulleted list.".to_string(),
certainty: None,
patch_name: None,
plans: Vec::new(),
}])
}
declare_detector! {
name: "debian-news-entry-uses-asterisk",
tags: ["debian-news-entry-uses-asterisk"],
triggers: [debian_workspace::Trigger::File("debian/NEWS")],
detect: |ws, prefs| detect(ws, prefs),
}
#[cfg(test)]
mod tests {
use super::*;
use debian_workspace::fs_workspace::FsWorkspace;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn workspace(base: &Path) -> FsWorkspace {
let version: crate::Version = "1.0".parse().unwrap();
FsWorkspace::new(base, Some("test-package".to_string()), Some(version))
}
fn write_news(base: &Path, contents: &str) {
let debian = base.join("debian");
fs::create_dir_all(&debian).unwrap();
fs::write(debian.join("NEWS"), contents).unwrap();
}
#[test]
fn test_is_asterisk_bullet() {
assert!(is_asterisk_bullet("* foo"));
assert!(is_asterisk_bullet(" * foo"));
assert!(is_asterisk_bullet("*\tfoo"));
assert!(!is_asterisk_bullet("*foo"));
assert!(!is_asterisk_bullet("Regular paragraph."));
assert!(!is_asterisk_bullet("- foo"));
assert!(!is_asterisk_bullet(""));
assert!(!is_asterisk_bullet("text * with asterisk"));
}
#[test]
fn test_detects_asterisk_entry() {
let tmp = TempDir::new().unwrap();
write_news(
tmp.path(),
"test-package (1.0-1) unstable; urgency=low\n\n * This is a change.\n * Another change.\n\n -- Joe <joe@example.com> Mon, 01 Jan 2024 00:00:00 +0000\n",
);
let diags = detect(&workspace(tmp.path()), &FixerPreferences::default()).unwrap();
assert_eq!(diags.len(), 1);
let issue = diags[0].issue.as_ref().unwrap();
assert_eq!(
issue.tag.as_deref(),
Some("debian-news-entry-uses-asterisk")
);
assert_eq!(issue.visibility, Some(Visibility::Info));
assert_eq!(
issue.info.as_deref(),
Some("[usr/share/doc/test-package/NEWS.Debian.gz:1]")
);
assert!(diags[0].plans.is_empty());
}
#[test]
fn test_ignores_prose_entry() {
let tmp = TempDir::new().unwrap();
write_news(
tmp.path(),
"test-package (1.0-1) unstable; urgency=low\n\n This is a regular paragraph describing the change.\n\n -- Joe <joe@example.com> Mon, 01 Jan 2024 00:00:00 +0000\n",
);
let diags = detect(&workspace(tmp.path()), &FixerPreferences::default()).unwrap();
assert!(diags.is_empty());
}
#[test]
fn test_only_checks_top_entry() {
let tmp = TempDir::new().unwrap();
write_news(
tmp.path(),
"test-package (2.0-1) unstable; urgency=low\n\n Prose paragraph for the latest entry.\n\n -- Joe <joe@example.com> Tue, 02 Jan 2024 00:00:00 +0000\n\ntest-package (1.0-1) unstable; urgency=low\n\n * An older bulleted entry.\n\n -- Joe <joe@example.com> Mon, 01 Jan 2024 00:00:00 +0000\n",
);
let diags = detect(&workspace(tmp.path()), &FixerPreferences::default()).unwrap();
assert!(diags.is_empty());
}
#[test]
fn test_reports_top_entry_line_number() {
let tmp = TempDir::new().unwrap();
write_news(
tmp.path(),
"test-package (2.0-1) unstable; urgency=low\n\n * Bulleted latest entry.\n\n -- Joe <joe@example.com> Tue, 02 Jan 2024 00:00:00 +0000\n",
);
let diags = detect(&workspace(tmp.path()), &FixerPreferences::default()).unwrap();
assert_eq!(diags.len(), 1);
assert_eq!(
diags[0].issue.as_ref().unwrap().info.as_deref(),
Some("[usr/share/doc/test-package/NEWS.Debian.gz:1]")
);
}
#[test]
fn test_no_news_file() {
let tmp = TempDir::new().unwrap();
let debian = tmp.path().join("debian");
fs::create_dir_all(&debian).unwrap();
let diags = detect(&workspace(tmp.path()), &FixerPreferences::default()).unwrap();
assert!(diags.is_empty());
}
#[test]
fn test_issue_honours_override() {
let tmp = TempDir::new().unwrap();
write_news(
tmp.path(),
"test-package (1.0-1) unstable; urgency=low\n\n * This is a change.\n\n -- Joe <joe@example.com> Mon, 01 Jan 2024 00:00:00 +0000\n",
);
fs::create_dir_all(tmp.path().join("debian/source")).unwrap();
fs::write(
tmp.path().join("debian/source/lintian-overrides"),
"debian-news-entry-uses-asterisk [usr/share/doc/test-package/NEWS.Debian.gz:1]\n",
)
.unwrap();
let diags = detect(&workspace(tmp.path()), &FixerPreferences::default()).unwrap();
assert_eq!(diags.len(), 1);
let issue = diags[0].issue.as_ref().unwrap();
assert!(!issue.should_fix(tmp.path()));
}
#[test]
fn test_report_only_yields_no_changes() {
let tmp = TempDir::new().unwrap();
write_news(
tmp.path(),
"test-package (1.0-1) unstable; urgency=low\n\n * This is a change.\n\n -- Joe <joe@example.com> Mon, 01 Jan 2024 00:00:00 +0000\n",
);
let diags = detect(&workspace(tmp.path()), &FixerPreferences::default()).unwrap();
let result = crate::builtin_fixers::plan_diagnostics(
tmp.path(),
&diags,
&FixerPreferences::default(),
);
assert!(matches!(result, Err(FixerError::NoChanges)));
}
}