1//! Diagnostic for a Rust `...` parameter that is not the last one (#1737).
2//!
3//! Rust's `...` parameter parses only in last position, so
4//! `fn f(sources: ..., dosing_type: Strs)` is not a function to `syn`. The item
5//! fails `ItemFn` (and, as a method, `ItemImpl` / `ItemTrait`), which would leave
6//! `#[miniextendr]` with its struct/enum message. [`find`] recognises that shape
7//! at token level so the fallback can name the cause and the `&Dots` spelling.
8//!
9//! Mirror: `miniextendr-lint/src/misplaced_dots.rs` appends the same text to the
10//! lint's "failed to parse" warning. Keep the detector and [`message`] in sync.
1112use proc_macro2::{Delimiter, Spacing, TokenStream, TokenTree};
1314/// A Rust `...` parameter followed by another parameter.
15pub(crate) struct MisplacedDots {
16/// The parameter's name; `None` for unnamed dots (a bare `...` or `_: ...`).
17pub(crate) name: Option<String>,
18/// The three `.` tokens, so a diagnostic can span the whole `...`.
19pub(crate) dots: TokenStream,
20}
2122/// The diagnostic for a `...` parameter that is not last, named `name` (`None` for unnamed dots).
23///
24/// Unnamed dots get `_dots`: `_: &Dots` would work on a function (its `_`
25/// parameters are renamed) but not on a method, where the `&Dots` parameter
26/// needs a plain name. No spelling of Rust `...` parses before another
27/// parameter, so the hint names `&Dots`, never `_: ...`.
28pub(crate) fn message(name: Option<&str>) -> String {
29let param = name.unwrap_or("_dots");
30::alloc::__export::must_use({
::alloc::fmt::format(format_args!("Rust\'s `...` is only valid as the last parameter; write a dots parameter that is not last as `{0}: &Dots` (`miniextendr_api::dots::Dots`), which is R\'s `...` at that position. See https://a2-ai.github.io/miniextendr/manual/dots-typed-list/#formals-after",
param))
})format!(
31"Rust's `...` is only valid as the last parameter; write a dots parameter that is not \
32 last as `{param}: &Dots` (`miniextendr_api::dots::Dots`), which is R's `...` at that \
33 position. See https://a2-ai.github.io/miniextendr/manual/dots-typed-list/#formals-after"
34)35}
3637/// The first `...` parameter that another parameter follows, in any `fn` signature in `tokens`.
38pub(crate) fn find(tokens: TokenStream) -> Option<MisplacedDots> {
39let tts: Vec<TokenTree> = tokens.into_iter().collect();
40for (i, tt) in tts.iter().enumerate() {
41match tt {
42 TokenTree::Ident(kw) if kw == "fn" => {
43if let Some(TokenTree::Ident(_)) = tts.get(i + 1)
44 && let Some(params) = params_group(&tts[i + 2..])
45 && let Some(found) = in_params(params)
46 {
47return Some(found);
48 }
49 }
50// Impl and trait bodies hold methods.
51TokenTree::Group(group) => {
52if let Some(found) = find(group.stream()) {
53return Some(found);
54 }
55 }
56_ => {}
57 }
58 }
59None60}
6162/// The parameter list after `fn name`: the first parenthesized group outside the
63/// generics' angle brackets.
64fn params_group(after_name: &[TokenTree]) -> Option<TokenStream> {
65let mut depth = 0usize;
66let mut after_minus = false;
67for tt in after_name {
68match tt {
69 TokenTree::Group(group) if depth == 0 => {
70return (group.delimiter() == Delimiter::Parenthesis).then(|| group.stream());
71 }
72 TokenTree::Punct(p) if p.as_char() == '<' => depth += 1,
73// The `>` of `->` (in a `Fn() -> T` bound) closes nothing.
74TokenTree::Punct(p) if p.as_char() == '>' && !after_minus => {
75 depth = depth.checked_sub(1)?;
76 }
77_ if depth == 0 => return None,
78_ => {}
79 }
80 after_minus = #[allow(non_exhaustive_omitted_patterns)] match tt {
TokenTree::Punct(p) if p.as_char() == '-' && p.spacing() == Spacing::Joint
=> true,
_ => false,
}matches!(tt, TokenTree::Punct(p) if p.as_char() == '-' && p.spacing() == Spacing::Joint);
81 }
82None83}
8485/// A `...` parameter in `params` with another parameter after it (a trailing comma is fine).
86fn in_params(params: TokenStream) -> Option<MisplacedDots> {
87let tts: Vec<TokenTree> = params.into_iter().collect();
88 (0..tts.len()).find_map(|i| {
89let dots = dots_at(&tts, i)?;
90let comma_next = #[allow(non_exhaustive_omitted_patterns)] match tts.get(i + 3) {
Some(TokenTree::Punct(p)) if p.as_char() == ',' => true,
_ => false,
}matches!(tts.get(i + 3), Some(TokenTree::Punct(p)) if p.as_char() == ',');
91if !comma_next || tts.len() <= i + 4 {
92return None;
93 }
94let name = match (
95i.checked_sub(2).map(|j| &tts[j]),
96i.checked_sub(1).map(|j| &tts[j]),
97 ) {
98 (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(colon)))
99if colon.as_char() == ':' && name != "_" =>
100 {
101Some(name.to_string())
102 }
103_ => None,
104 };
105Some(MisplacedDots { name, dots })
106 })
107}
108109/// The three `.` tokens of a `...` that starts at `tts[i]`.
110fn dots_at(tts: &[TokenTree], i: usize) -> Option<TokenStream> {
111let three = tts.get(i..i + 3)?;
112// The first two dots are joint (`...`, not `. . .`); the last one's spacing
113 // depends on what follows it.
114let dot = |tt: &TokenTree, joint: bool| #[allow(non_exhaustive_omitted_patterns)] match tt {
TokenTree::Punct(p) if
p.as_char() == '.' && (!joint || p.spacing() == Spacing::Joint) =>
true,
_ => false,
}matches!(tt, TokenTree::Punct(p) if p.as_char() == '.' && (!joint || p.spacing() == Spacing::Joint));
115 (dot(&three[0], true) && dot(&three[1], true) && dot(&three[2], false))
116 .then(|| three.iter().cloned().collect())
117}
118119#[cfg(test)]
120mod tests {
121use super::*;
122123fn name_of(src: &str) -> Option<Option<String>> {
124 find(src.parse().unwrap()).map(|found| found.name)
125 }
126127#[test]
128fn named_dots_before_another_parameter() {
129assert_eq!(
130 name_of("pub fn f(sources: ..., dosing_type: Strs) {}"),
131Some(Some("sources".to_string()))
132 );
133 }
134135#[test]
136fn bare_dots_before_another_parameter() {
137assert_eq!(name_of("fn f(x: i32, ..., y: i32) {}"), Some(None));
138 }
139140/// `_: ...` is unnamed dots too, so the hint names `_dots: &Dots`, which a
141 /// method accepts (`_: &Dots` there needs a plain name).
142#[test]
143fn wild_dots_before_another_parameter_are_unnamed() {
144assert_eq!(name_of("fn f(x: i32, _: ..., y: i32) {}"), Some(None));
145assert_eq!(
146 name_of("impl T { fn m(&self, _: ..., n: i32) {} }"),
147Some(None)
148 );
149 }
150151#[test]
152fn method_in_an_impl_body() {
153assert_eq!(
154 name_of("impl T { pub fn m<'a>(&self, rest: ..., n: &'a str) {} }"),
155Some(Some("rest".to_string()))
156 );
157 }
158159#[test]
160fn generics_with_a_fn_bound() {
161assert_eq!(
162 name_of("fn f<F: Fn(i32) -> i32>(rest: ..., f: F) {}"),
163Some(Some("rest".to_string()))
164 );
165 }
166167#[test]
168fn last_dots_are_fine() {
169assert_eq!(name_of("fn f(x: i32, rest: ...) {}"), None);
170assert_eq!(name_of("fn f(x: i32, rest: ...,) {}"), None);
171assert_eq!(name_of("fn f(x: i32, ...) {}"), None);
172assert_eq!(name_of("fn f(x: i32, _: ...) {}"), None);
173 }
174175#[test]
176fn explicit_dots_type_is_fine() {
177assert_eq!(name_of("fn f(rest: &Dots, x: i32) {}"), None);
178 }
179180#[test]
181fn message_names_the_parameter() {
182let msg = message(Some("sources"));
183assert!(msg.starts_with("Rust's `...` is only valid as the last parameter"));
184assert!(msg.contains("`sources: &Dots`"));
185assert!(message(None).contains("`_dots: &Dots`"));
186 }
187}