arity 0.19.0

A language server, formatter, and linter for R
//! `deprecated-suppression`: a directive written in one of the spellings the
//! linter shipped with.
//!
//! `# arity-ignore <rule>` and `# arity-ignore-file <rule>` predate the verb
//! grammar. They still parse and behave *exactly* as `# arity-lint skip` and
//! `# arity-lint skip-file`, so this is a migration aid, not a correctness fix:
//! nothing is broken today, and nothing changes when the fix is applied. What it
//! buys is one spelling across a codebase before the aliases are removed.
//!
//! The fix replaces the prefix and nothing else. The rule ID, the reason prose,
//! and the author's spacing all sit outside the replaced span, which is what
//! makes it `Safe`: a suppression comment carries no program behavior, and the
//! rewritten directive covers the same rule over the same code.

use crate::directive::Spelling;
use crate::linter::diagnostic::{Diagnostic, Fix, ViolationData};
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::linter::suppression::Directive;

pub struct DeprecatedSuppression;

const EXAMPLES: &[Example] = &[
    Example {
        caption: "The shipped spelling of `# arity-lint skip`:",
        source: "# arity-ignore unused-binding: part of the documented API\nconfig <- list(width = 80)\n",
    },
    Example {
        caption: "…and of `# arity-lint skip-file`:",
        source: "# arity-ignore-file unused-binding: generated by tools/codegen.R\nx <- 1\n",
    },
];

impl Rule for DeprecatedSuppression {
    fn id(&self) -> &'static str {
        "deprecated-suppression"
    }

    fn description(&self) -> &'static str {
        "Flags `# arity-ignore` and `# arity-ignore-file`, the spellings the \
linter shipped with, and rewrites them to `# arity-lint skip` and \
`# arity-lint skip-file`. Both still parse and behave identically, so nothing \
is broken and nothing changes when the fix is applied — this is a migration \
aid, so that a codebase reaches one spelling before the aliases are removed. \
The fix is `Safe` and replaces the prefix alone: the rule ID, the reason, and \
the author's spacing are left exactly as written. Directives in a `DESCRIPTION` \
are not covered, as with every `meta` rule."
    }

    fn examples(&self) -> &'static [Example] {
        EXAMPLES
    }

    fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
        for directive in ctx.suppressions.directives() {
            if directive.spelling == Spelling::Deprecated {
                sink.push(report(directive));
            }
        }
    }
}

fn report(directive: &Directive) -> Diagnostic {
    let written = directive.tool.prefix();
    let verb = directive.verb.as_str();
    let replacement = format!("{written} {verb}");
    Diagnostic {
        rule: "deprecated-suppression",
        severity: Default::default(),
        path: Default::default(),
        range: directive.prefix,
        message: ViolationData::new(
            "deprecated-suppression",
            format!("this spelling is deprecated; it means `# {replacement}`"),
        )
        .with_suggestion(format!("write `# {replacement}` instead")),
        fix: Some(Fix::safe(
            directive.prefix.start().into(),
            directive.prefix.end().into(),
            replacement.clone(),
            format!("Replace with `{replacement}`"),
        )),
    }
}