use std::path::{Path, PathBuf};
use rpm_spec::ast::{Span, SpecFile};
use crate::diagnostic::{Diagnostic, LintCategory, Severity};
use crate::lint::{Lint, LintMetadata};
use crate::rules::util::{package_name, spec_span};
use crate::visit::Visit;
pub static METADATA: LintMetadata = LintMetadata {
id: "RPM312",
name: "spec-filename-mismatch",
description: "Spec file name differs from `<Name>.spec` — most RPM tooling pairs specs to \
package names by filename.",
default_severity: Severity::Warn,
category: LintCategory::Packaging,
};
#[derive(Debug, Default)]
pub struct SpecFilenameMismatch {
diagnostics: Vec<Diagnostic>,
source_path: Option<PathBuf>,
}
impl SpecFilenameMismatch {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for SpecFilenameMismatch {
fn visit_spec(&mut self, spec: &'ast SpecFile<Span>) {
let Some(path) = self.source_path.as_ref() else {
return;
};
let Some(file_name) = path.file_name().and_then(|s| s.to_str()) else {
return;
};
let Some(name) = package_name(spec) else {
return;
};
let expected = format!("{name}.spec");
if file_name != expected {
self.diagnostics.push(Diagnostic::new(
&METADATA,
Severity::Warn,
format!(
"spec file is named `{file_name}` but `Name:` is `{name}`; expected \
`{expected}` so RPM tooling can pair them by filename"
),
spec_span(spec),
));
}
}
}
impl Lint for SpecFilenameMismatch {
fn metadata(&self) -> &'static LintMetadata {
&METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
fn set_source_path(&mut self, path: Option<&std::path::Path>) {
self.source_path = path.map(Path::to_path_buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::parse;
fn run(src: &str, path: Option<&Path>) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = SpecFilenameMismatch::new();
lint.set_source_path(path);
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn flags_filename_mismatch() {
let diags = run("Name: hello\n", Some(Path::new("/tmp/world.spec")));
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].lint_id, "RPM312");
assert!(diags[0].message.contains("hello.spec"));
assert!(diags[0].message.contains("world.spec"));
}
#[test]
fn silent_when_filename_matches() {
assert!(run("Name: hello\n", Some(Path::new("/tmp/hello.spec"))).is_empty());
}
#[test]
fn silent_when_no_path() {
assert!(run("Name: hello\n", None).is_empty());
}
#[test]
fn silent_when_name_missing() {
assert!(run("Version: 1\n", Some(Path::new("/tmp/world.spec"))).is_empty());
}
#[test]
fn silent_when_name_is_macro() {
assert!(run("Name: %{base_name}\n", Some(Path::new("/tmp/world.spec"))).is_empty());
}
}