Skip to main content

miniextendr_lint/rules/
raw_r_eval.rs

1//! Raw R evaluation in a `#[miniextendr]` body.
2//!
3//! - MXL305: Warns on `Rf_eval` / `R_forceAndCall` / `Rf_applyClosure` /
4//!   `R_CheckUserInterrupt` (or their `_unchecked` forms) inside a `#[miniextendr]`
5//!   fn, a method of a `#[miniextendr]` impl block, or a default method of a
6//!   `#[miniextendr]` trait.
7//!   An R error or a pending interrupt there longjmps over the body's Rust frames:
8//!   their destructors don't run, and a receiver the generated code borrowed stays
9//!   borrowed for the rest of the session (#2093).
10
11use crate::crate_index::CrateIndex;
12use crate::diagnostic::Diagnostic;
13use crate::lint_code::LintCode;
14
15pub fn check(index: &CrateIndex, diagnostics: &mut Vec<Diagnostic>) {
16    for (path, data) in &index.file_data {
17        for (fn_name, line) in &data.raw_r_eval_calls {
18            let interrupt = fn_name.starts_with("R_CheckUserInterrupt");
19            let what = if interrupt {
20                "a pending interrupt"
21            } else {
22                "an R error in the code it runs"
23            };
24            let help = if interrupt {
25                "Use `#[miniextendr(check_interrupt)]` to check once, before the arguments \
26                 are converted and the receiver is borrowed. A check inside a long loop has \
27                 no safe helper yet (A2-ai/miniextendr#2101). Suppress an intentional site \
28                 with `// mxl::allow(MXL305)`."
29            } else {
30                "Run R code through the `miniextendr_api::expression` evaluators, which stop \
31                 an R error before it crosses a Rust frame: `RCall::eval` / `eval_as` and \
32                 `r_eval_str` return it as `Err` (the caller's condition handlers don't see \
33                 the evaluation); `eval_with_handlers` keeps the caller's handlers and \
34                 carries an R error through the Rust frames as an unwind; \
35                 `try_eval_with_handlers` keeps the handlers and returns the error as `Err`. \
36                 `Quoted::eval()` evaluates an unevaluated argument. For `R_forceAndCall` / \
37                 `Rf_applyClosure`, build the call with `RCall::from_sexp(f).arg(..)` and \
38                 evaluate it the same way. Suppress an intentional site with \
39                 `// mxl::allow(MXL305)`."
40            };
41            diagnostics.push(
42                Diagnostic::new(
43                    LintCode::MXL305,
44                    path,
45                    *line,
46                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("`{0}()` in a `#[miniextendr]` body: {1} longjmps over the body\'s Rust frames, so their destructors don\'t run and a receiver the generated code borrowed stays borrowed (`miniextendr_borrow_conflict`) for the rest of the R session.",
                fn_name, what))
    })format!(
47                        "`{fn_name}()` in a `#[miniextendr]` body: {what} longjmps over the \
48                         body's Rust frames, so their destructors don't run and a receiver \
49                         the generated code borrowed stays borrowed \
50                         (`miniextendr_borrow_conflict`) for the rest of the R session.",
51                    ),
52                )
53                .with_help(help),
54            );
55        }
56    }
57}