agent-wiki-cli 0.3.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_wiki_cli-0.3.28.dist-info/METADATA +425 -0
- agent_wiki_cli-0.3.28.dist-info/RECORD +47 -0
- agent_wiki_cli-0.3.28.dist-info/WHEEL +5 -0
- agent_wiki_cli-0.3.28.dist-info/entry_points.txt +2 -0
- agent_wiki_cli-0.3.28.dist-info/licenses/LICENSE +21 -0
- agent_wiki_cli-0.3.28.dist-info/top_level.txt +1 -0
- llm_wiki_cli/__init__.py +7 -0
- llm_wiki_cli/cli.py +231 -0
- llm_wiki_cli/commands/__init__.py +1 -0
- llm_wiki_cli/commands/bootstrap_cmd.py +1072 -0
- llm_wiki_cli/commands/bump_cmd.py +55 -0
- llm_wiki_cli/commands/context_cmd.py +427 -0
- llm_wiki_cli/commands/extract_cmd.py +745 -0
- llm_wiki_cli/commands/generate_prompt_cmd.py +89 -0
- llm_wiki_cli/commands/hook_cmd.py +161 -0
- llm_wiki_cli/commands/init_cmd.py +92 -0
- llm_wiki_cli/commands/lint_cmd.py +294 -0
- llm_wiki_cli/commands/migrate_cmd.py +892 -0
- llm_wiki_cli/commands/release_cmd.py +163 -0
- llm_wiki_cli/commands/status_cmd.py +70 -0
- llm_wiki_cli/commands/sync_cmd.py +521 -0
- llm_wiki_cli/commands/trigger_cmd.py +205 -0
- llm_wiki_cli/commands/uninstall_cmd.py +221 -0
- llm_wiki_cli/commands/upgrade_cmd.py +196 -0
- llm_wiki_cli/config.py +318 -0
- llm_wiki_cli/extractors/__init__.py +46 -0
- llm_wiki_cli/extractors/common.py +90 -0
- llm_wiki_cli/extractors/go_extractor.py +143 -0
- llm_wiki_cli/extractors/go_scripts/go.mod +3 -0
- llm_wiki_cli/extractors/go_scripts/main.go +668 -0
- llm_wiki_cli/extractors/python_extractor.py +346 -0
- llm_wiki_cli/extractors/rust_extractor.py +143 -0
- llm_wiki_cli/extractors/rust_scripts/Cargo.lock +110 -0
- llm_wiki_cli/extractors/rust_scripts/Cargo.toml +11 -0
- llm_wiki_cli/extractors/rust_scripts/src/main.rs +803 -0
- llm_wiki_cli/extractors/ts_extractor.py +206 -0
- llm_wiki_cli/extractors/ts_scripts/extract.js +485 -0
- llm_wiki_cli/extractors/ts_scripts/package.json +10 -0
- llm_wiki_cli/services/__init__.py +0 -0
- llm_wiki_cli/services/circuit_breaker.py +79 -0
- llm_wiki_cli/services/io.py +47 -0
- llm_wiki_cli/services/lockfile.py +60 -0
- llm_wiki_cli/services/packages.py +173 -0
- llm_wiki_cli/services/paths.py +31 -0
- llm_wiki_cli/services/schema.py +214 -0
- llm_wiki_cli/services/secure_file.py +22 -0
- llm_wiki_cli/services/versioning.py +193 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
// Rust AST extractor for agent-wiki-cli.
|
|
2
|
+
//
|
|
3
|
+
// Usage: cargo run -- --src-dir <path> [--only-files <f1,f2,...>] [--deep]
|
|
4
|
+
//
|
|
5
|
+
// Outputs a JSON inventory to stdout (same canonical schema as PythonExtractor,
|
|
6
|
+
// TypeScriptExtractor, and GoExtractor). The "language" field is intentionally
|
|
7
|
+
// absent — RustExtractor stamps it in Python.
|
|
8
|
+
// Errors/warnings go to stderr. Exit 0 on success.
|
|
9
|
+
|
|
10
|
+
use std::collections::{BTreeMap, HashMap};
|
|
11
|
+
use std::env;
|
|
12
|
+
use std::fs;
|
|
13
|
+
use std::io::Write;
|
|
14
|
+
use std::path::{Path, PathBuf};
|
|
15
|
+
|
|
16
|
+
use serde::Serialize;
|
|
17
|
+
|
|
18
|
+
// ── Schema types ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
#[derive(Serialize, Clone, Default)]
|
|
21
|
+
struct ParamInfo {
|
|
22
|
+
name: String,
|
|
23
|
+
#[serde(rename = "type", skip_serializing_if = "String::is_empty")]
|
|
24
|
+
ty: String,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Serialize, Clone, Default)]
|
|
28
|
+
struct MethodInfo {
|
|
29
|
+
name: String,
|
|
30
|
+
line: usize,
|
|
31
|
+
is_async: bool,
|
|
32
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
33
|
+
docstring: String,
|
|
34
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
35
|
+
decorators: Vec<String>,
|
|
36
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
37
|
+
params: Vec<ParamInfo>,
|
|
38
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
39
|
+
return_type: String,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[derive(Serialize, Clone, Default)]
|
|
43
|
+
struct AttributeInfo {
|
|
44
|
+
name: String,
|
|
45
|
+
#[serde(rename = "type", skip_serializing_if = "String::is_empty")]
|
|
46
|
+
ty: String,
|
|
47
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
48
|
+
default: String,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[derive(Serialize, Clone)]
|
|
52
|
+
struct ClassInfo {
|
|
53
|
+
name: String,
|
|
54
|
+
kind: String,
|
|
55
|
+
bases: Vec<String>,
|
|
56
|
+
line: usize,
|
|
57
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
58
|
+
docstring: String,
|
|
59
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
60
|
+
decorators: Vec<String>,
|
|
61
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
62
|
+
attributes: Vec<AttributeInfo>,
|
|
63
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
64
|
+
methods: Vec<MethodInfo>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[derive(Serialize, Clone, Default)]
|
|
68
|
+
struct FunctionInfo {
|
|
69
|
+
name: String,
|
|
70
|
+
line: usize,
|
|
71
|
+
is_async: bool,
|
|
72
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
73
|
+
receiver: String,
|
|
74
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
75
|
+
docstring: String,
|
|
76
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
77
|
+
decorators: Vec<String>,
|
|
78
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
79
|
+
params: Vec<ParamInfo>,
|
|
80
|
+
#[serde(skip_serializing_if = "String::is_empty")]
|
|
81
|
+
return_type: String,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[derive(Serialize, Clone)]
|
|
85
|
+
struct ImportInfo {
|
|
86
|
+
module: String,
|
|
87
|
+
name: String,
|
|
88
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
89
|
+
alias: Option<String>,
|
|
90
|
+
#[serde(rename = "type")]
|
|
91
|
+
import_type: String,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[derive(Serialize, Default)]
|
|
95
|
+
struct FileEntry {
|
|
96
|
+
classes: Vec<ClassInfo>,
|
|
97
|
+
functions: Vec<FunctionInfo>,
|
|
98
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
99
|
+
imports: Vec<ImportInfo>,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Excluded directories ──────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
fn is_excluded_dir(name: &str) -> bool {
|
|
105
|
+
matches!(
|
|
106
|
+
name,
|
|
107
|
+
".cache"
|
|
108
|
+
| ".direnv"
|
|
109
|
+
| ".eggs"
|
|
110
|
+
| ".env"
|
|
111
|
+
| ".git"
|
|
112
|
+
| ".mypy_cache"
|
|
113
|
+
| ".next"
|
|
114
|
+
| ".nox"
|
|
115
|
+
| ".npm"
|
|
116
|
+
| ".nuxt"
|
|
117
|
+
| ".parcel-cache"
|
|
118
|
+
| ".pnpm-store"
|
|
119
|
+
| ".pyre"
|
|
120
|
+
| ".pytest_cache"
|
|
121
|
+
| ".ruff_cache"
|
|
122
|
+
| ".svelte-kit"
|
|
123
|
+
| ".tox"
|
|
124
|
+
| ".venv"
|
|
125
|
+
| ".virtualenv"
|
|
126
|
+
| ".vite"
|
|
127
|
+
| ".yarn"
|
|
128
|
+
| "__pycache__"
|
|
129
|
+
| "__pypackages__"
|
|
130
|
+
| "bower_components"
|
|
131
|
+
| "build"
|
|
132
|
+
| "coverage"
|
|
133
|
+
| "dist"
|
|
134
|
+
| "env"
|
|
135
|
+
| "htmlcov"
|
|
136
|
+
| "jspm_packages"
|
|
137
|
+
| "node_modules"
|
|
138
|
+
| "out"
|
|
139
|
+
| "site-packages"
|
|
140
|
+
| "target"
|
|
141
|
+
| "vendor"
|
|
142
|
+
| "testdata"
|
|
143
|
+
| "venv"
|
|
144
|
+
| "virtualenv"
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/// Check if a Rust item has `#[cfg(test)]` attribute.
|
|
151
|
+
fn is_cfg_test(attrs: &[syn::Attribute]) -> bool {
|
|
152
|
+
for attr in attrs {
|
|
153
|
+
if attr.path().is_ident("cfg") {
|
|
154
|
+
if let Ok(nested) = attr.parse_args::<syn::Ident>() {
|
|
155
|
+
if nested == "test" {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Extract doc comments from attributes (/// or #[doc = "..."]).
|
|
165
|
+
fn extract_doc_comment(attrs: &[syn::Attribute]) -> String {
|
|
166
|
+
let mut lines = Vec::new();
|
|
167
|
+
for attr in attrs {
|
|
168
|
+
if attr.path().is_ident("doc") {
|
|
169
|
+
if let syn::Meta::NameValue(nv) = &attr.meta {
|
|
170
|
+
if let syn::Expr::Lit(syn::ExprLit {
|
|
171
|
+
lit: syn::Lit::Str(s),
|
|
172
|
+
..
|
|
173
|
+
}) = &nv.value
|
|
174
|
+
{
|
|
175
|
+
let raw = s.value();
|
|
176
|
+
// Strip leading space (Rust doc comments have a leading space)
|
|
177
|
+
let line = raw.strip_prefix(' ').unwrap_or(&raw);
|
|
178
|
+
lines.push(line.to_string());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
lines.join("\n").trim().to_string()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Extract #[derive(...)] as decorator names.
|
|
187
|
+
fn extract_derives(attrs: &[syn::Attribute]) -> Vec<String> {
|
|
188
|
+
let mut derives = Vec::new();
|
|
189
|
+
for attr in attrs {
|
|
190
|
+
if attr.path().is_ident("derive") {
|
|
191
|
+
if let Ok(nested) =
|
|
192
|
+
attr.parse_args_with(syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated)
|
|
193
|
+
{
|
|
194
|
+
for path in nested {
|
|
195
|
+
derives.push(path_to_string(&path));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
derives
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Convert a syn::Type to a string representation.
|
|
204
|
+
fn type_to_string(ty: &syn::Type) -> String {
|
|
205
|
+
// Use the token stream for a quick display.
|
|
206
|
+
quote_to_string(ty)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn path_to_string(path: &syn::Path) -> String {
|
|
210
|
+
path.segments
|
|
211
|
+
.iter()
|
|
212
|
+
.map(|seg| seg.ident.to_string())
|
|
213
|
+
.collect::<Vec<_>>()
|
|
214
|
+
.join("::")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn quote_to_string<T: quote::ToTokens>(t: &T) -> String {
|
|
218
|
+
let ts = quote::quote!(#t);
|
|
219
|
+
// Normalize spacing — collapse multiple spaces
|
|
220
|
+
let s = ts.to_string();
|
|
221
|
+
// Remove excess whitespace around punctuation for readability.
|
|
222
|
+
s.replace(" :: ", "::")
|
|
223
|
+
.replace(" < ", "<")
|
|
224
|
+
.replace(" > ", ">")
|
|
225
|
+
.replace(" , ", ", ")
|
|
226
|
+
.replace("& ", "&")
|
|
227
|
+
.replace("& mut ", "&mut ")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Extract the return type string from a syn::ReturnType.
|
|
231
|
+
fn return_type_to_string(ret: &syn::ReturnType) -> String {
|
|
232
|
+
match ret {
|
|
233
|
+
syn::ReturnType::Default => String::new(),
|
|
234
|
+
syn::ReturnType::Type(_, ty) => type_to_string(ty),
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Extract parameters from an `fn` signature, skipping `self`.
|
|
239
|
+
fn extract_params(sig: &syn::Signature) -> Vec<ParamInfo> {
|
|
240
|
+
let mut params = Vec::new();
|
|
241
|
+
for input in &sig.inputs {
|
|
242
|
+
match input {
|
|
243
|
+
syn::FnArg::Receiver(_) => {} // skip self
|
|
244
|
+
syn::FnArg::Typed(pat_ty) => {
|
|
245
|
+
let name = quote_to_string(&pat_ty.pat);
|
|
246
|
+
let ty = type_to_string(&pat_ty.ty);
|
|
247
|
+
params.push(ParamInfo { name, ty });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
params
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Compute 1-based line number from a Span using the source text.
|
|
255
|
+
fn line_of(span: proc_macro2::Span) -> usize {
|
|
256
|
+
span.start().line
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── File collection ───────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
fn collect_rs_files(root: &Path) -> Vec<PathBuf> {
|
|
262
|
+
let mut files = Vec::new();
|
|
263
|
+
walk_dir(root, &mut files);
|
|
264
|
+
files.sort();
|
|
265
|
+
files
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn walk_dir(dir: &Path, out: &mut Vec<PathBuf>) {
|
|
269
|
+
let entries = match fs::read_dir(dir) {
|
|
270
|
+
Ok(e) => e,
|
|
271
|
+
Err(_) => return,
|
|
272
|
+
};
|
|
273
|
+
for entry in entries.flatten() {
|
|
274
|
+
let path = entry.path();
|
|
275
|
+
if path.is_dir() {
|
|
276
|
+
let name = entry.file_name();
|
|
277
|
+
let name_str = name.to_string_lossy();
|
|
278
|
+
if is_excluded_dir(&name_str)
|
|
279
|
+
|| name_str.starts_with('.')
|
|
280
|
+
|| name_str.starts_with('_')
|
|
281
|
+
{
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
walk_dir(&path, out);
|
|
285
|
+
} else if path.extension().map_or(false, |e| e == "rs") {
|
|
286
|
+
out.push(path);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn has_excluded_dir(path: &Path) -> bool {
|
|
292
|
+
let Some(parent) = path.parent() else {
|
|
293
|
+
return false;
|
|
294
|
+
};
|
|
295
|
+
for component in parent.components() {
|
|
296
|
+
if let std::path::Component::Normal(name) = component {
|
|
297
|
+
let name_str = name.to_string_lossy();
|
|
298
|
+
if is_excluded_dir(&name_str)
|
|
299
|
+
|| name_str.starts_with('.')
|
|
300
|
+
|| name_str.starts_with('_')
|
|
301
|
+
{
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Per-file extraction ───────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
struct ExtractedImpl {
|
|
312
|
+
target: String, // The type being impl'd (e.g. "Foo")
|
|
313
|
+
trait_name: Option<String>, // If `impl Trait for Foo`, the trait name
|
|
314
|
+
methods: Vec<MethodInfo>,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn extract_file(path: &Path, deep: bool) -> Option<(FileEntry, Vec<ExtractedImpl>)> {
|
|
318
|
+
let source = match fs::read_to_string(path) {
|
|
319
|
+
Ok(s) => s,
|
|
320
|
+
Err(e) => {
|
|
321
|
+
eprintln!("Warning: could not read {}: {}", path.display(), e);
|
|
322
|
+
return None;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
let syntax = match syn::parse_file(&source) {
|
|
327
|
+
Ok(f) => f,
|
|
328
|
+
Err(e) => {
|
|
329
|
+
eprintln!("Warning: could not parse {}: {}", path.display(), e);
|
|
330
|
+
return None;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
let mut entry = FileEntry::default();
|
|
335
|
+
let mut impls = Vec::new();
|
|
336
|
+
|
|
337
|
+
// Map class name → index in entry.classes for same-file impl attachment.
|
|
338
|
+
let mut class_index: HashMap<String, usize> = HashMap::new();
|
|
339
|
+
|
|
340
|
+
for item in &syntax.items {
|
|
341
|
+
// Skip #[cfg(test)] modules entirely.
|
|
342
|
+
match item {
|
|
343
|
+
syn::Item::Mod(m) if is_cfg_test(&m.attrs) => continue,
|
|
344
|
+
_ => {}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
match item {
|
|
348
|
+
syn::Item::Struct(s) => {
|
|
349
|
+
if !is_pub(&s.vis) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
let mut ci = ClassInfo {
|
|
353
|
+
name: s.ident.to_string(),
|
|
354
|
+
kind: "struct".into(),
|
|
355
|
+
bases: Vec::new(),
|
|
356
|
+
line: line_of(s.ident.span()),
|
|
357
|
+
docstring: String::new(),
|
|
358
|
+
decorators: Vec::new(),
|
|
359
|
+
attributes: Vec::new(),
|
|
360
|
+
methods: Vec::new(),
|
|
361
|
+
};
|
|
362
|
+
if deep {
|
|
363
|
+
ci.docstring = extract_doc_comment(&s.attrs);
|
|
364
|
+
ci.decorators = extract_derives(&s.attrs);
|
|
365
|
+
// Extract fields.
|
|
366
|
+
if let syn::Fields::Named(ref fields) = s.fields {
|
|
367
|
+
for f in &fields.named {
|
|
368
|
+
if let Some(ref ident) = f.ident {
|
|
369
|
+
ci.attributes.push(AttributeInfo {
|
|
370
|
+
name: ident.to_string(),
|
|
371
|
+
ty: type_to_string(&f.ty),
|
|
372
|
+
default: String::new(),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
class_index.insert(ci.name.clone(), entry.classes.len());
|
|
379
|
+
entry.classes.push(ci);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
syn::Item::Enum(e) => {
|
|
383
|
+
if !is_pub(&e.vis) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
let mut ci = ClassInfo {
|
|
387
|
+
name: e.ident.to_string(),
|
|
388
|
+
kind: "enum".into(),
|
|
389
|
+
bases: Vec::new(),
|
|
390
|
+
line: line_of(e.ident.span()),
|
|
391
|
+
docstring: String::new(),
|
|
392
|
+
decorators: Vec::new(),
|
|
393
|
+
attributes: Vec::new(),
|
|
394
|
+
methods: Vec::new(),
|
|
395
|
+
};
|
|
396
|
+
if deep {
|
|
397
|
+
ci.docstring = extract_doc_comment(&e.attrs);
|
|
398
|
+
ci.decorators = extract_derives(&e.attrs);
|
|
399
|
+
// Extract variants as attributes.
|
|
400
|
+
for v in &e.variants {
|
|
401
|
+
ci.attributes.push(AttributeInfo {
|
|
402
|
+
name: v.ident.to_string(),
|
|
403
|
+
ty: String::new(),
|
|
404
|
+
default: String::new(),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
class_index.insert(ci.name.clone(), entry.classes.len());
|
|
409
|
+
entry.classes.push(ci);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
syn::Item::Trait(t) => {
|
|
413
|
+
if !is_pub(&t.vis) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
let mut ci = ClassInfo {
|
|
417
|
+
name: t.ident.to_string(),
|
|
418
|
+
kind: "trait".into(),
|
|
419
|
+
bases: Vec::new(),
|
|
420
|
+
line: line_of(t.ident.span()),
|
|
421
|
+
docstring: String::new(),
|
|
422
|
+
decorators: Vec::new(),
|
|
423
|
+
attributes: Vec::new(),
|
|
424
|
+
methods: Vec::new(),
|
|
425
|
+
};
|
|
426
|
+
// Supertraits as bases.
|
|
427
|
+
for bound in &t.supertraits {
|
|
428
|
+
if let syn::TypeParamBound::Trait(tb) = bound {
|
|
429
|
+
ci.bases.push(path_to_string(&tb.path));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if deep {
|
|
433
|
+
ci.docstring = extract_doc_comment(&t.attrs);
|
|
434
|
+
// Trait methods.
|
|
435
|
+
for item in &t.items {
|
|
436
|
+
if let syn::TraitItem::Fn(m) = item {
|
|
437
|
+
ci.methods.push(MethodInfo {
|
|
438
|
+
name: m.sig.ident.to_string(),
|
|
439
|
+
line: line_of(m.sig.ident.span()),
|
|
440
|
+
is_async: m.sig.asyncness.is_some(),
|
|
441
|
+
docstring: extract_doc_comment(&m.attrs),
|
|
442
|
+
decorators: Vec::new(),
|
|
443
|
+
params: extract_params(&m.sig),
|
|
444
|
+
return_type: return_type_to_string(&m.sig.output),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
class_index.insert(ci.name.clone(), entry.classes.len());
|
|
450
|
+
entry.classes.push(ci);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
syn::Item::Type(t) => {
|
|
454
|
+
if !is_pub(&t.vis) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
let mut ci = ClassInfo {
|
|
458
|
+
name: t.ident.to_string(),
|
|
459
|
+
kind: "type_alias".into(),
|
|
460
|
+
bases: Vec::new(),
|
|
461
|
+
line: line_of(t.ident.span()),
|
|
462
|
+
docstring: String::new(),
|
|
463
|
+
decorators: Vec::new(),
|
|
464
|
+
attributes: Vec::new(),
|
|
465
|
+
methods: Vec::new(),
|
|
466
|
+
};
|
|
467
|
+
if deep {
|
|
468
|
+
ci.docstring = extract_doc_comment(&t.attrs);
|
|
469
|
+
}
|
|
470
|
+
class_index.insert(ci.name.clone(), entry.classes.len());
|
|
471
|
+
entry.classes.push(ci);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
syn::Item::Fn(f) => {
|
|
475
|
+
if !is_pub(&f.vis) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
let mut fi = FunctionInfo {
|
|
479
|
+
name: f.sig.ident.to_string(),
|
|
480
|
+
line: line_of(f.sig.ident.span()),
|
|
481
|
+
is_async: f.sig.asyncness.is_some(),
|
|
482
|
+
..Default::default()
|
|
483
|
+
};
|
|
484
|
+
if deep {
|
|
485
|
+
fi.docstring = extract_doc_comment(&f.attrs);
|
|
486
|
+
fi.params = extract_params(&f.sig);
|
|
487
|
+
fi.return_type = return_type_to_string(&f.sig.output);
|
|
488
|
+
}
|
|
489
|
+
entry.functions.push(fi);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
syn::Item::Impl(imp) => {
|
|
493
|
+
// Determine the target type name.
|
|
494
|
+
let target_name = if let syn::Type::Path(tp) = imp.self_ty.as_ref() {
|
|
495
|
+
tp.path.segments.last().map(|s| s.ident.to_string())
|
|
496
|
+
} else {
|
|
497
|
+
None
|
|
498
|
+
};
|
|
499
|
+
let target_name = match target_name {
|
|
500
|
+
Some(n) => n,
|
|
501
|
+
None => continue,
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Trait name if `impl Trait for Type`.
|
|
505
|
+
let trait_name = imp.trait_.as_ref().map(|(_, path, _)| path_to_string(path));
|
|
506
|
+
|
|
507
|
+
let mut methods = Vec::new();
|
|
508
|
+
for impl_item in &imp.items {
|
|
509
|
+
if let syn::ImplItem::Fn(m) = impl_item {
|
|
510
|
+
if !is_pub(&m.vis) && imp.trait_.is_none() {
|
|
511
|
+
// Skip private methods in inherent impls.
|
|
512
|
+
// Trait impl methods are always public.
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
let mut mi = MethodInfo {
|
|
516
|
+
name: m.sig.ident.to_string(),
|
|
517
|
+
line: line_of(m.sig.ident.span()),
|
|
518
|
+
is_async: m.sig.asyncness.is_some(),
|
|
519
|
+
..Default::default()
|
|
520
|
+
};
|
|
521
|
+
if deep {
|
|
522
|
+
mi.docstring = extract_doc_comment(&m.attrs);
|
|
523
|
+
mi.params = extract_params(&m.sig);
|
|
524
|
+
mi.return_type = return_type_to_string(&m.sig.output);
|
|
525
|
+
}
|
|
526
|
+
methods.push(mi);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Try same-file attachment first (deep mode).
|
|
531
|
+
if deep {
|
|
532
|
+
if let Some(&idx) = class_index.get(&target_name) {
|
|
533
|
+
// Add trait to bases if this is a trait impl.
|
|
534
|
+
if let Some(ref tn) = trait_name {
|
|
535
|
+
if !entry.classes[idx].bases.contains(tn) {
|
|
536
|
+
entry.classes[idx].bases.push(tn.clone());
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
entry.classes[idx].methods.extend(methods.clone());
|
|
540
|
+
// Still record for cross-file if the methods might
|
|
541
|
+
// need to be merged, but the main attachment is done.
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Record for cross-file attachment or shallow-mode impl bases.
|
|
547
|
+
if !deep {
|
|
548
|
+
// In shallow mode, just record trait as a base if type is in same file.
|
|
549
|
+
if let Some(ref tn) = trait_name {
|
|
550
|
+
if let Some(&idx) = class_index.get(&target_name) {
|
|
551
|
+
if !entry.classes[idx].bases.contains(tn) {
|
|
552
|
+
entry.classes[idx].bases.push(tn.clone());
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Shallow mode: methods as standalone functions with receiver.
|
|
557
|
+
for mi in &methods {
|
|
558
|
+
entry.functions.push(FunctionInfo {
|
|
559
|
+
name: mi.name.clone(),
|
|
560
|
+
line: mi.line,
|
|
561
|
+
is_async: mi.is_async,
|
|
562
|
+
receiver: target_name.clone(),
|
|
563
|
+
..Default::default()
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
// Deep mode but target not in same file — record for cross-file.
|
|
568
|
+
impls.push(ExtractedImpl {
|
|
569
|
+
target: target_name,
|
|
570
|
+
trait_name,
|
|
571
|
+
methods,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
syn::Item::Use(u) if deep => {
|
|
577
|
+
extract_use_tree(&u.tree, &[], &mut entry.imports);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
_ => {}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
Some((entry, impls))
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/// Recursively extract use paths into ImportInfo entries.
|
|
588
|
+
fn extract_use_tree(tree: &syn::UseTree, prefix: &[String], out: &mut Vec<ImportInfo>) {
|
|
589
|
+
match tree {
|
|
590
|
+
syn::UseTree::Path(p) => {
|
|
591
|
+
let mut new_prefix = prefix.to_vec();
|
|
592
|
+
new_prefix.push(p.ident.to_string());
|
|
593
|
+
extract_use_tree(&p.tree, &new_prefix, out);
|
|
594
|
+
}
|
|
595
|
+
syn::UseTree::Name(n) => {
|
|
596
|
+
let name = n.ident.to_string();
|
|
597
|
+
let mut parts = prefix.to_vec();
|
|
598
|
+
parts.push(name.clone());
|
|
599
|
+
out.push(ImportInfo {
|
|
600
|
+
module: parts.join("::"),
|
|
601
|
+
name,
|
|
602
|
+
alias: None,
|
|
603
|
+
import_type: "use".into(),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
syn::UseTree::Rename(r) => {
|
|
607
|
+
let name = r.ident.to_string();
|
|
608
|
+
let alias = r.rename.to_string();
|
|
609
|
+
let mut parts = prefix.to_vec();
|
|
610
|
+
parts.push(name.clone());
|
|
611
|
+
out.push(ImportInfo {
|
|
612
|
+
module: parts.join("::"),
|
|
613
|
+
name,
|
|
614
|
+
alias: Some(alias),
|
|
615
|
+
import_type: "use".into(),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
syn::UseTree::Glob(_) => {
|
|
619
|
+
let mut parts = prefix.to_vec();
|
|
620
|
+
parts.push("*".into());
|
|
621
|
+
out.push(ImportInfo {
|
|
622
|
+
module: parts.join("::"),
|
|
623
|
+
name: "*".into(),
|
|
624
|
+
alias: None,
|
|
625
|
+
import_type: "use".into(),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
syn::UseTree::Group(g) => {
|
|
629
|
+
for tree in &g.items {
|
|
630
|
+
extract_use_tree(tree, prefix, out);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
fn is_pub(vis: &syn::Visibility) -> bool {
|
|
637
|
+
matches!(vis, syn::Visibility::Public(_))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ── Arg parsing (manual, no clap needed) ──────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
struct Args {
|
|
643
|
+
src_dir: String,
|
|
644
|
+
only_files: Option<Vec<String>>,
|
|
645
|
+
deep: bool,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
fn parse_args() -> Args {
|
|
649
|
+
let args: Vec<String> = env::args().collect();
|
|
650
|
+
let mut src_dir = ".".to_string();
|
|
651
|
+
let mut only_files: Option<Vec<String>> = None;
|
|
652
|
+
let mut deep = false;
|
|
653
|
+
let mut i = 1;
|
|
654
|
+
while i < args.len() {
|
|
655
|
+
match args[i].as_str() {
|
|
656
|
+
"--src-dir" => {
|
|
657
|
+
i += 1;
|
|
658
|
+
if i < args.len() {
|
|
659
|
+
src_dir = args[i].clone();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
"--only-files" => {
|
|
663
|
+
i += 1;
|
|
664
|
+
if i < args.len() {
|
|
665
|
+
only_files = Some(
|
|
666
|
+
args[i]
|
|
667
|
+
.split(',')
|
|
668
|
+
.map(|s| s.trim().to_string())
|
|
669
|
+
.filter(|s| !s.is_empty())
|
|
670
|
+
.collect(),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
"--deep" => {
|
|
675
|
+
deep = true;
|
|
676
|
+
}
|
|
677
|
+
_ => {}
|
|
678
|
+
}
|
|
679
|
+
i += 1;
|
|
680
|
+
}
|
|
681
|
+
Args {
|
|
682
|
+
src_dir,
|
|
683
|
+
only_files,
|
|
684
|
+
deep,
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
fn main() {
|
|
691
|
+
let args = parse_args();
|
|
692
|
+
let root = Path::new(&args.src_dir);
|
|
693
|
+
|
|
694
|
+
// Collect files.
|
|
695
|
+
let files: Vec<PathBuf> = if let Some(ref only) = args.only_files {
|
|
696
|
+
let mut out = Vec::new();
|
|
697
|
+
for f in only {
|
|
698
|
+
let p = if Path::new(f).is_absolute() {
|
|
699
|
+
PathBuf::from(f)
|
|
700
|
+
} else {
|
|
701
|
+
root.join(f)
|
|
702
|
+
};
|
|
703
|
+
let rel = match p.strip_prefix(root) {
|
|
704
|
+
Ok(r) => r,
|
|
705
|
+
Err(_) => continue,
|
|
706
|
+
};
|
|
707
|
+
if has_excluded_dir(rel) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if p.exists() && p.extension().map_or(false, |e| e == "rs") {
|
|
711
|
+
out.push(p);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
out
|
|
715
|
+
} else {
|
|
716
|
+
collect_rs_files(root)
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// Per-file extraction.
|
|
720
|
+
let mut inventory: BTreeMap<String, FileEntry> = BTreeMap::new();
|
|
721
|
+
let mut all_impls: Vec<(String, ExtractedImpl)> = Vec::new(); // (rel_path, impl)
|
|
722
|
+
|
|
723
|
+
for file in &files {
|
|
724
|
+
let (entry, impls) = match extract_file(file, args.deep) {
|
|
725
|
+
Some(v) => v,
|
|
726
|
+
None => continue,
|
|
727
|
+
};
|
|
728
|
+
let rel = match file.strip_prefix(root) {
|
|
729
|
+
Ok(r) => r.to_string_lossy().to_string(),
|
|
730
|
+
Err(_) => file.to_string_lossy().to_string(),
|
|
731
|
+
};
|
|
732
|
+
// Normalize path separators.
|
|
733
|
+
let rel = rel.replace('\\', "/");
|
|
734
|
+
for imp in impls {
|
|
735
|
+
all_impls.push((rel.clone(), imp));
|
|
736
|
+
}
|
|
737
|
+
inventory.insert(rel, entry);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ── Cross-file impl attachment (deep mode) ────────────────────────────────
|
|
741
|
+
if args.deep {
|
|
742
|
+
// Build: dir → type_name → (rel_path, class_idx)
|
|
743
|
+
let mut dir_class_index: HashMap<String, HashMap<String, (String, usize)>> = HashMap::new();
|
|
744
|
+
for (rel, entry) in &inventory {
|
|
745
|
+
let dir = Path::new(rel)
|
|
746
|
+
.parent()
|
|
747
|
+
.map(|p| p.to_string_lossy().to_string())
|
|
748
|
+
.unwrap_or_default();
|
|
749
|
+
let map = dir_class_index.entry(dir).or_default();
|
|
750
|
+
for (idx, cls) in entry.classes.iter().enumerate() {
|
|
751
|
+
map.insert(cls.name.clone(), (rel.clone(), idx));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
for (impl_rel, ext_impl) in &all_impls {
|
|
756
|
+
let dir = Path::new(impl_rel)
|
|
757
|
+
.parent()
|
|
758
|
+
.map(|p| p.to_string_lossy().to_string())
|
|
759
|
+
.unwrap_or_default();
|
|
760
|
+
if let Some(map) = dir_class_index.get(&dir) {
|
|
761
|
+
if let Some((target_rel, target_idx)) = map.get(&ext_impl.target) {
|
|
762
|
+
if let Some(target_entry) = inventory.get_mut(target_rel) {
|
|
763
|
+
// Attach trait to bases.
|
|
764
|
+
if let Some(ref tn) = ext_impl.trait_name {
|
|
765
|
+
if !target_entry.classes[*target_idx].bases.contains(tn) {
|
|
766
|
+
target_entry.classes[*target_idx].bases.push(tn.clone());
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Attach methods.
|
|
770
|
+
target_entry.classes[*target_idx]
|
|
771
|
+
.methods
|
|
772
|
+
.extend(ext_impl.methods.clone());
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// Unresolvable — expose as standalone functions with receiver.
|
|
778
|
+
if let Some(entry) = inventory.get_mut(impl_rel) {
|
|
779
|
+
for mi in &ext_impl.methods {
|
|
780
|
+
entry.functions.push(FunctionInfo {
|
|
781
|
+
name: mi.name.clone(),
|
|
782
|
+
line: mi.line,
|
|
783
|
+
is_async: mi.is_async,
|
|
784
|
+
receiver: ext_impl.target.clone(),
|
|
785
|
+
docstring: mi.docstring.clone(),
|
|
786
|
+
decorators: mi.decorators.clone(),
|
|
787
|
+
params: mi.params.clone(),
|
|
788
|
+
return_type: mi.return_type.clone(),
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Output JSON.
|
|
796
|
+
let stdout = std::io::stdout();
|
|
797
|
+
let mut handle = stdout.lock();
|
|
798
|
+
if let Err(e) = serde_json::to_writer_pretty(&mut handle, &inventory) {
|
|
799
|
+
eprintln!("Error encoding JSON: {}", e);
|
|
800
|
+
std::process::exit(1);
|
|
801
|
+
}
|
|
802
|
+
let _ = writeln!(handle);
|
|
803
|
+
}
|