sofie_model/
references.rs1use 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 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 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 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(¯o_call.path),
82 first_string: arguments
83 .as_ref()
84 .and_then(|arguments| arguments.first())
85 .and_then(string_literal),
86 line: line_of(¯o_call.path),
87 });
88 match arguments {
89 Some(arguments) => arguments
90 .iter()
91 .for_each(|argument| self.visit_expr(argument)),
92 None => self.paths.extend(token_paths(macro_call.tokens.clone())),
94 }
95 }
96}
97
98fn 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
132fn 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(¤t[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}