Skip to main content

sofie_model/
syntax.rs

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
23/// A file outside the crate's module tree is not in `parsed`, yet the model still reads it.
24pub(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
34/// `pub fn <name>(<receiver>: …)`, which `sofie gen` reads without compiling; with its receiver's name.
35pub(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
70/// As written, since a modifier's argument is copied into generated code.
71pub(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
93/// An editor's byte order mark is not Rust, but the compiler skips it.
94pub 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
119/// By its last segment, so `#[sofie::contract]` and `#[contract]` are the same attribute.
120pub(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
170/// A `/** */` doc is one attribute of several lines, each decorated with a `*`.
171pub(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
209/// `vec![x; n]` is no comma list; a body that is neither (`info!(key = %value)`) is skipped.
210pub(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}