use crate::config::{CompatConfig, CompatVersion};
use crate::linter::diagnostic::{Diagnostic, Fix, ViolationData};
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct RCompat;
const EXAMPLES: &[Example] = &[Example {
caption: "The native pipe under a declared `R (>= 4.0)` floor \
(`r = \"4.0\"` under `[compat]` in `arity.toml`):",
source: "y <- c(1, 2) |> sum()\nprint(y)\n",
}];
struct Requirement {
label: &'static str,
version: &'static str,
}
impl Rule for RCompat {
fn id(&self) -> &'static str {
"r-compat"
}
fn description(&self) -> &'static str {
"Flag syntax newer than the project's minimum supported R version.\
\n\nThe floor comes from `[compat] r` in `arity.toml`, or from \
`Depends: R (>= …)` in the package `DESCRIPTION` when the key is \
unset; with neither, the rule stays silent. Raw strings (`r\"(…)\"`) \
need R 4.0.0, the native pipe `|>` and the lambda shorthand `\\(x)` \
need 4.1.0, and the pipe placeholder `_` needs 4.2.0 — on an older \
R, each is a syntax error. Only the lambda carries a fix \
(`function(x)` is its exact meaning); the pipe and raw strings have \
no drop-in textual equivalent, so those findings are report-only."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn doc_compat(&self) -> CompatConfig {
CompatConfig {
r: Some("4.0".to_string()),
roxygen2: None,
}
}
fn interests(&self) -> &'static [SyntaxKind] {
&[
SyntaxKind::PIPE,
SyntaxKind::FUNCTION_KW,
SyntaxKind::STRING,
SyntaxKind::IDENT,
]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(token) = el.as_token() else {
return;
};
let requirement = match el.kind() {
SyntaxKind::PIPE => Requirement {
label: "the native pipe `|>`",
version: "4.1.0",
},
SyntaxKind::FUNCTION_KW if token.text() == "\\" => Requirement {
label: "the lambda shorthand `\\(…)`",
version: "4.1.0",
},
SyntaxKind::STRING if is_raw_string(token.text()) => Requirement {
label: "a raw string literal",
version: "4.0.0",
},
SyntaxKind::IDENT if token.text() == "_" => Requirement {
label: "the pipe placeholder `_`",
version: "4.2.0",
},
_ => return,
};
let Some(floor) = ctx.r_compat_floor() else {
return;
};
let required = CompatVersion::parse(requirement.version).expect("table versions are valid");
if floor >= required {
return;
}
let range = token.text_range();
let fix = (el.kind() == SyntaxKind::FUNCTION_KW).then(|| {
Fix::safe(
usize::from(range.start()),
usize::from(range.end()),
"function".to_string(),
"Replace `\\` with `function`".to_string(),
)
});
sink.push(Diagnostic {
rule: "r-compat",
severity: Default::default(),
path: Default::default(),
range,
message: ViolationData::new(
"r-compat",
format!(
"{} requires R >= {}, but this project supports R >= {floor}",
requirement.label, requirement.version
),
)
.with_suggestion(
"Raise the floor (`[compat] r` in `arity.toml`, or `Depends: R (>= …)` \
in `DESCRIPTION`) or rewrite with older syntax.",
),
fix,
});
}
}
fn is_raw_string(text: &str) -> bool {
let mut chars = text.chars();
matches!(chars.next(), Some('r' | 'R')) && matches!(chars.next(), Some('"' | '\''))
}