use std::path::PathBuf;
use crate::directives::{DirectiveOutcome, Verb};
use crate::linter::diagnostic::{Diagnostic, Fix, Severity};
use super::{Example, Rule, RuleContext};
const EXAMPLES: &[Example] = &[Example {
caption: "A retired suppression directive:",
source: "% badness-ignore deprecated-command: legacy source\n{\\bf text}\n",
}];
pub struct DeprecatedSuppressionSyntax;
impl Rule for DeprecatedSuppressionSyntax {
fn id(&self) -> &'static str {
"deprecated-suppression-syntax"
}
fn description(&self) -> &'static str {
"Flag the retired `% badness-ignore <rule>` and `% badness-ignore-file \
[<rule>]` suppression spellings, which remain accepted for compatibility \
but are no longer documented. The Safe autofix rewrites only the family \
and verb to `% badness-lint skip <rule>` or `% badness-lint skip-file \
[<rule>]`; the selector and reason remain byte-for-byte unchanged, and \
the edit stays entirely inside a comment. This meta diagnostic is not \
silenced by the retired directive it reports; use `[lint].ignore` to \
disable the rule deliberately."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn emits_fix(&self) -> bool {
true
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
for located in ctx.suppressions.directives().iter().filter(|located| {
located.directive.deprecated && located.outcome != DirectiveOutcome::Unsupported
}) {
let replacement = match located.directive.verb {
Verb::Skip => "badness-lint skip",
Verb::SkipFile => "badness-lint skip-file",
Verb::Off | Verb::On => continue,
};
let start = usize::from(located.family_range.start());
let end = usize::from(located.family_range.end());
sink.push(Diagnostic {
rule: self.id(),
severity: Severity::Warning,
path: PathBuf::new(),
start,
end,
message: format!("retired suppression syntax; use `% {replacement}` instead"),
fix: Some(Fix::safe(
start,
end,
replacement,
"Rewrite the retired suppression directive",
)),
related: Vec::new(),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linter::diagnostic::Applicability;
use crate::linter::fix::apply_fixes;
use crate::parser::{LatexFlavor, LexConfig, parse_with_flavor};
use crate::semantic::SemanticModel;
use crate::syntax::SyntaxNode;
fn findings_with(src: &str, config: LexConfig) -> Vec<Diagnostic> {
let root = SyntaxNode::new_root(parse_with_flavor(src, config).green);
let model = SemanticModel::build(&root);
let ctx = RuleContext::new(
std::path::Path::new("x.tex"),
&root,
&model,
None,
None,
None,
);
let mut out = Vec::new();
DeprecatedSuppressionSyntax.check_file(&ctx, &mut out);
out
}
fn findings(src: &str) -> Vec<Diagnostic> {
findings_with(src, LexConfig::default())
}
#[test]
fn rewrites_node_suppression_safely() {
let src = "% badness-ignore deprecated-command: legacy\n{\\bf text}\n";
let out = findings(src);
assert_eq!(out.len(), 1);
assert_eq!(out[0].rule, "deprecated-suppression-syntax");
assert_eq!(&src[out[0].start..out[0].end], "badness-ignore");
let fix = out[0].fix.as_ref().expect("has fix");
assert_eq!(fix.applicability, Applicability::Safe);
assert_eq!(
apply_fixes(src, std::slice::from_ref(fix), false).output,
"% badness-lint skip deprecated-command: legacy\n{\\bf text}\n"
);
}
#[test]
fn rewrites_file_suppression_and_preserves_reason() {
let src = "%%% badness-ignore-file: generated file\ntext\n";
let out = findings(src);
assert_eq!(out.len(), 1);
let fix = out[0].fix.as_ref().expect("has fix");
assert_eq!(
apply_fixes(src, std::slice::from_ref(fix), false).output,
"%%% badness-lint skip-file: generated file\ntext\n"
);
}
#[test]
fn modern_and_ordinary_comments_are_fine() {
assert!(findings("% badness-lint skip deprecated-command\n\\bf\n").is_empty());
assert!(findings("% badness is good\n").is_empty());
}
#[test]
fn does_not_rewrite_dtx_documentation_prose() {
let src = "% badness-ignore deprecated-command\nDocumentation.\n";
let out = findings_with(
src,
LexConfig {
flavor: LatexFlavor::Document,
dtx: true,
},
);
assert!(out.is_empty());
}
}