use crate::linter::diagnostic::{Diagnostic, Fix, ViolationData};
use crate::linter::rules::matchers;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct ClassEquals;
const EXAMPLES: &[Example] = &[Example {
caption: "Testing an object's class:",
source: "if (class(x) == \"factor\") levels(x)\n",
}];
impl Rule for ClassEquals {
fn id(&self) -> &'static str {
"class-equals"
}
fn description(&self) -> &'static str {
"Flag comparisons of `class(x)` against a string literal—`class(x) == \
\"cls\"`, `class(x) != \"cls\"`, and the `%in%` membership \
spellings—which are `inherits(x, \"cls\")` (or its negation): \
`class()` returns the whole class vector, so the comparison is \
elementwise (an error in an `if ()` condition on a multi-class \
object) and misses subclasses, while `inherits()` asks the intended \
question directly without materializing the vector.\n\nThe rule \
fires only on the clean single-argument shape and only when `class` \
resolves to base R; a local redefinition is left alone. The fix is \
**unsafe**: on a multi-class object the comparison yields an \
elementwise vector where `inherits()` yields a scalar, and for S4 \
objects `inherits()` follows the formal inheritance chain."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::BINARY_EXPR]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let Some((lhs, op, rhs)) = matchers::binary_parts(node) else {
return;
};
let negate = match op.kind() {
SyntaxKind::EQUAL2 => false,
SyntaxKind::NOT_EQUAL => true,
SyntaxKind::USER_OP if op.text() == "%in%" => false,
_ => return,
};
let (call_el, string_el) = if is_string_literal(&rhs) {
(&lhs, &rhs)
} else if is_string_literal(&lhs) {
(&rhs, &lhs)
} else {
return;
};
let Some(call) = call_el
.as_node()
.and_then(|n| matchers::call_named(n, "class"))
else {
return;
};
let Some(arg) = matchers::sole_positional(&call) else {
return;
};
if !ctx.resolves_to_base(&call) {
return;
}
let r = node.text_range();
let (arg_range, string_range) = (arg.text_range(), string_el.text_range());
let drops_comment = node.descendants_with_tokens().any(|e| {
e.kind() == SyntaxKind::COMMENT
&& !arg_range.contains_range(e.text_range())
&& !string_range.contains_range(e.text_range())
});
let context_ok = !negate || matchers::is_safe_splice_context(node);
let fix = (!drops_comment && context_ok).then(|| {
let bang = if negate { "!" } else { "" };
Fix::unsafe_(
usize::from(r.start()),
usize::from(r.end()),
format!(
"{bang}inherits({}, {})",
matchers::element_text(&arg),
matchers::element_text(string_el)
),
format!("Replace the `class()` comparison with `{bang}inherits(...)`"),
)
});
sink.push(Diagnostic {
rule: "class-equals",
severity: Default::default(),
path: Default::default(),
range: r,
message: ViolationData::new(
"class-equals",
"comparing `class(x)` to a string is fragile—`class()` returns a vector; \
`inherits()` asks directly",
)
.with_suggestion("Use `inherits(x, \"cls\")` (or `!inherits(x, \"cls\")`)."),
fix,
});
}
}
fn is_string_literal(el: &SyntaxElement) -> bool {
el.as_token()
.is_some_and(|t| matchers::string_literal(t).is_some())
}