1//! Hint for a file that fails to parse because a Rust `...` parameter is not the
2//! last one (#1737).
3//!
4//! Rust's `...` parameter parses only in last position, so a file holding
5//! `fn f(sources: ..., dosing_type: Strs)` fails `syn::parse_file` and no rule
6//! checks it. [`hint`] tokenizes the source (which still works, and drops
7//! comments) and names the cause and the `&Dots` spelling, for the lint to
8//! append to its "failed to parse" warning.
9//!
10//! Mirror: `miniextendr-macros/src/misplaced_dots.rs` gives `#[miniextendr]` the
11//! same error. Keep the detector and [`message`] in sync.
1213use proc_macro2::{Delimiter, Spacing, TokenStream, TokenTree};
1415/// A Rust `...` parameter followed by another parameter.
16struct MisplacedDots {
17/// The parameter's name; `None` for unnamed dots (a bare `...` or `_: ...`).
18name: Option<String>,
19/// The three `.` tokens.
20dots: TokenStream,
21}
2223/// The diagnostic for a `...` parameter that is not last, named `name` (`None` for unnamed dots).
24///
25/// Unnamed dots get `_dots`: `_: &Dots` would work on a function (its `_`
26/// parameters are renamed) but not on a method, where the `&Dots` parameter
27/// needs a plain name. No spelling of Rust `...` parses before another
28/// parameter, so the hint names `&Dots`, never `_: ...`.
29fn message(name: Option<&str>) -> String {
30let param = name.unwrap_or("_dots");
31::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!(
32"Rust's `...` is only valid as the last parameter; write a dots parameter that is not \
33 last as `{param}: &Dots` (`miniextendr_api::dots::Dots`), which is R's `...` at that \
34 position. See https://a2-ai.github.io/miniextendr/manual/dots-typed-list/#formals-after"
35)36}
3738/// The hint for a source file that holds a `...` parameter with another parameter
39/// after it: `line N: <message>`. `None` when it holds none or does not tokenize.
40pub(crate) fn hint(src: &str) -> Option<String> {
41let found = find(src.parse().ok()?)?;
42let line = found43 .dots
44 .into_iter()
45 .next()
46 .map_or(0, |dot| dot.span().start().line);
47Some(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("line {1}: {0}",
message(found.name.as_deref()), line))
})format!("line {line}: {}", message(found.name.as_deref())))
48}
4950/// The first `...` parameter that another parameter follows, in any `fn` signature in `tokens`.
51fn find(tokens: TokenStream) -> Option<MisplacedDots> {
52let tts: Vec<TokenTree> = tokens.into_iter().collect();
53for (i, tt) in tts.iter().enumerate() {
54match tt {
55 TokenTree::Ident(kw) if kw == "fn" => {
56if let Some(TokenTree::Ident(_)) = tts.get(i + 1)
57 && let Some(params) = params_group(&tts[i + 2..])
58 && let Some(found) = in_params(params)
59 {
60return Some(found);
61 }
62 }
63// Module, impl and trait bodies hold more functions.
64TokenTree::Group(group) => {
65if let Some(found) = find(group.stream()) {
66return Some(found);
67 }
68 }
69_ => {}
70 }
71 }
72None73}
7475/// The parameter list after `fn name`: the first parenthesized group outside the
76/// generics' angle brackets.
77fn params_group(after_name: &[TokenTree]) -> Option<TokenStream> {
78let mut depth = 0usize;
79let mut after_minus = false;
80for tt in after_name {
81match tt {
82 TokenTree::Group(group) if depth == 0 => {
83return (group.delimiter() == Delimiter::Parenthesis).then(|| group.stream());
84 }
85 TokenTree::Punct(p) if p.as_char() == '<' => depth += 1,
86// The `>` of `->` (in a `Fn() -> T` bound) closes nothing.
87TokenTree::Punct(p) if p.as_char() == '>' && !after_minus => {
88 depth = depth.checked_sub(1)?;
89 }
90_ if depth == 0 => return None,
91_ => {}
92 }
93 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);
94 }
95None96}
9798/// A `...` parameter in `params` with another parameter after it (a trailing comma is fine).
99fn in_params(params: TokenStream) -> Option<MisplacedDots> {
100let tts: Vec<TokenTree> = params.into_iter().collect();
101 (0..tts.len()).find_map(|i| {
102let dots = dots_at(&tts, i)?;
103let 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() == ',');
104if !comma_next || tts.len() <= i + 4 {
105return None;
106 }
107let name = match (
108i.checked_sub(2).map(|j| &tts[j]),
109i.checked_sub(1).map(|j| &tts[j]),
110 ) {
111 (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(colon)))
112if colon.as_char() == ':' && name != "_" =>
113 {
114Some(name.to_string())
115 }
116_ => None,
117 };
118Some(MisplacedDots { name, dots })
119 })
120}
121122/// The three `.` tokens of a `...` that starts at `tts[i]`.
123fn dots_at(tts: &[TokenTree], i: usize) -> Option<TokenStream> {
124let three = tts.get(i..i + 3)?;
125// The first two dots are joint (`...`, not `. . .`); the last one's spacing
126 // depends on what follows it.
127let 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));
128 (dot(&three[0], true) && dot(&three[1], true) && dot(&three[2], false))
129 .then(|| three.iter().cloned().collect())
130}
131132#[cfg(test)]
133mod tests {
134use super::*;
135136#[test]
137fn hint_names_the_parameter_and_line() {
138let src = "use x::Strs;\n\n#[miniextendr]\npub fn f(sources: ..., dosing_type: Strs) {}\n";
139let hint = hint(src).expect("hint");
140assert!(hint.starts_with("line 4: Rust's `...` is only valid as the last parameter"));
141assert!(hint.contains("`sources: &Dots`"));
142 }
143144#[test]
145fn bare_dots_get_a_placeholder_name() {
146let hint = hint("mod m { fn f(x: i32, ..., y: i32) {} }").expect("hint");
147assert!(hint.contains("`_dots: &Dots`"));
148 }
149150/// `_: ...` is unnamed dots too: `_dots: &Dots` works on a method, where
151 /// `_: &Dots` needs a plain name.
152#[test]
153fn wild_dots_get_the_placeholder_name() {
154let hint = hint("impl T { fn m(&self, _: ..., n: i32) {} }").expect("hint");
155assert!(hint.contains("`_dots: &Dots`"), "got: {hint}");
156assert!(!hint.contains("`_: &Dots`"), "got: {hint}");
157 }
158159#[test]
160fn method_in_an_impl_body() {
161assert!(hint("impl T { fn m(&self, rest: ..., n: i32) {} }").is_some());
162 }
163164#[test]
165fn last_dots_and_comments_get_no_hint() {
166assert_eq!(hint("fn f(x: i32, rest: ...) {}"), None);
167assert_eq!(hint("fn f(x: i32, rest: ...,) {}"), None);
168assert_eq!(hint("fn f(x: i32, _: ...) {}"), None);
169assert_eq!(hint("// fn f(rest: ..., x: i32) {}\nfn g() {}"), None);
170assert_eq!(hint("/// fn f(rest: ..., x: i32) {}\nfn g() {}"), None);
171 }
172173#[test]
174fn generics_before_the_parameters() {
175assert!(hint("fn f<'a, F: Fn(i32) -> i32>(rest: ..., f: &'a F) {}").is_some());
176 }
177}