cargo_gazelle/
context.rs
1use std::collections::BTreeSet;
13use std::fs::File;
14use std::io::Read;
15use std::path::Path;
16
17use anyhow::{Context, anyhow};
18use camino::Utf8PathBuf;
19use guppy::graph::{BuildTargetId, PackageMetadata};
20use quote::ToTokens;
21
22use crate::config::{CrateConfig, GlobalConfig};
23
24#[derive(Default, Debug, Clone)]
25pub struct CrateContext {
26 pub build_script: Option<BuildScriptContext>,
28}
29
30impl CrateContext {
31 pub fn generate(
33 config: &GlobalConfig,
34 crate_config: &CrateConfig,
35 metadata: &PackageMetadata<'_>,
36 ) -> Result<CrateContext, anyhow::Error> {
37 tracing::debug!(name = metadata.name(), "generating context");
38
39 let mut context = CrateContext::default();
40
41 if let Some(build) = metadata.build_target(&BuildTargetId::BuildScript) {
42 if !crate_config.build().skip_proto_search() {
43 let build_script_context =
44 BuildScriptContext::generate(config, build.path()).context("build script")?;
45 context.build_script = Some(build_script_context);
46 }
47 }
48
49 Ok(context)
50 }
51}
52
53#[derive(Default, Debug, Clone)]
54pub struct BuildScriptContext {
55 pub generated_protos: Vec<String>,
57 pub proto_dependencies: BTreeSet<Utf8PathBuf>,
59}
60
61impl BuildScriptContext {
62 pub fn generate(
63 config: &GlobalConfig,
64 build_script_path: impl AsRef<Path>,
65 ) -> Result<BuildScriptContext, anyhow::Error> {
66 let build_script_path = build_script_path.as_ref();
67 tracing::debug!(?build_script_path, "generating build script context");
68
69 let mut context = BuildScriptContext::default();
70
71 let mut file = File::open(build_script_path)?;
72 let mut content = String::new();
73 file.read_to_string(&mut content)?;
74
75 let ast = syn::parse_file(&content)?;
78 let proto_files: Vec<String> = ast
79 .items
80 .iter()
81 .filter_map(|item| match item {
82 syn::Item::Fn(func) => Some(func),
83 _ => None,
84 })
85 .flat_map(|func| {
86 func.block.stmts.iter().filter_map(|stmt| match stmt {
87 syn::Stmt::Expr(expr) | syn::Stmt::Semi(expr, _) => Some(expr),
88 _ => None,
89 })
90 })
91 .filter(|expr| find_proto_build(config, expr))
92 .map(|expr| find_proto_files(config, expr.to_token_stream()))
93 .flat_map(|files| files.into_iter())
94 .map(|s| {
96 tracing::debug!(path = s, "protobuf path");
97 let mut path = Utf8PathBuf::new();
98 for c in camino::Utf8PathBuf::from(s).components().skip(1) {
99 path.push(c);
100 }
101 path.to_string()
102 })
103 .collect();
104
105 let crate_root_path = build_script_path
107 .parent()
108 .ok_or_else(|| anyhow!("build script at the root of the filesystem?"))?;
109 let proto_dependencies: BTreeSet<_> = proto_files
110 .iter()
111 .map(|sub_path| {
112 tracing::debug!(?sub_path, "protobuf dependency");
113 let full_path = crate_root_path.join(sub_path);
114 parse_proto_dependencies(&full_path).context("parsing build script proto")
115 })
116 .collect::<Result<Vec<_>, _>>()?
117 .into_iter()
118 .flat_map(|paths| paths.into_iter())
119 .collect();
120
121 context.generated_protos = proto_files;
122 context.proto_dependencies = proto_dependencies;
123
124 Ok(context)
125 }
126}
127
128fn find_proto_build(config: &GlobalConfig, expr: &syn::Expr) -> bool {
130 match expr {
131 syn::Expr::Path(func_path) => {
132 let calls_proto_gen = func_path.path.segments.iter().any(|segment| {
133 config
134 .proto_build_crates
135 .iter()
136 .any(|proto_crate_names| segment.ident == proto_crate_names)
137 });
138 calls_proto_gen
139 }
140 syn::Expr::Call(call) => find_proto_build(config, &call.func),
141 syn::Expr::MethodCall(call) => find_proto_build(config, &call.receiver),
142 syn::Expr::Block(inner) => inner.block.stmts.iter().any(|s| match &s {
143 syn::Stmt::Semi(expr, _) | syn::Stmt::Expr(expr) => find_proto_build(config, expr),
144 _ => false,
145 }),
146 syn::Expr::Try(inner) => find_proto_build(config, &inner.expr),
147 _ => false,
148 }
149}
150
151fn find_proto_files(_config: &GlobalConfig, tokens: proc_macro2::TokenStream) -> Vec<String> {
152 let mut files = Vec::new();
153 for token in tokens {
154 match token {
155 proc_macro2::TokenTree::Literal(val) => {
156 let val = syn::parse2::<syn::LitStr>(val.to_token_stream())
158 .expect("shouldn't fail")
159 .value();
160 if val.ends_with(".proto") {
161 files.push(val);
162 }
163 }
164 proc_macro2::TokenTree::Group(group) => {
165 let inner_files = find_proto_files(_config, group.stream());
166 files.extend(inner_files);
167 }
168 _ => (),
169 }
170 }
171 files
172}
173
174fn parse_proto_dependencies(
177 full_path: &Path,
178) -> Result<BTreeSet<camino::Utf8PathBuf>, anyhow::Error> {
179 tracing::debug!(proto = ?full_path, "reading deps");
180
181 let mut proto_file = File::open(full_path)?;
182 let mut content = String::new();
183 proto_file.read_to_string(&mut content)?;
184
185 let proto_desc = protobuf_parse::pure::parse_dependencies(&content).context("parsing proto")?;
186 let dependencies: BTreeSet<_> = proto_desc
187 .dependency
188 .into_iter()
189 .map(camino::Utf8PathBuf::from)
190 .collect();
191
192 Ok(dependencies)
193}