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 = syn::parse_file(&content)
24        .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            if attr.path().is_ident("doc") {
31                if let Ok(syn::MetaNameValue { value: syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }), .. }) =
32                    attr.meta.require_name_value().cloned()
33                {
34                    let text = s.value().trim().to_string();
35                    if !text.is_empty() {
36                        module_doc_parts.push(text);
37                    }
38                }
39            }
40        }
41    }
42    let module_doc = if module_doc_parts.is_empty() {
43        None
44    } else {
45        Some(module_doc_parts.join(" "))
46    };
47
48    let mut public_items: Vec<String> = Vec::new();
49    let mut struct_count = 0usize;
50    let mut enum_count = 0usize;
51    let mut type_alias_count = 0usize;
52    let mut function_names: Vec<String> = Vec::new();
53    let mut reexports: Vec<String> = Vec::new();
54
55    for item in &syntax.items {
56        match item {
57            syn::Item::Struct(s) => {
58                struct_count += 1;
59                if is_public(&s.vis) {
60                    public_items.push(s.ident.to_string());
61                }
62            }
63            syn::Item::Enum(e) => {
64                enum_count += 1;
65                if is_public(&e.vis) {
66                    public_items.push(e.ident.to_string());
67                }
68            }
69            syn::Item::Type(t) => {
70                type_alias_count += 1;
71                if is_public(&t.vis) {
72                    public_items.push(t.ident.to_string());
73                }
74            }
75            syn::Item::Fn(f) => {
76                function_names.push(f.sig.ident.to_string());
77                if is_public(&f.vis) {
78                    public_items.push(f.sig.ident.to_string());
79                }
80            }
81            syn::Item::Trait(t) => {
82                if is_public(&t.vis) {
83                    public_items.push(t.ident.to_string());
84                }
85            }
86            syn::Item::Const(c) => {
87                if is_public(&c.vis) {
88                    public_items.push(c.ident.to_string());
89                }
90            }
91            syn::Item::Static(s) => {
92                if is_public(&s.vis) {
93                    public_items.push(s.ident.to_string());
94                }
95            }
96            syn::Item::Mod(m) => {
97                if is_public(&m.vis) {
98                    public_items.push(m.ident.to_string());
99                }
100            }
101            syn::Item::Use(u) => {
102                if is_public(&u.vis) {
103                    let tokens = quote::quote!(#u);
104                    reexports.push(tokens.to_string().replace(" :: ", "::").replace(" as ", " as "));
105                }
106            }
107            _ => {}
108        }
109    }
110
111    Ok(SummaryReport {
112        path: path.clone(),
113        module_doc,
114        public_items,
115        struct_count,
116        enum_count,
117        type_alias_count,
118        function_names,
119        reexports,
120    })
121}
122
123pub fn render(report: &SummaryReport) {
124    println!("Module: {}", report.path.display());
125
126    if report.public_items.is_empty() {
127        println!("Public items: (none)");
128    } else {
129        println!("Public items: {}", report.public_items.join(", "));
130    }
131
132    println!(
133        "Types: {} struct{}, {} enum{}, {} type alias{}",
134        report.struct_count,
135        if report.struct_count == 1 { "" } else { "s" },
136        report.enum_count,
137        if report.enum_count == 1 { "" } else { "s" },
138        report.type_alias_count,
139        if report.type_alias_count == 1 { "" } else { "es" },
140    );
141
142    if report.function_names.is_empty() {
143        println!("Functions: (none)");
144    } else {
145        println!("Functions: {}", report.function_names.join(", "));
146    }
147
148    if report.reexports.is_empty() {
149        println!("Re-exports: (none)");
150    } else {
151        for r in &report.reexports {
152            println!("Re-exports: {}", r);
153        }
154    }
155
156    match &report.module_doc {
157        Some(doc) => println!("Doc: {:?}", doc),
158        None => println!("Doc: (none)"),
159    }
160}
161
162fn is_public(vis: &syn::Visibility) -> bool {
163    matches!(vis, syn::Visibility::Public(_) | syn::Visibility::Restricted(_))
164}