arity 0.17.0

A language server, formatter, and linter for R
//! `blanket-suppression`: a `# arity-ignore` directive that names no rule.
//!
//! Two shapes name no rule, and they fail in opposite directions:
//!
//! - `# arity-ignore-file: <reason>` turns the whole linter off for the file.
//!   Every rule arity adds later is silently disabled there too, so the file
//!   quietly stops being checked as the rule set grows.
//! - `# arity-ignore` with nothing after it suppresses *nothing* — the parser
//!   finds 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-ignore-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.

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

pub struct BlanketSuppression;

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

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

    fn description(&self) -> &'static str {
        "Flags a `# arity-ignore` directive that names no rule. \
`# arity-ignore-file: <reason>` disables every rule for the file — including \
every rule arity ships in the future — so the file quietly stops being checked \
as the rule set grows. A bare `# arity-ignore` is the opposite failure: it \
names nothing, so it suppresses nothing. Both are fixed by naming the rule. \
The rule-scoped `# arity-ignore-file <rule>: <reason>` is not flagged; it is \
broad in range but narrow in effect. 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() {
            if directive.rule.is_some() {
                continue;
            }
            sink.push(report(directive));
        }
    }
}

fn report(directive: &Directive) -> Diagnostic {
    let (body, suggestion) = match directive.kind {
        DirectiveKind::FileAll => (
            "this directive disables every lint rule for the whole file",
            "scope it with `# arity-ignore-file <rule>: <reason>`",
        ),
        _ => (
            "this directive names no rule, so it suppresses nothing",
            "name the rule: `# arity-ignore <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,
    }
}