Skip to main content

rs_hack/commands/
summary.rs

1//! `summary` command: print a module inventory for a single .rs file.
2
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6
7#[derive(Debug)]
8pub struct SummaryReport {
9    pub path: PathBuf,
10    pub module_doc: Option<String>,
11    pub public_items: Vec<String>,
12    pub struct_count: usize,
13    pub enum_count: usize,
14    pub type_alias_count: usize,
15    pub function_names: Vec<String>,
16    pub reexports: Vec<String>,
17}
18
19pub fn run(path: &PathBuf) -> Result<SummaryReport> {
20    let content = std::fs::read_to_string(path)
21        .with_context(|| format!("Failed to read file: {:?}", path))?;
22
23    let syntax =
24        syn::parse_file(&content).with_context(|| format!("Failed to parse file: {:?}", path))?;
25
26    // Module-level doc: inner doc attrs (//! or #![doc = ...])
27    let mut module_doc_parts: Vec<String> = Vec::new();
28    for attr in &syntax.attrs {
29        if let syn::AttrStyle::Inner(_) = attr.style
30            && attr.path().is_ident("doc")
31            && let Ok(syn::MetaNameValue {
32                value:
33                    syn::Expr::Lit(syn::ExprLit {
34                        lit: syn::Lit::Str(s),
35                        ..
36                    }),
37                ..
38            }) = attr.meta.require_name_value().cloned()
39        {
40            let text = s.value().trim().to_string();
41            if !text.is_empty() {
42                module_doc_parts.push(text);
43            }
44        }
45    }
46    let module_doc = if module_doc_parts.is_empty() {
47        None
48    } else {
49        Some(module_doc_parts.join(" "))
50    };
51
52    let mut public_items: Vec<String> = Vec::new();
53    let mut struct_count = 0usize;
54    let mut enum_count = 0usize;
55    let mut type_alias_count = 0usize;
56    let mut function_names: Vec<String> = Vec::new();
57    let mut reexports: Vec<String> = Vec::new();
58
59    for item in &syntax.items {
60        match item {
61            syn::Item::Struct(s) => {
62                struct_count += 1;
63                if is_public(&s.vis) {
64                    public_items.push(s.ident.to_string());
65                }
66            }
67            syn::Item::Enum(e) => {
68                enum_count += 1;
69                if is_public(&e.vis) {
70                    public_items.push(e.ident.to_string());
71                }
72            }
73            syn::Item::Type(t) => {
74                type_alias_count += 1;
75                if is_public(&t.vis) {
76                    public_items.push(t.ident.to_string());
77                }
78            }
79            syn::Item::Fn(f) => {
80                function_names.push(f.sig.ident.to_string());
81                if is_public(&f.vis) {
82                    public_items.push(f.sig.ident.to_string());
83                }
84            }
85            syn::Item::Trait(t) if is_public(&t.vis) => {
86                public_items.push(t.ident.to_string());
87            }
88            syn::Item::Const(c) if is_public(&c.vis) => {
89                public_items.push(c.ident.to_string());
90            }
91            syn::Item::Static(s) if is_public(&s.vis) => {
92                public_items.push(s.ident.to_string());
93            }
94            syn::Item::Mod(m) if is_public(&m.vis) => {
95                public_items.push(m.ident.to_string());
96            }
97            syn::Item::Use(u) if is_public(&u.vis) => {
98                let tokens = quote::quote!(#u);
99                reexports.push(tokens.to_string().replace(" :: ", "::"));
100            }
101            _ => {}
102        }
103    }
104
105    Ok(SummaryReport {
106        path: path.clone(),
107        module_doc,
108        public_items,
109        struct_count,
110        enum_count,
111        type_alias_count,
112        function_names,
113        reexports,
114    })
115}
116
117pub fn render(report: &SummaryReport) {
118    println!("Module: {}", report.path.display());
119
120    if report.public_items.is_empty() {
121        println!("Public items: (none)");
122    } else {
123        println!("Public items: {}", report.public_items.join(", "));
124    }
125
126    println!(
127        "Types: {} struct{}, {} enum{}, {} type alias{}",
128        report.struct_count,
129        if report.struct_count == 1 { "" } else { "s" },
130        report.enum_count,
131        if report.enum_count == 1 { "" } else { "s" },
132        report.type_alias_count,
133        if report.type_alias_count == 1 {
134            ""
135        } else {
136            "es"
137        },
138    );
139
140    if report.function_names.is_empty() {
141        println!("Functions: (none)");
142    } else {
143        println!("Functions: {}", report.function_names.join(", "));
144    }
145
146    if report.reexports.is_empty() {
147        println!("Re-exports: (none)");
148    } else {
149        for r in &report.reexports {
150            println!("Re-exports: {}", r);
151        }
152    }
153
154    match &report.module_doc {
155        Some(doc) => println!("Doc: {:?}", doc),
156        None => println!("Doc: (none)"),
157    }
158}
159
160const fn is_public(vis: &syn::Visibility) -> bool {
161    matches!(
162        vis,
163        syn::Visibility::Public(_) | syn::Visibility::Restricted(_)
164    )
165}