Skip to main content

miniextendr_lint/
misplaced_dots.rs

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.
12
13use proc_macro2::{Delimiter, Spacing, TokenStream, TokenTree};
14
15/// A Rust `...` parameter followed by another parameter.
16struct MisplacedDots {
17    /// The parameter's name; `None` for unnamed dots (a bare `...` or `_: ...`).
18    name: Option<String>,
19    /// The three `.` tokens.
20    dots: TokenStream,
21}
22
23/// 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 {
30    let 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}
37
38/// 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> {
41    let found = find(src.parse().ok()?)?;
42    let line = found
43        .dots
44        .into_iter()
45        .next()
46        .map_or(0, |dot| dot.span().start().line);
47    Some(::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}
49
50/// The first `...` parameter that another parameter follows, in any `fn` signature in `tokens`.
51fn find(tokens: TokenStream) -> Option<MisplacedDots> {
52    let tts: Vec<TokenTree> = tokens.into_iter().collect();
53    for (i, tt) in tts.iter().enumerate() {
54        match tt {
55            TokenTree::Ident(kw) if kw == "fn" => {
56                if 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                {
60                    return Some(found);
61                }
62            }
63            // Module, impl and trait bodies hold more functions.
64            TokenTree::Group(group) => {
65                if let Some(found) = find(group.stream()) {
66                    return Some(found);
67                }
68            }
69            _ => {}
70        }
71    }
72    None
73}
74
75/// The parameter list after `fn name`: the first parenthesized group outside the
76/// generics' angle brackets.
77fn params_group(after_name: &[TokenTree]) -> Option<TokenStream> {
78    let mut depth = 0usize;
79    let mut after_minus = false;
80    for tt in after_name {
81        match tt {
82            TokenTree::Group(group) if depth == 0 => {
83                return (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.
87            TokenTree::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    }
95    None
96}
97
98/// A `...` parameter in `params` with another parameter after it (a trailing comma is fine).
99fn in_params(params: TokenStream) -> Option<MisplacedDots> {
100    let tts: Vec<TokenTree> = params.into_iter().collect();
101    (0..tts.len()).find_map(|i| {
102        let dots = dots_at(&tts, i)?;
103        let 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() == ',');
104        if !comma_next || tts.len() <= i + 4 {
105            return None;
106        }
107        let name = match (
108            i.checked_sub(2).map(|j| &tts[j]),
109            i.checked_sub(1).map(|j| &tts[j]),
110        ) {
111            (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(colon)))
112                if colon.as_char() == ':' && name != "_" =>
113            {
114                Some(name.to_string())
115            }
116            _ => None,
117        };
118        Some(MisplacedDots { name, dots })
119    })
120}
121
122/// The three `.` tokens of a `...` that starts at `tts[i]`.
123fn dots_at(tts: &[TokenTree], i: usize) -> Option<TokenStream> {
124    let 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.
127    let 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}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn hint_names_the_parameter_and_line() {
138        let src = "use x::Strs;\n\n#[miniextendr]\npub fn f(sources: ..., dosing_type: Strs) {}\n";
139        let hint = hint(src).expect("hint");
140        assert!(hint.starts_with("line 4: Rust's `...` is only valid as the last parameter"));
141        assert!(hint.contains("`sources: &Dots`"));
142    }
143
144    #[test]
145    fn bare_dots_get_a_placeholder_name() {
146        let hint = hint("mod m { fn f(x: i32, ..., y: i32) {} }").expect("hint");
147        assert!(hint.contains("`_dots: &Dots`"));
148    }
149
150    /// `_: ...` is unnamed dots too: `_dots: &Dots` works on a method, where
151    /// `_: &Dots` needs a plain name.
152    #[test]
153    fn wild_dots_get_the_placeholder_name() {
154        let hint = hint("impl T { fn m(&self, _: ..., n: i32) {} }").expect("hint");
155        assert!(hint.contains("`_dots: &Dots`"), "got: {hint}");
156        assert!(!hint.contains("`_: &Dots`"), "got: {hint}");
157    }
158
159    #[test]
160    fn method_in_an_impl_body() {
161        assert!(hint("impl T { fn m(&self, rest: ..., n: i32) {} }").is_some());
162    }
163
164    #[test]
165    fn last_dots_and_comments_get_no_hint() {
166        assert_eq!(hint("fn f(x: i32, rest: ...) {}"), None);
167        assert_eq!(hint("fn f(x: i32, rest: ...,) {}"), None);
168        assert_eq!(hint("fn f(x: i32, _: ...) {}"), None);
169        assert_eq!(hint("// fn f(rest: ..., x: i32) {}\nfn g() {}"), None);
170        assert_eq!(hint("/// fn f(rest: ..., x: i32) {}\nfn g() {}"), None);
171    }
172
173    #[test]
174    fn generics_before_the_parameters() {
175        assert!(hint("fn f<'a, F: Fn(i32) -> i32>(rest: ..., f: &'a F) {}").is_some());
176    }
177}