Skip to main content

miniextendr_lint/rules/
vctrs_self_ctor.rs

1//! MXL120: vctrs constructor returns `Self` / named type, or impl has an instance-method receiver.
2//!
3//! A `#[miniextendr(vctrs(...))]` impl has two hard invariants:
4//!
5//! 1. **Constructor return type** — the constructor (`fn new` or a method tagged
6//!    `#[miniextendr(constructor)]`) must NOT return `Self`, `&Self`, `&mut Self`,
7//!    `Box<Self>`, the named impl type, `Result<Self, _>`, or `Result<NamedType, _>`.
8//!    The generated R wrapper passes the return value to `vctrs::new_vctr()` /
9//!    `new_rcrd()` / `new_list_of()`, which require a plain vector payload, not an
10//!    `ExternalPtr` (`EXTPTRSXP`).
11//!
12//! 2. **Instance receivers** — no method may carry any form of `self` receiver
13//!    (`&self`, `&mut self`, `self`, `self: &ExternalPtr<Self>`, etc.).
14//!    A vctrs object is an S3-classed base vector; there is no Rust `Self` stored
15//!    inside the SEXP.  The C wrapper cannot reconstruct `Self` from a base vector,
16//!    so calling an instance method would panic at runtime.
17//!
18//! ## Mirror
19//!
20//! The same checks fire as proc-macro hard errors in
21//! `miniextendr-macros/src/miniextendr_impl.rs` (search `MXL120`).
22//! This lint is defence-in-depth: it catches the mistake during the
23//! build-time static-analysis pass (when the macro isn't being expanded,
24//! e.g. lint-only IDE runs, third-party tooling).
25//! Keep both implementations in sync: if the macro relaxes either check,
26//! update this rule too.
27
28use crate::crate_index::{CrateIndex, MethodReceiverKind};
29use crate::diagnostic::Diagnostic;
30use crate::lint_code::LintCode;
31
32pub fn check(index: &CrateIndex, diagnostics: &mut Vec<Diagnostic>) {
33    for (path, data) in &index.file_data {
34        for (impl_type, methods) in &data.impl_methods {
35            for entry in methods {
36                if entry.class_system != "vctrs" {
37                    continue;
38                }
39
40                // Check 1: constructor return type.
41                //
42                // A method is a vctrs constructor when it either:
43                //   (a) is named `new` with no instance receiver, OR
44                //   (b) carries `#[miniextendr(constructor)]`.
45                //
46                // Note: a method with an instance receiver named `new` is already
47                // caught by Check 2 below, so we restrict the `new`-heuristic to
48                // static methods only (receiver == None), matching the macro's
49                // `method.env != ReceiverKind::Ref && method.env != ReceiverKind::RefMut`
50                // guard in `miniextendr_impl.rs:2209-2212`.
51                let is_ctor = (entry.method_name == "new"
52                    && entry.receiver_kind == MethodReceiverKind::None)
53                    || entry.has_constructor_attr;
54
55                if is_ctor && return_type_is_self_or_named(&entry.return_type_str, impl_type) {
56                    diagnostics.push(
57                        Diagnostic::new(
58                            LintCode::MXL120,
59                            path,
60                            entry.line,
61                            format!(
62                                "[MXL120] vctrs constructor `{}` must not return `Self` or `{}`.\n\
63                                 \n\
64                                 The generated R wrapper passes the constructor result to \
65                                 `vctrs::new_vctr()` (or `new_rcrd`/`new_list_of`), which requires a \
66                                 plain vector payload — not an ExternalPtr (`EXTPTRSXP`).\n\
67                                 \n\
68                                 Fix: return the vector payload directly instead of `Self`.\n\
69                                 For example, return `Vec<f64>` (for vctr), a \
70                                 `std::collections::HashMap` / named-list struct (for rcrd), or a \
71                                 `Vec<Vec<T>>` (for list_of).",
72                                entry.method_name, impl_type,
73                            ),
74                        )
75                        .with_help(
76                            "vctrs objects are S3-classed base vectors — there is no ExternalPtr \
77                             to return; return the plain vector payload instead",
78                        ),
79                    );
80                }
81
82                // Check 2: instance receivers are not supported on vctrs impls.
83                if entry.receiver_kind.is_instance() {
84                    diagnostics.push(
85                        Diagnostic::new(
86                            LintCode::MXL120,
87                            path,
88                            entry.line,
89                            format!(
90                                "[MXL120] vctrs impl method `{}` uses a `{}` receiver, which is \
91                                 not supported on `#[miniextendr(vctrs(...))]` impls.\n\
92                                 \n\
93                                 A vctrs object is an S3-classed base vector (REALSXP, INTSXP, \
94                                 etc.). There is no Rust `Self` stored inside the R SEXP — the \
95                                 vector payload IS the R object. The C wrapper cannot reconstruct \
96                                 `Self` from a base vector, so calling an instance method would \
97                                 panic at runtime.\n\
98                                 \n\
99                                 Fix: convert this method to a static method whose parameters \
100                                 receive the vector data directly.",
101                                entry.method_name,
102                                entry.receiver_kind.spelling(),
103                            ),
104                        )
105                        .with_help(
106                            "convert instance methods to static methods; pass the vector data \
107                             (e.g. `Vec<f64>`) as an explicit parameter instead of `self`",
108                        ),
109                    );
110                }
111            }
112        }
113    }
114}
115
116// region: Return-type predicate
117//
118// Mirror of `vctrs_ctor_returns_self_or_type` + `ty_is_self_or_named` in
119// `miniextendr-macros/src/miniextendr_impl.rs`.
120// Intentionally duplicated (not shared) — the lint crate must remain independent
121// of the macros crate at the type level.  When the macro changes the predicate,
122// update both.
123
124/// Returns true if `return_type_str` represents `Self`, `&Self`, `&mut Self`,
125/// `Box<Self>`, the named type, `Result<Self, _>`, or `Result<NamedType, _>`.
126///
127/// `return_type_str` is the stringified token form produced by
128/// `quote::ToTokens::to_token_stream()` on a `syn::ReturnType::Type`.
129/// An empty string means the return type is `()` (no explicit return) and
130/// always returns `false`.
131fn return_type_is_self_or_named(return_type_str: &str, type_name: &str) -> bool {
132    if return_type_str.is_empty() {
133        return false;
134    }
135    // Re-parse the stored token string.  If parsing fails (shouldn't happen for
136    // well-formed Rust), conservatively return false so we don't emit spurious errors.
137    let Ok(ty) = syn::parse_str::<syn::Type>(return_type_str) else {
138        return false;
139    };
140    ty_is_self_or_named(&ty, type_name)
141}
142
143/// Recursively checks whether `ty` is `Self`, `&Self`, `&mut Self`, `Box<Self>`,
144/// the named type, or `Result<(Self | NamedType), _>`.
145fn ty_is_self_or_named(ty: &syn::Type, type_name: &str) -> bool {
146    match ty {
147        syn::Type::Path(p) => {
148            let Some(last) = p.path.segments.last() else {
149                return false;
150            };
151            // Plain `Self` or `TypeName`
152            if last.ident == "Self" || last.ident == type_name {
153                return true;
154            }
155            // `Result<Self, _>` or `Result<TypeName, _>`
156            if last.ident == "Result"
157                && let syn::PathArguments::AngleBracketed(ref args) = last.arguments
158                && let Some(syn::GenericArgument::Type(first_ty)) = args.args.first()
159            {
160                return ty_is_self_or_named(first_ty, type_name);
161            }
162            // `Box<Self>` or `Box<TypeName>`
163            if last.ident == "Box"
164                && let syn::PathArguments::AngleBracketed(ref args) = last.arguments
165                && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
166            {
167                return ty_is_self_or_named(inner, type_name);
168            }
169            false
170        }
171        // `&Self` or `&mut Self`
172        syn::Type::Reference(r) => ty_is_self_or_named(r.elem.as_ref(), type_name),
173        _ => false,
174    }
175}
176
177// endregion