use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use syn::visit_mut::VisitMut;
use crate::collect::meta::cargo_metadata;
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EntryKind {
Main,
Lib,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntryFile {
pub crate_name: String,
pub kind: EntryKind,
pub path: PathBuf,
pub rendered: String,
pub parse_failed: bool,
pub raw_line_count: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EntryPoints {
pub files: Vec<EntryFile>,
}
impl EntryPoints {
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
}
pub fn entry_points(root: &Path) -> Result<EntryPoints> {
let meta = cargo_metadata(root)?;
let mut files = Vec::new();
for member in &meta.members {
let manifest_dir = match member.manifest_path.parent() {
Some(d) => d,
None => continue,
};
let candidates = [
("src/main.rs", EntryKind::Main),
("src/lib.rs", EntryKind::Lib),
];
for (rel, kind) in candidates {
let path = manifest_dir.join(rel);
if path.exists() {
match parse_entry_file(&member.name, &path, kind) {
Ok(f) => files.push(f),
Err(_) => continue, }
}
}
}
Ok(EntryPoints { files })
}
fn parse_entry_file(crate_name: &str, path: &Path, kind: EntryKind) -> Result<EntryFile> {
let source = std::fs::read_to_string(path)?;
let raw_line_count = source.lines().count();
let (rendered, parse_failed) = match syn::parse_file(&source) {
Ok(file) => (render_file(file, kind), false),
Err(_) => (source.clone(), true),
};
Ok(EntryFile {
crate_name: crate_name.to_string(),
kind,
path: path.to_path_buf(),
rendered,
parse_failed,
raw_line_count,
})
}
fn render_file(mut file: syn::File, kind: EntryKind) -> String {
match kind {
EntryKind::Main => prettyplease::unparse(&file),
EntryKind::Lib => {
file.items.retain(is_public_api_item);
let mut stripper = BodyStripper;
stripper.visit_file_mut(&mut file);
prettyplease::unparse(&file)
}
}
}
fn is_public_api_item(item: &syn::Item) -> bool {
use syn::{Item, Visibility};
let vis: Option<&Visibility> = match item {
Item::Fn(f) => Some(&f.vis),
Item::Struct(s) => Some(&s.vis),
Item::Enum(e) => Some(&e.vis),
Item::Trait(t) => Some(&t.vis),
Item::Mod(m) => Some(&m.vis),
Item::Const(c) => Some(&c.vis),
Item::Static(s) => Some(&s.vis),
Item::Type(t) => Some(&t.vis),
Item::Use(u) => Some(&u.vis),
Item::TraitAlias(t) => Some(&t.vis),
Item::Union(u) => Some(&u.vis),
Item::Macro(_) | Item::ExternCrate(_) | Item::ForeignMod(_) => return true,
Item::Impl(_) => return false,
_ => None,
};
matches!(vis, Some(Visibility::Public(_)))
}
struct BodyStripper;
impl VisitMut for BodyStripper {
fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
*node.block = stub_block();
}
fn visit_item_impl_mut(&mut self, _node: &mut syn::ItemImpl) {
}
fn visit_item_mod_mut(&mut self, node: &mut syn::ItemMod) {
if let Some((_, items)) = node.content.as_mut() {
items.retain(is_public_api_item);
for item in items.iter_mut() {
self.visit_item_mut(item);
}
}
}
}
fn stub_block() -> syn::Block {
syn::parse_quote! { { } }
}
#[cfg(test)]
mod tests {
use super::*;
fn write_tmp(contents: &str) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("src.rs");
std::fs::write(&path, contents).unwrap();
(tmp, path)
}
#[test]
fn lib_strips_private_items() {
let src = r#"
pub fn kept() -> i32 { 42 }
fn dropped() -> i32 { 0 }
pub struct Keeper { pub x: i32 }
struct Hidden { x: i32 }
"#;
let (_tmp, path) = write_tmp(src);
let f = parse_entry_file("x", &path, EntryKind::Lib).unwrap();
assert!(f.rendered.contains("pub fn kept"));
assert!(!f.rendered.contains("fn dropped"));
assert!(f.rendered.contains("pub struct Keeper"));
assert!(!f.rendered.contains("struct Hidden"));
}
#[test]
fn lib_strips_function_bodies() {
let src = "pub fn compute(x: i32) -> i32 { x * 2 + 7 }";
let (_tmp, path) = write_tmp(src);
let f = parse_entry_file("x", &path, EntryKind::Lib).unwrap();
assert!(
f.rendered.contains("pub fn compute"),
"signature dropped; rendered:\n{}",
f.rendered
);
assert!(
!f.rendered.contains("x * 2 + 7"),
"body leaked; rendered:\n{}",
f.rendered
);
}
#[test]
fn main_preserves_bodies() {
let src = r#"fn main() { println!("hi"); }"#;
let (_tmp, path) = write_tmp(src);
let f = parse_entry_file("x", &path, EntryKind::Main).unwrap();
assert!(f.rendered.contains("println"));
}
#[test]
fn unparseable_source_returns_raw_with_flag() {
let src = "fn main( { // intentionally broken }";
let (_tmp, path) = write_tmp(src);
let f = parse_entry_file("x", &path, EntryKind::Main).unwrap();
assert!(f.parse_failed);
assert_eq!(f.rendered, src);
}
#[test]
fn impls_are_filtered_from_lib() {
let src = r#"
pub struct S;
impl S {
pub fn hi() -> i32 { 1 }
}
"#;
let (_tmp, path) = write_tmp(src);
let f = parse_entry_file("x", &path, EntryKind::Lib).unwrap();
assert!(f.rendered.contains("pub struct S"));
assert!(!f.rendered.contains("impl S"));
}
#[test]
fn entry_points_finds_own_workspace_lib() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let ep = entry_points(root).expect("collect entry points");
assert!(
ep.files
.iter()
.any(|f| f.kind == EntryKind::Lib && f.crate_name == "cargo-context-core"),
"expected cargo-context-core lib entry, got: {:?}",
ep.files
.iter()
.map(|f| (&f.crate_name, f.kind))
.collect::<Vec<_>>()
);
}
}