arity 0.19.0

A language server, formatter, and linter for R
//! `blanket-suppression`: a directive that names no rule where it could have.
//!
//! Two shapes name no rule, and they fail in opposite directions:
//!
//! - `# arity-lint skip-file: <reason>` turns the whole linter off for the file,
//!   and `# arity-lint off: <reason>` does the same until the matching `on`.
//!   Every rule arity adds later is silently disabled there too, so the code
//!   quietly stops being checked as the rule set grows.
//! - `# arity-lint skip` with nothing after it suppresses *nothing* — there is
//!   no rule ID, so the directive is inert.
//!
//! Both are repaired the same way: name the rule. Report-only, because doing
//! that for the author means guessing which rules they meant — deleting the
//! blanket would un-suppress arbitrarily much, and inventing a rule list would
//! suppress things they never asked to hide.
//!
//! `# arity-lint skip-file <rule>: <reason>` is *not* flagged. It is broad in
//! range but scoped to one rule, and it is the legitimate idiom for a generated
//! file. Nor is `# arity skip: <reason>`: it covers every rule, but only over
//! the one statement in front of the author.

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

pub struct BlanketSuppression;

const EXAMPLES: &[Example] = &[
    Example {
        caption: "Disabling every rule for the file, including rules that do not exist yet:",
        source: "# arity-lint skip-file: generated by a script\nx <- 1\n",
    },
    Example {
        caption: "A directive with no rule ID suppresses nothing at all:",
        source: "# arity-lint skip\nx <- 1\n",
    },
];

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

    fn description(&self) -> &'static str {
        "Flags an `# arity-lint` directive that names no rule where it could \
have. `# arity-lint skip-file: <reason>` disables every rule for the file, and \
`# arity-lint off: <reason>` does so until the matching `on` — including every \
rule arity ships in the future, so the code quietly stops being checked as the \
rule set grows. A directive with nothing after the verb is the opposite \
failure: it names nothing, so it suppresses nothing. Both are fixed by naming \
the rule. Not flagged: the rule-scoped `# arity-lint skip-file <rule>`, broad \
in range but narrow in effect, and `# arity skip: <reason>`, broad in rules but \
bounded to one statement. Report-only — choosing the rules for the author would \
guess at intent in either direction."
    }

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

    fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
        for directive in ctx.suppressions.directives() {
            // A format directive names no rule because it cannot, and an `on`
            // closes whatever is open rather than naming anything.
            if !directive.has_rule_slot() {
                continue;
            }
            match directive.scope {
                RuleScope::Rule(_) => continue,
                // Every rule, but bounded to the one statement in front of the
                // author. That is the documented way to say "leave this alone".
                RuleScope::All if directive.verb == Verb::Skip => continue,
                _ => sink.push(report(directive)),
            }
        }
    }
}

fn report(directive: &Directive) -> Diagnostic {
    let (body, suggestion) = match (&directive.scope, directive.verb) {
        (RuleScope::All, Verb::Off) => (
            "this directive disables every lint rule until `# arity-lint on`",
            "scope it with `# arity-lint off <rule>: <reason>`",
        ),
        (RuleScope::All, _) => (
            "this directive disables every lint rule for the whole file",
            "scope it with `# arity-lint skip-file <rule>: <reason>`",
        ),
        _ => (
            "this directive names no rule, so it suppresses nothing",
            "name the rule: `# arity-lint skip <rule>: <reason>`",
        ),
    };
    Diagnostic {
        rule: "blanket-suppression",
        severity: Default::default(),
        path: Default::default(),
        range: directive.comment,
        message: ViolationData::new("blanket-suppression", body).with_suggestion(suggestion),
        fix: None,
    }
}