workspacer_syntax/
generate_function_signature.rs

1// ---------------- [ File: workspacer-syntax/src/generate_function_signature.rs ]
2crate::ix!();
3
4/// Minimal post-processing to ensure correct spacing around arrows, where clauses, etc.
5pub fn post_process_spacing(signature: &str) -> String {
6    signature
7        .replace(")->", ") ->")
8        .replace(">where", "> where")
9}
10
11#[derive(Debug, Clone)]
12pub struct FnSignatureGenerator(ast::Fn);
13
14impl GenerateSignature for ast::Fn {
15
16    fn generate_signature_with_opts(&self, opts: &SignatureOptions) -> String {
17        use tracing::{debug, trace};
18
19        trace!("Generating signature for ast::Fn with opts: {:?}", opts);
20
21        // 1) Possibly gather doc lines
22        let doc_text = if *opts.include_docs() {
23            extract_docs(&self.syntax())
24                .map(|d| format!("{}\n", d))
25                .unwrap_or_default()
26        } else {
27            "".to_string()
28        };
29
30        // 2) Gather visibility, async, etc.
31        let vis_str = self
32            .visibility()
33            .map(|v| format!("{} ", v.syntax().text()))
34            .unwrap_or_default();
35
36        let async_str = if let Some(token) = self.async_token() {
37            format!("{} ", token.text())
38        } else {
39            "".to_string()
40        };
41
42        let fn_keyword = "fn";
43        let name_str = self
44            .name()
45            .map(|n| n.text().to_string())
46            .unwrap_or_else(|| "<anon>".to_string());
47
48        // 3) Generic params
49        let generic_params = self
50            .generic_param_list()
51            .map(|gp| gp.syntax().text().to_string())
52            .unwrap_or_default();
53
54        // 4) Gather parameter entries (including self)
55        let mut param_entries = Vec::new();
56        if let Some(plist) = self.param_list() {
57            debug!(?plist, "Found param_list for fn");
58
59            // Possibly handle "self"
60            if let Some(sp) = plist.self_param() {
61                let has_amp = sp.amp_token().is_some();
62                let has_mut = sp.mut_token().is_some();
63                let lifetime_str = sp
64                    .lifetime()
65                    .map(|lt| lt.syntax().text().to_string())
66                    .unwrap_or_default();
67
68                let mut name_part = String::new();
69                if has_amp {
70                    name_part.push('&');
71                    if !lifetime_str.is_empty() {
72                        name_part.push_str(&lifetime_str);
73                        name_part.push(' ');
74                    }
75                }
76                if has_mut {
77                    name_part.push_str("mut ");
78                }
79                name_part.push_str("self");
80
81                // For "self", treat it as name="self", type="".
82                param_entries.push((name_part.trim_end().to_string(), "".to_string()));
83            }
84
85            // The rest of the param_list
86            for param in plist.params() {
87                if let Some(normal) = ast::Param::cast(param.syntax().clone()) {
88                    let pat_str = normal
89                        .pat()
90                        .map(|p| p.syntax().text().to_string())
91                        .unwrap_or_default();
92                    let ty_str = normal
93                        .ty()
94                        .map(|t| t.syntax().text().to_string())
95                        .unwrap_or_default();
96
97                    if !pat_str.is_empty() && !ty_str.is_empty() {
98                        param_entries.push((pat_str, ty_str));
99                    } else if !ty_str.is_empty() {
100                        param_entries.push((ty_str, "".to_string()));
101                    } else if !pat_str.is_empty() {
102                        param_entries.push((pat_str, "".to_string()));
103                    } else {
104                        param_entries.push(("<unknown_param>".to_string(), "".to_string()));
105                    }
106                } else {
107                    param_entries.push(("<unrecognized_param>".to_string(), "".to_string()));
108                }
109            }
110        }
111
112        let param_count = param_entries.len();
113
114        // 5) Return type
115        let ret_str = if let Some(ret_type) = self.ret_type() {
116            if let Some(ty_node) = ret_type.ty() {
117                format!(" -> {}", ty_node.syntax().text())
118            } else {
119                "".to_string()
120            }
121        } else {
122            "".to_string()
123        };
124
125        // 6) Where clause
126        let where_str = full_clean_where_clause(&self.where_clause());
127
128        // 7) Build final lines
129        //    We'll produce something like:
130        //
131        //    <docs?>
132        //    pub async fn name<T>(...) -> ...
133        //    or multiline version, but with the opening "(" on the same line as "name<T>"
134
135        // Prefix = "pub async fn name<T>"
136        let prefix_line = format!("{vis_str}{async_str}{fn_keyword} {name_str}{generic_params}");
137
138        // If we have <= 3 parameters, do a single-line param string:
139        let multiline: bool = param_count > 3;
140
141        let mut param_str = String::new();
142        if param_count == 0 {
143            // no params => "()"
144            param_str.push_str("()");
145        } else if !multiline {
146            // single-line approach
147            let joined: Vec<String> = param_entries
148                .iter()
149                .map(|(n, t)| {
150                    if t.is_empty() {
151                        n.to_string()
152                    } else {
153                        format!("{}: {}", n, t)
154                    }
155                })
156                .collect();
157            param_str.push('(');
158            param_str.push_str(&joined.join(", "));
159            param_str.push(')');
160        } else {
161            // multiline approach => align
162            // 1) find longest name part
163            let max_name_len = param_entries
164                .iter()
165                .map(|(n, _)| n.len())
166                .max()
167                .unwrap_or(0);
168
169            // open paren on the same line
170            param_str.push('(');
171
172            // next lines for each param
173            for (i, (name_part, ty_part)) in param_entries.iter().enumerate() {
174                param_str.push('\n');
175                // indent 4 spaces
176                param_str.push_str("    ");
177                let spacing_needed = max_name_len.saturating_sub(name_part.len());
178                if ty_part.is_empty() {
179                    // e.g. "self"
180                    param_str.push_str(name_part);
181                    // trailing comma
182                    if i + 1 < param_count {
183                        param_str.push(',');
184                    }
185                } else {
186                    // e.g. "name: Type"
187                    param_str.push_str(name_part);
188                    param_str.push_str(": ");
189                    param_str.push_str(&" ".repeat(spacing_needed));
190                    param_str.push_str(ty_part);
191                    // trailing comma
192                    if i + 1 < param_count {
193                        param_str.push(',');
194                    }
195                }
196            }
197            param_str.push('\n');
198            param_str.push(')');
199        }
200
201        // If we have a where clause, attach it after ret_str
202        // So e.g. " -> i32 where T: Debug"
203        let suffix = if !where_str.is_empty() {
204            format!("{ret_str} {where_str}")
205        } else {
206            ret_str
207        };
208
209        // If multiline, we typically place suffix on the same line as ")"
210        // but we ended up with no trailing place. Let's see:
211        // We'll build the final line: param_str + suffix
212        let final_func_line = if multiline {
213            // Insert suffix right after the closing parenthesis
214            // Possibly check if param_str ends with ')' or not
215            if suffix.trim().is_empty() {
216                param_str
217            } else {
218                format!("{param_str}{suffix}")
219            }
220        } else {
221            // single-line => e.g. "(self, x: i32) -> i32 where T: Debug"
222            format!("{}{}", param_str, suffix)
223        };
224
225        // Combine everything. If user wants a semicolon appended, do so:
226        let raw_sig = format!("{prefix_line}{final_func_line}");
227
228        let combined = match opts.add_semicolon() {
229            true => format!("{doc_text}{raw_sig};"),
230            false => format!("{doc_text}{raw_sig}"),
231        };
232
233        post_process_spacing(&combined)
234    }
235}