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