use rpm_spec::ast::{FileTrigger, Scriptlet, Section, Span, SpecFile, Text, TextSegment, Trigger};
use crate::diagnostic::{Applicability, Diagnostic, LintCategory, Severity, Suggestion};
use crate::lint::{Lint, LintMetadata};
use crate::visit::{self, Visit};
pub static BUILDROOT_METADATA: LintMetadata = LintMetadata {
id: "RPM053",
name: "rpm-buildroot-shell-var",
description: "Use `%{buildroot}` instead of the legacy `$RPM_BUILD_ROOT` environment variable.",
default_severity: Severity::Warn,
category: LintCategory::Style,
};
pub static SOURCE_DIR_METADATA: LintMetadata = LintMetadata {
id: "RPM054",
name: "rpm-source-dir-shell-var",
description: "Use `%{_sourcedir}` instead of the legacy `$RPM_SOURCE_DIR` environment variable.",
default_severity: Severity::Warn,
category: LintCategory::Style,
};
#[derive(Debug, Default)]
pub struct RpmBuildrootShellVar {
diagnostics: Vec<Diagnostic>,
current_shell_span: Option<Span>,
}
impl RpmBuildrootShellVar {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for RpmBuildrootShellVar {
fn visit_section(&mut self, node: &'ast Section<Span>) {
scan_section(
self,
node,
"$RPM_BUILD_ROOT",
"%{buildroot}",
&BUILDROOT_METADATA,
);
}
fn visit_scriptlet(&mut self, node: &'ast Scriptlet<Span>) {
scan_scriptlet(
self,
node,
"$RPM_BUILD_ROOT",
"%{buildroot}",
&BUILDROOT_METADATA,
);
}
fn visit_trigger(&mut self, node: &'ast Trigger<Span>) {
scan_trigger(
self,
node,
"$RPM_BUILD_ROOT",
"%{buildroot}",
&BUILDROOT_METADATA,
);
}
fn visit_file_trigger(&mut self, node: &'ast FileTrigger<Span>) {
scan_file_trigger(
self,
node,
"$RPM_BUILD_ROOT",
"%{buildroot}",
&BUILDROOT_METADATA,
);
}
}
impl Lint for RpmBuildrootShellVar {
fn metadata(&self) -> &'static LintMetadata {
&BUILDROOT_METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
}
#[derive(Debug, Default)]
pub struct RpmSourceDirShellVar {
diagnostics: Vec<Diagnostic>,
current_shell_span: Option<Span>,
}
impl RpmSourceDirShellVar {
pub fn new() -> Self {
Self::default()
}
}
impl<'ast> Visit<'ast> for RpmSourceDirShellVar {
fn visit_section(&mut self, node: &'ast Section<Span>) {
scan_section(
self,
node,
"$RPM_SOURCE_DIR",
"%{_sourcedir}",
&SOURCE_DIR_METADATA,
);
}
fn visit_scriptlet(&mut self, node: &'ast Scriptlet<Span>) {
scan_scriptlet(
self,
node,
"$RPM_SOURCE_DIR",
"%{_sourcedir}",
&SOURCE_DIR_METADATA,
);
}
fn visit_trigger(&mut self, node: &'ast Trigger<Span>) {
scan_trigger(
self,
node,
"$RPM_SOURCE_DIR",
"%{_sourcedir}",
&SOURCE_DIR_METADATA,
);
}
fn visit_file_trigger(&mut self, node: &'ast FileTrigger<Span>) {
scan_file_trigger(
self,
node,
"$RPM_SOURCE_DIR",
"%{_sourcedir}",
&SOURCE_DIR_METADATA,
);
}
}
impl Lint for RpmSourceDirShellVar {
fn metadata(&self) -> &'static LintMetadata {
&SOURCE_DIR_METADATA
}
fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
}
trait ShellLint {
fn current_shell_span(&mut self) -> &mut Option<Span>;
fn diagnostics(&mut self) -> &mut Vec<Diagnostic>;
}
impl ShellLint for RpmBuildrootShellVar {
fn current_shell_span(&mut self) -> &mut Option<Span> {
&mut self.current_shell_span
}
fn diagnostics(&mut self) -> &mut Vec<Diagnostic> {
&mut self.diagnostics
}
}
impl ShellLint for RpmSourceDirShellVar {
fn current_shell_span(&mut self) -> &mut Option<Span> {
&mut self.current_shell_span
}
fn diagnostics(&mut self) -> &mut Vec<Diagnostic> {
&mut self.diagnostics
}
}
fn scan_section<L: ShellLint + for<'a> Visit<'a>>(
lint: &mut L,
node: &Section<Span>,
needle: &str,
replacement: &str,
meta: &'static LintMetadata,
) {
let span = match node {
Section::BuildScript { data, body, .. } | Section::Verify { data, body, .. } => {
*lint.current_shell_span() = Some(*data);
for line in &body.lines {
emit_if_match(line, *data, needle, replacement, meta, lint.diagnostics());
}
*data
}
Section::Sepolicy { data, body, .. } => {
*lint.current_shell_span() = Some(*data);
for line in &body.lines {
emit_if_match(line, *data, needle, replacement, meta, lint.diagnostics());
}
*data
}
_ => {
visit::walk_section(lint, node);
*lint.current_shell_span() = None;
return;
}
};
let _ = span;
*lint.current_shell_span() = None;
}
fn scan_scriptlet<L: ShellLint + for<'a> Visit<'a>>(
lint: &mut L,
node: &Scriptlet<Span>,
needle: &str,
replacement: &str,
meta: &'static LintMetadata,
) {
let span = node.data;
*lint.current_shell_span() = Some(span);
for line in &node.body.lines {
emit_if_match(line, span, needle, replacement, meta, lint.diagnostics());
}
*lint.current_shell_span() = None;
}
fn scan_trigger<L: ShellLint + for<'a> Visit<'a>>(
lint: &mut L,
node: &Trigger<Span>,
needle: &str,
replacement: &str,
meta: &'static LintMetadata,
) {
let span = node.data;
*lint.current_shell_span() = Some(span);
for line in &node.body.lines {
emit_if_match(line, span, needle, replacement, meta, lint.diagnostics());
}
*lint.current_shell_span() = None;
}
fn scan_file_trigger<L: ShellLint + for<'a> Visit<'a>>(
lint: &mut L,
node: &FileTrigger<Span>,
needle: &str,
replacement: &str,
meta: &'static LintMetadata,
) {
let span = node.data;
*lint.current_shell_span() = Some(span);
for line in &node.body.lines {
emit_if_match(line, span, needle, replacement, meta, lint.diagnostics());
}
*lint.current_shell_span() = None;
}
fn emit_if_match(
line: &Text,
body_span: Span,
needle: &str,
replacement: &str,
meta: &'static LintMetadata,
out: &mut Vec<Diagnostic>,
) {
let hit = line.segments.iter().any(|seg| match seg {
TextSegment::Literal(s) => s.contains(needle),
_ => false,
});
if !hit {
return;
}
out.push(
Diagnostic::new(
meta,
Severity::Warn,
format!("use `{replacement}` instead of `{needle}`"),
body_span,
)
.with_suggestion(Suggestion::new(
format!("replace `{needle}` with `{replacement}` in this body"),
Vec::new(),
Applicability::Manual,
)),
);
}
#[allow(dead_code)]
const _: Option<SpecFile<Span>> = None;
#[cfg(test)]
mod tests {
use super::*;
use crate::session::parse;
fn run_buildroot(src: &str) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = RpmBuildrootShellVar::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
fn run_source_dir(src: &str) -> Vec<Diagnostic> {
let outcome = parse(src);
let mut lint = RpmSourceDirShellVar::new();
lint.visit_spec(&outcome.spec);
lint.take_diagnostics()
}
#[test]
fn rpm053_flags_build_root_in_install() {
let src = "Name: x\n%install\nmkdir -p $RPM_BUILD_ROOT/usr/bin\n";
let diags = run_buildroot(src);
assert!(!diags.is_empty(), "expected RPM053");
assert_eq!(diags[0].lint_id, "RPM053");
}
#[test]
fn rpm053_silent_when_macro_used() {
let src = "Name: x\n%install\nmkdir -p %{buildroot}/usr/bin\n";
assert!(run_buildroot(src).is_empty());
}
#[test]
fn rpm053_flags_build_root_in_scriptlet() {
let src = "Name: x\n%post\necho $RPM_BUILD_ROOT\n";
assert!(!run_buildroot(src).is_empty());
}
#[test]
fn rpm054_flags_source_dir_in_build() {
let src = "Name: x\n%build\ncp $RPM_SOURCE_DIR/patch .\n";
let diags = run_source_dir(src);
assert!(!diags.is_empty());
assert_eq!(diags[0].lint_id, "RPM054");
}
#[test]
fn rpm054_silent_when_macro_used() {
let src = "Name: x\n%build\ncp %{_sourcedir}/patch .\n";
assert!(run_source_dir(src).is_empty());
}
}