Skip to main content

sofie_model/
references.rs

1use std::path::Path;
2
3use proc_macro2::{Ident, Punct, Spacing, TokenStream, TokenTree};
4
5use syn::punctuated::Punctuated;
6use syn::visit::{self, Visit};
7use syn::{Expr, ItemExternCrate, ItemUse, Macro, Token};
8
9use crate::manifest::is_manifest_constant;
10use crate::syntax::{line_of, read_file, segments, string_literal, use_paths};
11use crate::usage::{Mentions, public_items};
12use crate::{MacroCall, ModelError, PathReference, Source};
13
14impl Source {
15    pub fn read(file: &Path) -> Result<Self, ModelError> {
16        Ok(Self::of_syntax(file, &read_file(file)?))
17    }
18
19    pub(crate) fn of_syntax(file: &Path, syntax: &syn::File) -> Self {
20        let mut references = References::default();
21        references.visit_file(syntax);
22        Self {
23            file: file.to_path_buf(),
24            paths: references.paths,
25            macros: references.macros,
26            mentions: Mentions::of(syntax),
27            // Tooling reads a manifest's constant; no code has to name it.
28            public_items: public_items(syntax)
29                .into_iter()
30                .filter(|item| !is_manifest_constant(file, &item.name))
31                .collect(),
32        }
33    }
34}
35
36#[derive(Default)]
37struct References {
38    paths: Vec<PathReference>,
39    macros: Vec<MacroCall>,
40}
41
42impl<'ast> Visit<'ast> for References {
43    fn visit_path(&mut self, path: &'ast syn::Path) {
44        // A single-segment path names a local item or binding, never a crate.
45        if path.segments.len() > 1 {
46            self.paths.push(PathReference {
47                segments: segments(path),
48                line: line_of(path),
49            });
50        }
51        visit::visit_path(self, path);
52    }
53
54    /// A glob ends in `*`, so a rule sees that `use std::net::*` brings in the socket types.
55    fn visit_item_use(&mut self, item_use: &'ast ItemUse) {
56        self.paths
57            .extend(use_paths(&item_use.tree).into_iter().map(|used| {
58                let mut segments = used.segments;
59                if used.binding.is_none() {
60                    segments.push("*".to_owned());
61                }
62                PathReference {
63                    segments,
64                    line: used.line,
65                }
66            }));
67    }
68
69    fn visit_item_extern_crate(&mut self, item: &'ast ItemExternCrate) {
70        self.paths.push(PathReference {
71            segments: vec![item.ident.to_string()],
72            line: line_of(&item.ident),
73        });
74    }
75
76    fn visit_macro(&mut self, macro_call: &'ast Macro) {
77        let arguments = macro_call
78            .parse_body_with(Punctuated::<Expr, Token![,]>::parse_terminated)
79            .ok();
80        self.macros.push(MacroCall {
81            path: segments(&macro_call.path),
82            first_string: arguments
83                .as_ref()
84                .and_then(|arguments| arguments.first())
85                .and_then(string_literal),
86            line: line_of(&macro_call.path),
87        });
88        match arguments {
89            Some(arguments) => arguments
90                .iter()
91                .for_each(|argument| self.visit_expr(argument)),
92            // `info!(key = %value)`, `vec![x; n]` and DSL bodies are no comma list of expressions.
93            None => self.paths.extend(token_paths(macro_call.tokens.clone())),
94        }
95    }
96}
97
98/// Every `a::b…` run of identifiers in the tokens, groups included.
99fn token_paths(tokens: TokenStream) -> Vec<PathReference> {
100    let mut paths = Vec::new();
101    let mut current: Vec<Ident> = Vec::new();
102    let mut tokens = tokens.into_iter().peekable();
103    while let Some(token) = tokens.next() {
104        match token {
105            TokenTree::Ident(ident) => {
106                push_path(&mut paths, &mut current);
107                current.push(ident);
108            }
109            TokenTree::Punct(punct) if is_path_separator(&punct, tokens.peek()) => {
110                tokens.next();
111                match tokens.next() {
112                    Some(TokenTree::Ident(ident)) => current.push(ident),
113                    _ => push_path(&mut paths, &mut current),
114                }
115            }
116            TokenTree::Group(group) => {
117                push_path(&mut paths, &mut current);
118                paths.extend(token_paths(group.stream()));
119            }
120            TokenTree::Punct(_) | TokenTree::Literal(_) => push_path(&mut paths, &mut current),
121        }
122    }
123    push_path(&mut paths, &mut current);
124    paths
125}
126
127fn is_path_separator(punct: &Punct, next: Option<&TokenTree>) -> bool {
128    let next_is_colon = matches!(next, Some(TokenTree::Punct(next)) if next.as_char() == ':');
129    punct.as_char() == ':' && punct.spacing() == Spacing::Joint && next_is_colon
130}
131
132/// A single identifier names a local item or binding, never a crate.
133fn push_path(paths: &mut Vec<PathReference>, current: &mut Vec<Ident>) {
134    if current.len() > 1 {
135        paths.push(PathReference {
136            segments: current.iter().map(Ident::to_string).collect(),
137            line: line_of(&current[0]),
138        });
139    }
140    current.clear();
141}
142
143#[cfg(test)]
144mod tests {
145    use std::path::Path;
146
147    use crate::Source;
148
149    #[test]
150    fn finds_paths_in_macro_bodies_that_are_no_comma_list() {
151        let syntax: syn::File = syn::parse_str(
152            "fn a() {\n\
153             info!(key = %std::env::var(\"KEY\").unwrap());\n\
154             let _ = vec![std::net::TcpStream::connect(address); 2];\n\
155             }\n",
156        )
157        .unwrap();
158
159        let source = Source::of_syntax(Path::new("jobs.rs"), &syntax);
160
161        let paths: Vec<(String, usize)> = source
162            .paths
163            .iter()
164            .map(|path| (path.segments.join("::"), path.line))
165            .collect();
166        assert_eq!(
167            paths,
168            [
169                ("std::env::var".to_owned(), 2),
170                ("std::net::TcpStream::connect".to_owned(), 3)
171            ]
172        );
173    }
174
175    #[test]
176    fn a_glob_import_ends_in_a_star() {
177        let syntax: syn::File = syn::parse_str(
178            "use std::net::*;
179use std::net::{self, IpAddr};
180",
181        )
182        .unwrap();
183
184        let source = Source::of_syntax(Path::new("lib.rs"), &syntax);
185
186        let paths: Vec<String> = source
187            .paths
188            .iter()
189            .map(|path| path.segments.join("::"))
190            .collect();
191        assert_eq!(paths, ["std::net::*", "std::net", "std::net::IpAddr"]);
192    }
193}