cargo_context_core/collect/
entry.rs1use std::path::{Path, PathBuf};
19
20use serde::{Deserialize, Serialize};
21use syn::visit_mut::VisitMut;
22
23use crate::collect::meta::cargo_metadata;
24use crate::error::Result;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum EntryKind {
29 Main,
31 Lib,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct EntryFile {
37 pub crate_name: String,
38 pub kind: EntryKind,
39 pub path: PathBuf,
40 pub rendered: String,
43 pub parse_failed: bool,
45 pub raw_line_count: usize,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49pub struct EntryPoints {
50 pub files: Vec<EntryFile>,
51}
52
53impl EntryPoints {
54 pub fn is_empty(&self) -> bool {
55 self.files.is_empty()
56 }
57}
58
59pub fn entry_points(root: &Path) -> Result<EntryPoints> {
61 let meta = cargo_metadata(root)?;
62 let mut files = Vec::new();
63 for member in &meta.members {
64 let manifest_dir = match member.manifest_path.parent() {
65 Some(d) => d,
66 None => continue,
67 };
68 let candidates = [
69 ("src/main.rs", EntryKind::Main),
70 ("src/lib.rs", EntryKind::Lib),
71 ];
72 for (rel, kind) in candidates {
73 let path = manifest_dir.join(rel);
74 if path.exists() {
75 match parse_entry_file(&member.name, &path, kind) {
76 Ok(f) => files.push(f),
77 Err(_) => continue, }
79 }
80 }
81 }
82 Ok(EntryPoints { files })
83}
84
85fn parse_entry_file(crate_name: &str, path: &Path, kind: EntryKind) -> Result<EntryFile> {
86 let source = std::fs::read_to_string(path)?;
87 let raw_line_count = source.lines().count();
88
89 let (rendered, parse_failed) = match syn::parse_file(&source) {
90 Ok(file) => (render_file(file, kind), false),
91 Err(_) => (source.clone(), true),
92 };
93
94 Ok(EntryFile {
95 crate_name: crate_name.to_string(),
96 kind,
97 path: path.to_path_buf(),
98 rendered,
99 parse_failed,
100 raw_line_count,
101 })
102}
103
104fn render_file(mut file: syn::File, kind: EntryKind) -> String {
105 match kind {
106 EntryKind::Main => prettyplease::unparse(&file),
107 EntryKind::Lib => {
108 file.items.retain(is_public_api_item);
110 let mut stripper = BodyStripper;
111 stripper.visit_file_mut(&mut file);
112 prettyplease::unparse(&file)
113 }
114 }
115}
116
117fn is_public_api_item(item: &syn::Item) -> bool {
118 use syn::{Item, Visibility};
119 let vis: Option<&Visibility> = match item {
120 Item::Fn(f) => Some(&f.vis),
121 Item::Struct(s) => Some(&s.vis),
122 Item::Enum(e) => Some(&e.vis),
123 Item::Trait(t) => Some(&t.vis),
124 Item::Mod(m) => Some(&m.vis),
125 Item::Const(c) => Some(&c.vis),
126 Item::Static(s) => Some(&s.vis),
127 Item::Type(t) => Some(&t.vis),
128 Item::Use(u) => Some(&u.vis),
129 Item::TraitAlias(t) => Some(&t.vis),
130 Item::Union(u) => Some(&u.vis),
131 Item::Macro(_) | Item::ExternCrate(_) | Item::ForeignMod(_) => return true,
133 Item::Impl(_) => return false,
135 _ => None,
136 };
137 matches!(vis, Some(Visibility::Public(_)))
138}
139
140struct BodyStripper;
143
144impl VisitMut for BodyStripper {
145 fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
146 *node.block = stub_block();
147 }
150
151 fn visit_item_impl_mut(&mut self, _node: &mut syn::ItemImpl) {
152 }
154
155 fn visit_item_mod_mut(&mut self, node: &mut syn::ItemMod) {
156 if let Some((_, items)) = node.content.as_mut() {
157 items.retain(is_public_api_item);
158 for item in items.iter_mut() {
159 self.visit_item_mut(item);
160 }
161 }
162 }
163}
164
165fn stub_block() -> syn::Block {
166 syn::parse_quote! { { } }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn write_tmp(contents: &str) -> (tempfile::TempDir, PathBuf) {
177 let tmp = tempfile::tempdir().unwrap();
178 let path = tmp.path().join("src.rs");
179 std::fs::write(&path, contents).unwrap();
180 (tmp, path)
181 }
182
183 #[test]
184 fn lib_strips_private_items() {
185 let src = r#"
186 pub fn kept() -> i32 { 42 }
187 fn dropped() -> i32 { 0 }
188 pub struct Keeper { pub x: i32 }
189 struct Hidden { x: i32 }
190 "#;
191 let (_tmp, path) = write_tmp(src);
192 let f = parse_entry_file("x", &path, EntryKind::Lib).unwrap();
193 assert!(f.rendered.contains("pub fn kept"));
194 assert!(!f.rendered.contains("fn dropped"));
195 assert!(f.rendered.contains("pub struct Keeper"));
196 assert!(!f.rendered.contains("struct Hidden"));
197 }
198
199 #[test]
200 fn lib_strips_function_bodies() {
201 let src = "pub fn compute(x: i32) -> i32 { x * 2 + 7 }";
202 let (_tmp, path) = write_tmp(src);
203 let f = parse_entry_file("x", &path, EntryKind::Lib).unwrap();
204 assert!(
205 f.rendered.contains("pub fn compute"),
206 "signature dropped; rendered:\n{}",
207 f.rendered
208 );
209 assert!(
210 !f.rendered.contains("x * 2 + 7"),
211 "body leaked; rendered:\n{}",
212 f.rendered
213 );
214 }
215
216 #[test]
217 fn main_preserves_bodies() {
218 let src = r#"fn main() { println!("hi"); }"#;
219 let (_tmp, path) = write_tmp(src);
220 let f = parse_entry_file("x", &path, EntryKind::Main).unwrap();
221 assert!(f.rendered.contains("println"));
222 }
223
224 #[test]
225 fn unparseable_source_returns_raw_with_flag() {
226 let src = "fn main( { // intentionally broken }";
227 let (_tmp, path) = write_tmp(src);
228 let f = parse_entry_file("x", &path, EntryKind::Main).unwrap();
229 assert!(f.parse_failed);
230 assert_eq!(f.rendered, src);
231 }
232
233 #[test]
234 fn impls_are_filtered_from_lib() {
235 let src = r#"
236 pub struct S;
237 impl S {
238 pub fn hi() -> i32 { 1 }
239 }
240 "#;
241 let (_tmp, path) = write_tmp(src);
242 let f = parse_entry_file("x", &path, EntryKind::Lib).unwrap();
243 assert!(f.rendered.contains("pub struct S"));
244 assert!(!f.rendered.contains("impl S"));
245 }
246
247 #[test]
248 fn entry_points_finds_own_workspace_lib() {
249 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
251 let ep = entry_points(root).expect("collect entry points");
252 assert!(
253 ep.files
254 .iter()
255 .any(|f| f.kind == EntryKind::Lib && f.crate_name == "cargo-context-core"),
256 "expected cargo-context-core lib entry, got: {:?}",
257 ep.files
258 .iter()
259 .map(|f| (&f.crate_name, f.kind))
260 .collect::<Vec<_>>()
261 );
262 }
263}