use crate::declare_detector;
use crate::diagnostic::{Action, Deb822Action, Diagnostic, ParagraphSelector};
use crate::{FixerError, FixerPreferences, LintianIssue, Visibility};
use deb822_lossless::Deb822;
use debian_analyzer::editor::check_generated_file;
use debian_workspace::Workspace;
use std::path::{Path, PathBuf};
use std::str::FromStr;
fn collect_diagnostics(
file_for_actions: &Path,
parsed_path: &Path,
use_typed_control_selectors: bool,
tagged: bool,
) -> Result<Vec<Diagnostic>, FixerError> {
let content = std::fs::read_to_string(parsed_path)?;
let Ok(deb822) = Deb822::from_str(&content) else {
return Ok(Vec::new());
};
let mut diagnostics = Vec::new();
for (idx, paragraph) in deb822.paragraphs().enumerate() {
let selector = if use_typed_control_selectors {
typed_selector_for(¶graph, idx)
} else {
generic_selector_for(¶graph, idx)
};
let entry_keys: Vec<(String, usize)> = paragraph
.entries()
.filter_map(|e| e.key().map(|k| (k.to_string(), e.line() + 1)))
.collect();
for (key, line_number) in entry_keys {
let probe_paragraph = paragraph.clone();
let Some(mut probe) = probe_paragraph.get_entry(&key) else {
continue;
};
if !probe.normalize_field_spacing() {
continue;
}
let actions = vec![Action::Deb822(Deb822Action::NormalizeFieldSpacing {
file: file_for_actions.to_path_buf(),
paragraph: selector.clone(),
field: key.clone(),
})];
let description = "debian/control has unusual field spacing.";
let label = "Strip unusual field spacing from debian/control.";
if tagged {
let issue = LintianIssue::source_with_info(
"debian-control-has-unusual-field-spacing",
Visibility::Pedantic,
vec![format!("{} [debian/control:{}]", key, line_number)],
);
diagnostics.push(Diagnostic::with_actions(issue, description, label, actions));
} else {
diagnostics.push(Diagnostic::untagged(description, label, actions));
}
}
}
Ok(diagnostics)
}
fn typed_selector_for(paragraph: &deb822_lossless::Paragraph, idx: usize) -> ParagraphSelector {
if let Some(pkg) = paragraph.get("Package") {
return ParagraphSelector::Binary { package: pkg };
}
if paragraph.get("Source").is_some() {
return ParagraphSelector::Source;
}
ParagraphSelector::Index { index: idx }
}
fn generic_selector_for(paragraph: &deb822_lossless::Paragraph, idx: usize) -> ParagraphSelector {
if let Some(pkg) = paragraph.get("Package") {
return ParagraphSelector::ByKey {
field: "Package".into(),
value: pkg,
};
}
if let Some(src) = paragraph.get("Source") {
return ParagraphSelector::ByKey {
field: "Source".into(),
value: src,
};
}
ParagraphSelector::Index { index: idx }
}
pub fn detect(
ws: &dyn Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
let Some(base_path) = ws.base_path() else {
return Ok(Vec::new());
};
let control_rel = PathBuf::from("debian/control");
let control_abs = base_path.join(&control_rel);
if !control_abs.exists() {
return Ok(Vec::new());
}
match check_generated_file(&control_abs) {
Err(generated_file) => {
let Some(template_abs) = &generated_file.template_path else {
return Ok(Vec::new());
};
let template_rel = template_abs
.strip_prefix(base_path)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| template_abs.clone());
let mut diagnostics = collect_diagnostics(&template_rel, template_abs, false, true)?;
diagnostics.extend(collect_diagnostics(
&control_rel,
&control_abs,
false,
false,
)?);
Ok(diagnostics)
}
Ok(()) => collect_diagnostics(&control_rel, &control_abs, true, true),
}
}
declare_detector! {
name: "debian-control-has-unusual-field-spacing",
tags: ["debian-control-has-unusual-field-spacing"],
before: ["file-contains-trailing-whitespace"],
triggers: [
debian_workspace::Trigger::File("debian/control"),
debian_workspace::Trigger::File("debian/control.in"),
],
detect: |ws, prefs| detect(ws, prefs),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detector::Detector;
use crate::{FixerPreferences, Version};
use std::fs;
use tempfile::TempDir;
fn run_apply(base: &Path) -> Result<crate::FixerResult, FixerError> {
let version: Version = "1.0".parse().unwrap();
let adapter = DetectorImpl;
{
let ws = debian_workspace::fs_workspace::FsWorkspace::new(
base,
Some("test".into()),
Some(version.clone()),
);
adapter.apply(&ws, &FixerPreferences::default())
}
}
#[test]
fn test_normalize_double_space() {
let tmp = TempDir::new().unwrap();
let debian = tmp.path().join("debian");
fs::create_dir_all(&debian).unwrap();
let control = debian.join("control");
fs::write(&control, "Source: blah\nRecommends: ${cdbs:Recommends}\n").unwrap();
run_apply(tmp.path()).unwrap();
assert_eq!(
fs::read_to_string(&control).unwrap(),
"Source: blah\nRecommends: ${cdbs:Recommends}\n",
);
}
#[test]
fn test_no_changes() {
let tmp = TempDir::new().unwrap();
let debian = tmp.path().join("debian");
fs::create_dir_all(&debian).unwrap();
fs::write(
debian.join("control"),
"Source: blah\nRecommends: ${cdbs:Recommends}\n",
)
.unwrap();
assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
}
#[test]
fn test_normalize_tab_after_colon() {
let tmp = TempDir::new().unwrap();
let debian = tmp.path().join("debian");
fs::create_dir_all(&debian).unwrap();
let control = debian.join("control");
fs::write(&control, "Source: blah\nBuild-Depends:\tpython3\n").unwrap();
run_apply(tmp.path()).unwrap();
assert_eq!(
fs::read_to_string(&control).unwrap(),
"Source: blah\nBuild-Depends: python3\n",
);
}
#[test]
fn test_preserves_continuation_lines() {
let tmp = TempDir::new().unwrap();
let debian = tmp.path().join("debian");
fs::create_dir_all(&debian).unwrap();
let control = debian.join("control");
fs::write(
&control,
"Source: blah\nBuild-Depends: cdbs (>= 0.4.123~),\n anotherline\n",
)
.unwrap();
run_apply(tmp.path()).unwrap();
assert_eq!(
fs::read_to_string(&control).unwrap(),
"Source: blah\nBuild-Depends: cdbs (>= 0.4.123~),\n anotherline\n",
);
}
}