1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use proc_macro2::TokenStream;
7use syn::punctuated::Punctuated;
8use syn::spanned::Spanned;
9use syn::{
10 Attribute, Expr, ExprMethodCall, FnArg, GenericArgument, Item, ItemFn, Lit, Macro, Pat,
11 PathArguments, PathSegment, Token, Type, UseTree, Visibility,
12};
13
14use crate::ModelError;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct UsePath {
18 pub segments: Vec<String>,
19 pub binding: Option<String>,
20 pub line: usize,
21}
22
23pub(crate) fn parsed_or_read<'parsed>(
25 parsed: &'parsed BTreeMap<PathBuf, syn::File>,
26 file: &Path,
27) -> Result<Cow<'parsed, syn::File>, ModelError> {
28 match parsed.get(file) {
29 Some(syntax) => Ok(Cow::Borrowed(syntax)),
30 None => Ok(Cow::Owned(read_file(file)?)),
31 }
32}
33
34pub(crate) fn builder_function<'items>(
36 file: &Path,
37 items: &'items [Item],
38 name: &str,
39 signature: &str,
40) -> Result<(&'items ItemFn, String), ModelError> {
41 let function = items
42 .iter()
43 .find_map(|item| match item {
44 Item::Fn(function) if function.sig.ident == name && is_public(&function.vis) => {
45 Some(function)
46 }
47 _ => None,
48 })
49 .ok_or_else(|| {
50 ModelError::in_file(
51 file,
52 format!(
53 "no {signature} found; add it, since `sofie gen` wires this function into the app"
54 ),
55 )
56 })?;
57 let mut inputs = function.sig.inputs.iter();
58 if let (Some(FnArg::Typed(argument)), None) = (inputs.next(), inputs.next())
59 && let Pat::Ident(receiver) = argument.pat.as_ref()
60 {
61 return Ok((function, receiver.ident.to_string()));
62 }
63 Err(ModelError::new(
64 file,
65 line_of(&function.sig),
66 format!("the {name} function takes exactly one argument: write {signature}"),
67 ))
68}
69
70pub(crate) fn single_argument(call: &ExprMethodCall) -> Option<String> {
72 match (call.args.len(), call.args.first()) {
73 (1, Some(argument)) => Some(
74 argument
75 .span()
76 .source_text()
77 .unwrap_or_else(|| "…".to_owned()),
78 ),
79 _ => None,
80 }
81}
82
83pub(crate) fn is_public(visibility: &Visibility) -> bool {
84 matches!(visibility, Visibility::Public(_))
85}
86
87pub(crate) fn read_file(file: &Path) -> Result<syn::File, ModelError> {
88 let text = fs::read_to_string(file)
89 .map_err(|error| ModelError::in_file(file, format!("cannot read the file: {error}")))?;
90 parse_tokens(file, tokenize(file, &text)?)
91}
92
93pub fn tokenize(file: &Path, text: &str) -> Result<TokenStream, ModelError> {
95 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
96 text.parse().map_err(|error: proc_macro2::LexError| {
97 ModelError::new(
98 file,
99 error.span().start().line,
100 format!("invalid Rust: {error}"),
101 )
102 })
103}
104
105pub fn parse_tokens(file: &Path, tokens: TokenStream) -> Result<syn::File, ModelError> {
106 syn::parse2(tokens).map_err(|error| {
107 ModelError::new(
108 file,
109 line_of(&error.span()),
110 format!("invalid Rust: {error}"),
111 )
112 })
113}
114
115pub(crate) fn line_of(node: &impl Spanned) -> usize {
116 node.span().start().line
117}
118
119pub(crate) fn is_attribute(attribute: &Attribute, name: &str) -> bool {
121 path_ends_with(attribute.path(), &[name])
122}
123
124pub(crate) fn has_attribute(attributes: &[Attribute], name: &str) -> bool {
125 attributes
126 .iter()
127 .any(|attribute| is_attribute(attribute, name))
128}
129
130pub(crate) fn path_ends_with(path: &syn::Path, names: &[&str]) -> bool {
131 path.segments.len() >= names.len()
132 && path
133 .segments
134 .iter()
135 .rev()
136 .zip(names.iter().rev())
137 .all(|(segment, name)| segment.ident == name)
138}
139
140pub(crate) fn segments(path: &syn::Path) -> Vec<String> {
141 path.segments
142 .iter()
143 .map(|segment| segment.ident.to_string())
144 .collect()
145}
146
147pub(crate) fn last_name(path: &syn::Path) -> Option<String> {
148 path.segments
149 .last()
150 .map(|segment| segment.ident.to_string())
151}
152
153pub(crate) fn type_last_name(ty: &Type) -> Option<String> {
154 match ty {
155 Type::Path(path) => last_name(&path.path),
156 _ => None,
157 }
158}
159
160pub(crate) fn first_type_argument(segment: &PathSegment) -> Option<&Type> {
161 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
162 return None;
163 };
164 match arguments.args.first() {
165 Some(GenericArgument::Type(argument)) => Some(argument),
166 _ => None,
167 }
168}
169
170pub(crate) fn doc_lines(attributes: &[Attribute]) -> Vec<String> {
172 attributes
173 .iter()
174 .filter_map(|attribute| match &attribute.meta {
175 syn::Meta::NameValue(pair) if pair.path.is_ident("doc") => string_literal(&pair.value),
176 _ => None,
177 })
178 .flat_map(|text| {
179 if !text.contains('\n') {
180 return vec![text.trim().to_owned()];
181 }
182 let lines: Vec<&str> = text
183 .lines()
184 .map(|line| {
185 let line = line.trim();
186 line.strip_prefix('*').unwrap_or(line).trim()
187 })
188 .collect();
189 let first = lines.iter().position(|line| !line.is_empty());
190 let last = lines.iter().rposition(|line| !line.is_empty());
191 match (first, last) {
192 (Some(first), Some(last)) => lines[first..=last]
193 .iter()
194 .map(|line| (*line).to_owned())
195 .collect(),
196 _ => Vec::new(),
197 }
198 })
199 .collect()
200}
201
202pub(crate) fn doc_line(attributes: &[Attribute]) -> Option<String> {
203 doc_lines(attributes)
204 .into_iter()
205 .next()
206 .filter(|line| !line.is_empty())
207}
208
209pub(crate) fn macro_expressions(call: &Macro) -> Vec<Expr> {
211 if let Ok(list) = call.parse_body_with(Punctuated::<Expr, Token![,]>::parse_terminated) {
212 return list.into_iter().collect();
213 }
214 call.parse_body_with(Punctuated::<Expr, Token![;]>::parse_terminated)
215 .map(|repeated| repeated.into_iter().collect())
216 .unwrap_or_default()
217}
218
219pub(crate) fn string_literal(expression: &Expr) -> Option<String> {
220 let Expr::Lit(literal) = expression else {
221 return None;
222 };
223 match &literal.lit {
224 Lit::Str(text) => Some(text.value()),
225 _ => None,
226 }
227}
228
229pub(crate) fn public_uses(items: &[Item]) -> impl Iterator<Item = UsePath> {
230 items
231 .iter()
232 .filter_map(|item| match item {
233 Item::Use(item_use) if is_public(&item_use.vis) => Some(use_paths(&item_use.tree)),
234 _ => None,
235 })
236 .flatten()
237}
238
239#[must_use]
240pub fn use_paths(tree: &UseTree) -> Vec<UsePath> {
241 let mut found = Vec::new();
242 collect_use_paths(tree, &mut Vec::new(), &mut found);
243 found
244}
245
246fn collect_use_paths(tree: &UseTree, prefix: &mut Vec<String>, found: &mut Vec<UsePath>) {
247 match tree {
248 UseTree::Path(path) => {
249 prefix.push(path.ident.to_string());
250 collect_use_paths(&path.tree, prefix, found);
251 prefix.pop();
252 }
253 UseTree::Name(name) => found.push(named(prefix, &name.ident, &name.ident)),
254 UseTree::Rename(rename) => found.push(named(prefix, &rename.ident, &rename.rename)),
255 UseTree::Glob(glob) => found.push(UsePath {
256 segments: prefix.clone(),
257 binding: None,
258 line: line_of(glob),
259 }),
260 UseTree::Group(group) => {
261 for nested in &group.items {
262 collect_use_paths(nested, prefix, found);
263 }
264 }
265 }
266}
267
268fn named(prefix: &[String], target: &syn::Ident, binding: &syn::Ident) -> UsePath {
269 let mut segments = prefix.to_vec();
270 if target != "self" {
271 segments.push(target.to_string());
272 }
273 let binding = if binding == "self" {
274 prefix.last().cloned()
275 } else {
276 Some(binding.to_string())
277 };
278 UsePath {
279 segments,
280 binding,
281 line: line_of(target),
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::{UsePath, doc_lines, use_paths};
288
289 #[test]
290 fn a_block_doc_gives_one_line_per_line_without_its_stars() {
291 let item: syn::ItemStruct = syn::parse_str(
292 "/// First.\n/**\n * Second,\n * third.\n */\n/** Fourth. */\nstruct Config;",
293 )
294 .unwrap();
295
296 assert_eq!(
297 doc_lines(&item.attrs),
298 ["First.", "Second,", "third.", "Fourth."]
299 );
300 }
301
302 fn paths(item: &str) -> Vec<(String, Option<String>)> {
303 let item: syn::ItemUse = syn::parse_str(item).unwrap();
304 use_paths(&item.tree)
305 .into_iter()
306 .map(
307 |UsePath {
308 segments, binding, ..
309 }| (segments.join("::"), binding),
310 )
311 .collect()
312 }
313
314 #[test]
315 fn expands_groups_renames_self_and_globs() {
316 let found = paths("use a::{b, c as d, e::{self}, f::{self as g}, h::*};");
317
318 let expected = [
319 ("a::b", Some("b")),
320 ("a::c", Some("d")),
321 ("a::e", Some("e")),
322 ("a::f", Some("g")),
323 ("a::h", None),
324 ]
325 .map(|(path, binding)| (path.to_owned(), binding.map(str::to_owned)));
326 assert_eq!(found, expected);
327 }
328}