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,668 @@
|
|
|
1
|
+
// Go AST extractor for agent-wiki-cli.
|
|
2
|
+
//
|
|
3
|
+
// Usage: go run . --src-dir <path> [--only-files <f1,f2,...>] [--deep]
|
|
4
|
+
//
|
|
5
|
+
// Outputs a JSON inventory to stdout (same canonical schema as PythonExtractor
|
|
6
|
+
// and TypeScriptExtractor). The "language" field is intentionally absent —
|
|
7
|
+
// GoExtractor stamps it in Python.
|
|
8
|
+
// Errors/warnings go to stderr. Exit 0 on success.
|
|
9
|
+
package main
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"encoding/json"
|
|
13
|
+
"flag"
|
|
14
|
+
"fmt"
|
|
15
|
+
"go/ast"
|
|
16
|
+
"go/parser"
|
|
17
|
+
"go/token"
|
|
18
|
+
"os"
|
|
19
|
+
"path/filepath"
|
|
20
|
+
"strings"
|
|
21
|
+
"unicode"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// ── Excluded directories ──────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
var excludedDirs = map[string]bool{
|
|
27
|
+
".cache": true,
|
|
28
|
+
".direnv": true,
|
|
29
|
+
".eggs": true,
|
|
30
|
+
".env": true,
|
|
31
|
+
".git": true,
|
|
32
|
+
".mypy_cache": true,
|
|
33
|
+
".next": true,
|
|
34
|
+
".nox": true,
|
|
35
|
+
".npm": true,
|
|
36
|
+
".nuxt": true,
|
|
37
|
+
".parcel-cache": true,
|
|
38
|
+
".pnpm-store": true,
|
|
39
|
+
".pyre": true,
|
|
40
|
+
".pytest_cache": true,
|
|
41
|
+
".ruff_cache": true,
|
|
42
|
+
".svelte-kit": true,
|
|
43
|
+
".tox": true,
|
|
44
|
+
".venv": true,
|
|
45
|
+
".virtualenv": true,
|
|
46
|
+
".vite": true,
|
|
47
|
+
".yarn": true,
|
|
48
|
+
"__pycache__": true,
|
|
49
|
+
"__pypackages__": true,
|
|
50
|
+
"bower_components": true,
|
|
51
|
+
"build": true,
|
|
52
|
+
"coverage": true,
|
|
53
|
+
"dist": true,
|
|
54
|
+
"env": true,
|
|
55
|
+
"htmlcov": true,
|
|
56
|
+
"jspm_packages": true,
|
|
57
|
+
"node_modules": true,
|
|
58
|
+
"out": true,
|
|
59
|
+
"site-packages": true,
|
|
60
|
+
"target": true,
|
|
61
|
+
"testdata": true,
|
|
62
|
+
"vendor": true,
|
|
63
|
+
"venv": true,
|
|
64
|
+
"virtualenv": true,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Schema types ──────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
type ParamInfo struct {
|
|
70
|
+
Name string `json:"name"`
|
|
71
|
+
Type string `json:"type,omitempty"`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type MethodInfo struct {
|
|
75
|
+
Name string `json:"name"`
|
|
76
|
+
Line int `json:"line"`
|
|
77
|
+
IsAsync bool `json:"is_async"`
|
|
78
|
+
Docstring string `json:"docstring,omitempty"`
|
|
79
|
+
Decorators []string `json:"decorators,omitempty"`
|
|
80
|
+
Params []ParamInfo `json:"params,omitempty"`
|
|
81
|
+
ReturnType string `json:"return_type,omitempty"`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type AttributeInfo struct {
|
|
85
|
+
Name string `json:"name"`
|
|
86
|
+
Type string `json:"type,omitempty"`
|
|
87
|
+
Default string `json:"default,omitempty"`
|
|
88
|
+
Tag string `json:"tag,omitempty"`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ClassInfo struct {
|
|
92
|
+
Name string `json:"name"`
|
|
93
|
+
Kind string `json:"kind"`
|
|
94
|
+
Bases []string `json:"bases"`
|
|
95
|
+
Line int `json:"line"`
|
|
96
|
+
Docstring string `json:"docstring,omitempty"`
|
|
97
|
+
Decorators []string `json:"decorators,omitempty"`
|
|
98
|
+
Attributes []AttributeInfo `json:"attributes,omitempty"`
|
|
99
|
+
Methods []MethodInfo `json:"methods,omitempty"`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type FunctionInfo struct {
|
|
103
|
+
Name string `json:"name"`
|
|
104
|
+
Line int `json:"line"`
|
|
105
|
+
IsAsync bool `json:"is_async"`
|
|
106
|
+
Receiver string `json:"receiver,omitempty"`
|
|
107
|
+
Docstring string `json:"docstring,omitempty"`
|
|
108
|
+
Decorators []string `json:"decorators,omitempty"`
|
|
109
|
+
Params []ParamInfo `json:"params,omitempty"`
|
|
110
|
+
ReturnType string `json:"return_type,omitempty"`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type ImportInfo struct {
|
|
114
|
+
Module string `json:"module"`
|
|
115
|
+
Name string `json:"name"`
|
|
116
|
+
Alias *string `json:"alias"`
|
|
117
|
+
Type string `json:"type"`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type FileEntry struct {
|
|
121
|
+
Classes []ClassInfo `json:"classes"`
|
|
122
|
+
Functions []FunctionInfo `json:"functions"`
|
|
123
|
+
Imports []ImportInfo `json:"imports,omitempty"`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
func isExported(name string) bool {
|
|
129
|
+
if name == "" {
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
return unicode.IsUpper(rune(name[0]))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func exprToString(expr ast.Expr) string {
|
|
136
|
+
if expr == nil {
|
|
137
|
+
return ""
|
|
138
|
+
}
|
|
139
|
+
switch e := expr.(type) {
|
|
140
|
+
case *ast.Ident:
|
|
141
|
+
return e.Name
|
|
142
|
+
case *ast.SelectorExpr:
|
|
143
|
+
return exprToString(e.X) + "." + e.Sel.Name
|
|
144
|
+
case *ast.StarExpr:
|
|
145
|
+
return "*" + exprToString(e.X)
|
|
146
|
+
case *ast.ArrayType:
|
|
147
|
+
return "[]" + exprToString(e.Elt)
|
|
148
|
+
case *ast.MapType:
|
|
149
|
+
return "map[" + exprToString(e.Key) + "]" + exprToString(e.Value)
|
|
150
|
+
case *ast.InterfaceType:
|
|
151
|
+
return "interface{}"
|
|
152
|
+
case *ast.ChanType:
|
|
153
|
+
switch e.Dir {
|
|
154
|
+
case ast.SEND:
|
|
155
|
+
return "chan<- " + exprToString(e.Value)
|
|
156
|
+
case ast.RECV:
|
|
157
|
+
return "<-chan " + exprToString(e.Value)
|
|
158
|
+
default:
|
|
159
|
+
return "chan " + exprToString(e.Value)
|
|
160
|
+
}
|
|
161
|
+
case *ast.FuncType:
|
|
162
|
+
return "func(...)"
|
|
163
|
+
case *ast.Ellipsis:
|
|
164
|
+
return "..." + exprToString(e.Elt)
|
|
165
|
+
case *ast.IndexExpr:
|
|
166
|
+
return exprToString(e.X) + "[" + exprToString(e.Index) + "]"
|
|
167
|
+
case *ast.IndexListExpr:
|
|
168
|
+
parts := make([]string, len(e.Indices))
|
|
169
|
+
for i, idx := range e.Indices {
|
|
170
|
+
parts[i] = exprToString(idx)
|
|
171
|
+
}
|
|
172
|
+
return exprToString(e.X) + "[" + strings.Join(parts, ", ") + "]"
|
|
173
|
+
default:
|
|
174
|
+
return ""
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func docText(doc *ast.CommentGroup) string {
|
|
179
|
+
if doc == nil {
|
|
180
|
+
return ""
|
|
181
|
+
}
|
|
182
|
+
return strings.TrimSpace(doc.Text())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func extractParams(fields *ast.FieldList) []ParamInfo {
|
|
186
|
+
if fields == nil {
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
189
|
+
var params []ParamInfo
|
|
190
|
+
for _, f := range fields.List {
|
|
191
|
+
typeStr := exprToString(f.Type)
|
|
192
|
+
if len(f.Names) == 0 {
|
|
193
|
+
// Unnamed parameter (e.g. in interface method signatures)
|
|
194
|
+
params = append(params, ParamInfo{Name: "", Type: typeStr})
|
|
195
|
+
} else {
|
|
196
|
+
for _, name := range f.Names {
|
|
197
|
+
params = append(params, ParamInfo{Name: name.Name, Type: typeStr})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return params
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func extractReturnType(fields *ast.FieldList) string {
|
|
205
|
+
if fields == nil || len(fields.List) == 0 {
|
|
206
|
+
return ""
|
|
207
|
+
}
|
|
208
|
+
if len(fields.List) == 1 && len(fields.List[0].Names) == 0 {
|
|
209
|
+
return exprToString(fields.List[0].Type)
|
|
210
|
+
}
|
|
211
|
+
// Multiple return values: (Type1, Type2, ...)
|
|
212
|
+
parts := make([]string, 0, len(fields.List))
|
|
213
|
+
for _, f := range fields.List {
|
|
214
|
+
typeStr := exprToString(f.Type)
|
|
215
|
+
if len(f.Names) == 0 {
|
|
216
|
+
parts = append(parts, typeStr)
|
|
217
|
+
} else {
|
|
218
|
+
for _, name := range f.Names {
|
|
219
|
+
parts = append(parts, name.Name+" "+typeStr)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return "(" + strings.Join(parts, ", ") + ")"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func receiverTypeName(fn *ast.FuncDecl) string {
|
|
227
|
+
if fn.Recv == nil || len(fn.Recv.List) == 0 {
|
|
228
|
+
return ""
|
|
229
|
+
}
|
|
230
|
+
t := fn.Recv.List[0].Type
|
|
231
|
+
// Dereference pointer receivers: *T → T
|
|
232
|
+
if star, ok := t.(*ast.StarExpr); ok {
|
|
233
|
+
t = star.X
|
|
234
|
+
}
|
|
235
|
+
if ident, ok := t.(*ast.Ident); ok {
|
|
236
|
+
return ident.Name
|
|
237
|
+
}
|
|
238
|
+
// Generic receiver: T[K] — use the base type name.
|
|
239
|
+
if idx, ok := t.(*ast.IndexExpr); ok {
|
|
240
|
+
if ident, ok := idx.X.(*ast.Ident); ok {
|
|
241
|
+
return ident.Name
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if idx, ok := t.(*ast.IndexListExpr); ok {
|
|
245
|
+
if ident, ok := idx.X.(*ast.Ident); ok {
|
|
246
|
+
return ident.Name
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return ""
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── File collection ───────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
func collectGoFiles(root string) ([]string, error) {
|
|
255
|
+
var files []string
|
|
256
|
+
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
257
|
+
if err != nil {
|
|
258
|
+
return nil // skip unreadable entries
|
|
259
|
+
}
|
|
260
|
+
if info.IsDir() {
|
|
261
|
+
name := info.Name()
|
|
262
|
+
if excludedDirs[name] || strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
|
|
263
|
+
return filepath.SkipDir
|
|
264
|
+
}
|
|
265
|
+
return nil
|
|
266
|
+
}
|
|
267
|
+
if filepath.Ext(path) == ".go" && !strings.HasSuffix(info.Name(), "_test.go") {
|
|
268
|
+
files = append(files, path)
|
|
269
|
+
}
|
|
270
|
+
return nil
|
|
271
|
+
})
|
|
272
|
+
return files, err
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
func isOutsideRoot(rel string) bool {
|
|
276
|
+
return rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func hasExcludedDir(relPath string) bool {
|
|
280
|
+
dir := filepath.Dir(relPath)
|
|
281
|
+
if dir == "." || dir == "" {
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
for _, part := range strings.Split(filepath.ToSlash(dir), "/") {
|
|
285
|
+
if part == "" {
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
if excludedDirs[part] || strings.HasPrefix(part, ".") || strings.HasPrefix(part, "_") {
|
|
289
|
+
return true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Per-file extraction ───────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
func extractFile(filename string, fset *token.FileSet, deep bool) (*FileEntry, error) {
|
|
298
|
+
src, err := os.ReadFile(filename)
|
|
299
|
+
if err != nil {
|
|
300
|
+
return nil, err
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
mode := parser.ParseComments
|
|
304
|
+
f, err := parser.ParseFile(fset, filename, src, mode)
|
|
305
|
+
if err != nil {
|
|
306
|
+
fmt.Fprintf(os.Stderr, "Warning: could not parse %s: %v\n", filename, err)
|
|
307
|
+
return nil, err
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
entry := &FileEntry{
|
|
311
|
+
Classes: []ClassInfo{},
|
|
312
|
+
Functions: []FunctionInfo{},
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Collect receiver methods so we can attach them to structs in deep mode.
|
|
316
|
+
type methodEntry struct {
|
|
317
|
+
receiver string
|
|
318
|
+
method MethodInfo
|
|
319
|
+
}
|
|
320
|
+
var receiverMethods []methodEntry
|
|
321
|
+
|
|
322
|
+
// Map struct/interface name → index in entry.Classes for attaching methods.
|
|
323
|
+
classIndex := map[string]int{}
|
|
324
|
+
|
|
325
|
+
for _, decl := range f.Decls {
|
|
326
|
+
switch d := decl.(type) {
|
|
327
|
+
case *ast.GenDecl:
|
|
328
|
+
for _, spec := range d.Specs {
|
|
329
|
+
ts, ok := spec.(*ast.TypeSpec)
|
|
330
|
+
if !ok {
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
if !isExported(ts.Name.Name) {
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
switch st := ts.Type.(type) {
|
|
338
|
+
case *ast.StructType:
|
|
339
|
+
ci := ClassInfo{
|
|
340
|
+
Name: ts.Name.Name,
|
|
341
|
+
Kind: "struct",
|
|
342
|
+
Bases: []string{},
|
|
343
|
+
Line: fset.Position(ts.Pos()).Line,
|
|
344
|
+
}
|
|
345
|
+
if deep {
|
|
346
|
+
ci.Docstring = docText(d.Doc)
|
|
347
|
+
if ci.Docstring == "" {
|
|
348
|
+
ci.Docstring = docText(ts.Doc)
|
|
349
|
+
}
|
|
350
|
+
ci.Decorators = []string{}
|
|
351
|
+
ci.Attributes = []AttributeInfo{}
|
|
352
|
+
ci.Methods = []MethodInfo{}
|
|
353
|
+
}
|
|
354
|
+
// Extract embedded types as bases + attributes.
|
|
355
|
+
if st.Fields != nil {
|
|
356
|
+
for _, field := range st.Fields.List {
|
|
357
|
+
if len(field.Names) == 0 {
|
|
358
|
+
// Embedded type → base
|
|
359
|
+
ci.Bases = append(ci.Bases, exprToString(field.Type))
|
|
360
|
+
} else if deep {
|
|
361
|
+
// Named field → attribute
|
|
362
|
+
tag := ""
|
|
363
|
+
if field.Tag != nil {
|
|
364
|
+
tag = field.Tag.Value
|
|
365
|
+
}
|
|
366
|
+
for _, name := range field.Names {
|
|
367
|
+
if isExported(name.Name) {
|
|
368
|
+
ci.Attributes = append(ci.Attributes, AttributeInfo{
|
|
369
|
+
Name: name.Name,
|
|
370
|
+
Type: exprToString(field.Type),
|
|
371
|
+
Tag: tag,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
classIndex[ci.Name] = len(entry.Classes)
|
|
379
|
+
entry.Classes = append(entry.Classes, ci)
|
|
380
|
+
|
|
381
|
+
case *ast.InterfaceType:
|
|
382
|
+
ci := ClassInfo{
|
|
383
|
+
Name: ts.Name.Name,
|
|
384
|
+
Kind: "interface",
|
|
385
|
+
Bases: []string{},
|
|
386
|
+
Line: fset.Position(ts.Pos()).Line,
|
|
387
|
+
}
|
|
388
|
+
if deep {
|
|
389
|
+
ci.Docstring = docText(d.Doc)
|
|
390
|
+
if ci.Docstring == "" {
|
|
391
|
+
ci.Docstring = docText(ts.Doc)
|
|
392
|
+
}
|
|
393
|
+
ci.Decorators = []string{}
|
|
394
|
+
ci.Attributes = []AttributeInfo{}
|
|
395
|
+
ci.Methods = []MethodInfo{}
|
|
396
|
+
}
|
|
397
|
+
if st.Methods != nil {
|
|
398
|
+
for _, m := range st.Methods.List {
|
|
399
|
+
if len(m.Names) == 0 {
|
|
400
|
+
// Embedded interface
|
|
401
|
+
ci.Bases = append(ci.Bases, exprToString(m.Type))
|
|
402
|
+
} else if deep {
|
|
403
|
+
ft, ok := m.Type.(*ast.FuncType)
|
|
404
|
+
if !ok {
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
ci.Methods = append(ci.Methods, MethodInfo{
|
|
408
|
+
Name: m.Names[0].Name,
|
|
409
|
+
Line: fset.Position(m.Pos()).Line,
|
|
410
|
+
IsAsync: false,
|
|
411
|
+
Docstring: docText(m.Doc),
|
|
412
|
+
Decorators: []string{},
|
|
413
|
+
Params: extractParams(ft.Params),
|
|
414
|
+
ReturnType: extractReturnType(ft.Results),
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
classIndex[ci.Name] = len(entry.Classes)
|
|
420
|
+
entry.Classes = append(entry.Classes, ci)
|
|
421
|
+
|
|
422
|
+
default:
|
|
423
|
+
// Type alias (type X = Y) or named type (type X Y).
|
|
424
|
+
kind := "named_type"
|
|
425
|
+
if ts.Assign.IsValid() {
|
|
426
|
+
kind = "type_alias"
|
|
427
|
+
}
|
|
428
|
+
ci := ClassInfo{
|
|
429
|
+
Name: ts.Name.Name,
|
|
430
|
+
Kind: kind,
|
|
431
|
+
Bases: []string{},
|
|
432
|
+
Line: fset.Position(ts.Pos()).Line,
|
|
433
|
+
}
|
|
434
|
+
if deep {
|
|
435
|
+
ci.Docstring = docText(d.Doc)
|
|
436
|
+
if ci.Docstring == "" {
|
|
437
|
+
ci.Docstring = docText(ts.Doc)
|
|
438
|
+
}
|
|
439
|
+
ci.Decorators = []string{}
|
|
440
|
+
ci.Attributes = []AttributeInfo{}
|
|
441
|
+
ci.Methods = []MethodInfo{}
|
|
442
|
+
}
|
|
443
|
+
classIndex[ci.Name] = len(entry.Classes)
|
|
444
|
+
entry.Classes = append(entry.Classes, ci)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
case *ast.FuncDecl:
|
|
449
|
+
if !isExported(d.Name.Name) {
|
|
450
|
+
continue
|
|
451
|
+
}
|
|
452
|
+
recv := receiverTypeName(d)
|
|
453
|
+
if recv != "" {
|
|
454
|
+
// Receiver method — collect for later attachment.
|
|
455
|
+
mi := MethodInfo{
|
|
456
|
+
Name: d.Name.Name,
|
|
457
|
+
Line: fset.Position(d.Pos()).Line,
|
|
458
|
+
IsAsync: false,
|
|
459
|
+
}
|
|
460
|
+
if deep {
|
|
461
|
+
mi.Docstring = docText(d.Doc)
|
|
462
|
+
mi.Decorators = []string{}
|
|
463
|
+
mi.Params = extractParams(d.Type.Params)
|
|
464
|
+
mi.ReturnType = extractReturnType(d.Type.Results)
|
|
465
|
+
}
|
|
466
|
+
receiverMethods = append(receiverMethods, methodEntry{
|
|
467
|
+
receiver: recv,
|
|
468
|
+
method: mi,
|
|
469
|
+
})
|
|
470
|
+
} else {
|
|
471
|
+
// Top-level function.
|
|
472
|
+
fi := FunctionInfo{
|
|
473
|
+
Name: d.Name.Name,
|
|
474
|
+
Line: fset.Position(d.Pos()).Line,
|
|
475
|
+
IsAsync: false,
|
|
476
|
+
}
|
|
477
|
+
if deep {
|
|
478
|
+
fi.Docstring = docText(d.Doc)
|
|
479
|
+
fi.Decorators = []string{}
|
|
480
|
+
fi.Params = extractParams(d.Type.Params)
|
|
481
|
+
fi.ReturnType = extractReturnType(d.Type.Results)
|
|
482
|
+
}
|
|
483
|
+
entry.Functions = append(entry.Functions, fi)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Attach receiver methods to their struct in deep mode.
|
|
489
|
+
if deep {
|
|
490
|
+
for _, rm := range receiverMethods {
|
|
491
|
+
if idx, ok := classIndex[rm.receiver]; ok {
|
|
492
|
+
entry.Classes[idx].Methods = append(entry.Classes[idx].Methods, rm.method)
|
|
493
|
+
} else {
|
|
494
|
+
// Receiver type not found as a class (e.g. unexported struct) —
|
|
495
|
+
// expose as a standalone function with receiver field.
|
|
496
|
+
entry.Functions = append(entry.Functions, FunctionInfo{
|
|
497
|
+
Name: rm.method.Name,
|
|
498
|
+
Line: rm.method.Line,
|
|
499
|
+
IsAsync: false,
|
|
500
|
+
Receiver: rm.receiver,
|
|
501
|
+
Docstring: rm.method.Docstring,
|
|
502
|
+
Decorators: rm.method.Decorators,
|
|
503
|
+
Params: rm.method.Params,
|
|
504
|
+
ReturnType: rm.method.ReturnType,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
// Shallow mode: receiver methods as standalone functions with receiver field.
|
|
510
|
+
for _, rm := range receiverMethods {
|
|
511
|
+
entry.Functions = append(entry.Functions, FunctionInfo{
|
|
512
|
+
Name: rm.method.Name,
|
|
513
|
+
Line: rm.method.Line,
|
|
514
|
+
IsAsync: false,
|
|
515
|
+
Receiver: rm.receiver,
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── Imports (deep only) ──────────────────────────────────────────────────
|
|
521
|
+
if deep && f.Imports != nil {
|
|
522
|
+
entry.Imports = []ImportInfo{}
|
|
523
|
+
for _, imp := range f.Imports {
|
|
524
|
+
path := strings.Trim(imp.Path.Value, `"`)
|
|
525
|
+
// Use the last segment as the name (e.g. "fmt" from "fmt",
|
|
526
|
+
// "json" from "encoding/json").
|
|
527
|
+
parts := strings.Split(path, "/")
|
|
528
|
+
name := parts[len(parts)-1]
|
|
529
|
+
var alias *string
|
|
530
|
+
impType := "import"
|
|
531
|
+
if imp.Name != nil {
|
|
532
|
+
switch imp.Name.Name {
|
|
533
|
+
case ".":
|
|
534
|
+
impType = "dot"
|
|
535
|
+
case "_":
|
|
536
|
+
impType = "blank"
|
|
537
|
+
default:
|
|
538
|
+
a := imp.Name.Name
|
|
539
|
+
alias = &a
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
entry.Imports = append(entry.Imports, ImportInfo{
|
|
543
|
+
Module: path,
|
|
544
|
+
Name: name,
|
|
545
|
+
Alias: alias,
|
|
546
|
+
Type: impType,
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return entry, nil
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
555
|
+
|
|
556
|
+
func main() {
|
|
557
|
+
srcDir := flag.String("src-dir", ".", "Root directory to scan")
|
|
558
|
+
onlyFiles := flag.String("only-files", "", "Comma-separated list of files to extract")
|
|
559
|
+
deep := flag.Bool("deep", false, "Include enriched data (docs, attributes, methods, imports)")
|
|
560
|
+
flag.Parse()
|
|
561
|
+
|
|
562
|
+
fset := token.NewFileSet()
|
|
563
|
+
inventory := map[string]*FileEntry{}
|
|
564
|
+
|
|
565
|
+
var files []string
|
|
566
|
+
if *onlyFiles != "" {
|
|
567
|
+
for _, f := range strings.Split(*onlyFiles, ",") {
|
|
568
|
+
f = strings.TrimSpace(f)
|
|
569
|
+
if f == "" {
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
572
|
+
abs := f
|
|
573
|
+
if !filepath.IsAbs(f) {
|
|
574
|
+
abs = filepath.Join(*srcDir, f)
|
|
575
|
+
}
|
|
576
|
+
rel, err := filepath.Rel(*srcDir, abs)
|
|
577
|
+
if err != nil || isOutsideRoot(rel) || hasExcludedDir(rel) {
|
|
578
|
+
continue
|
|
579
|
+
}
|
|
580
|
+
if _, err := os.Stat(abs); err == nil && filepath.Ext(abs) == ".go" && !strings.HasSuffix(filepath.Base(abs), "_test.go") {
|
|
581
|
+
files = append(files, abs)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
var err error
|
|
586
|
+
files, err = collectGoFiles(*srcDir)
|
|
587
|
+
if err != nil {
|
|
588
|
+
fmt.Fprintf(os.Stderr, "Error walking %s: %v\n", *srcDir, err)
|
|
589
|
+
os.Exit(1)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
for _, file := range files {
|
|
594
|
+
entry, err := extractFile(file, fset, *deep)
|
|
595
|
+
if err != nil {
|
|
596
|
+
continue // warning already printed inside extractFile
|
|
597
|
+
}
|
|
598
|
+
// Use relative path from srcDir as key.
|
|
599
|
+
rel, err := filepath.Rel(*srcDir, file)
|
|
600
|
+
if err != nil {
|
|
601
|
+
rel = file
|
|
602
|
+
}
|
|
603
|
+
inventory[rel] = entry
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ── Cross-file receiver attachment (deep mode, per-package) ────────────────
|
|
607
|
+
// Go methods are often defined in a different file from the type they extend.
|
|
608
|
+
// After all files are processed, attach any lingering receiver-method functions
|
|
609
|
+
// to their type within the same directory (= package boundary).
|
|
610
|
+
if *deep {
|
|
611
|
+
// Build: dir → typeName → {relPath, classIdx}
|
|
612
|
+
type classRef struct {
|
|
613
|
+
relPath string
|
|
614
|
+
classIdx int
|
|
615
|
+
}
|
|
616
|
+
dirClassIndex := map[string]map[string]classRef{}
|
|
617
|
+
for relPath, entry := range inventory {
|
|
618
|
+
dir := filepath.Dir(relPath)
|
|
619
|
+
if _, ok := dirClassIndex[dir]; !ok {
|
|
620
|
+
dirClassIndex[dir] = map[string]classRef{}
|
|
621
|
+
}
|
|
622
|
+
for idx, cls := range entry.Classes {
|
|
623
|
+
dirClassIndex[dir][cls.Name] = classRef{relPath: relPath, classIdx: idx}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// For each file, find functions with a Receiver resolvable within
|
|
628
|
+
// the same directory and move them into the target class's methods.
|
|
629
|
+
for relPath, entry := range inventory {
|
|
630
|
+
dir := filepath.Dir(relPath)
|
|
631
|
+
remaining := make([]FunctionInfo, 0, len(entry.Functions))
|
|
632
|
+
for _, fn := range entry.Functions {
|
|
633
|
+
if fn.Receiver == "" {
|
|
634
|
+
remaining = append(remaining, fn)
|
|
635
|
+
continue
|
|
636
|
+
}
|
|
637
|
+
ref, ok := dirClassIndex[dir][fn.Receiver]
|
|
638
|
+
if !ok {
|
|
639
|
+
// Unexported or unknown type — leave as a function.
|
|
640
|
+
remaining = append(remaining, fn)
|
|
641
|
+
continue
|
|
642
|
+
}
|
|
643
|
+
// Attach to the target class as a method.
|
|
644
|
+
target := inventory[ref.relPath]
|
|
645
|
+
target.Classes[ref.classIdx].Methods = append(
|
|
646
|
+
target.Classes[ref.classIdx].Methods,
|
|
647
|
+
MethodInfo{
|
|
648
|
+
Name: fn.Name,
|
|
649
|
+
Line: fn.Line,
|
|
650
|
+
IsAsync: false,
|
|
651
|
+
Docstring: fn.Docstring,
|
|
652
|
+
Decorators: fn.Decorators,
|
|
653
|
+
Params: fn.Params,
|
|
654
|
+
ReturnType: fn.ReturnType,
|
|
655
|
+
},
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
entry.Functions = remaining
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
enc := json.NewEncoder(os.Stdout)
|
|
663
|
+
enc.SetIndent("", " ")
|
|
664
|
+
if err := enc.Encode(inventory); err != nil {
|
|
665
|
+
fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
|
|
666
|
+
os.Exit(1)
|
|
667
|
+
}
|
|
668
|
+
}
|