devlensio 0.6.2 → 1.0.1

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.
Files changed (129) hide show
  1. package/README.md +89 -31
  2. package/dist/extractors/detectLanguage.d.ts +2 -0
  3. package/dist/extractors/detectLanguage.js +33 -0
  4. package/dist/extractors/index.d.ts +6 -0
  5. package/dist/extractors/index.js +155 -0
  6. package/dist/extractors/runner.d.ts +7 -0
  7. package/dist/extractors/runner.js +194 -0
  8. package/dist/extractors/types.d.ts +37 -0
  9. package/dist/extractors/types.js +8 -0
  10. package/dist/graph/buildLookup.d.ts +1 -0
  11. package/dist/graph/buildLookup.js +2 -1
  12. package/dist/graph/edges/callEdges.js +19 -5
  13. package/dist/graph/edges/callEdges.test.d.ts +1 -0
  14. package/dist/graph/edges/callEdges.test.js +200 -0
  15. package/dist/graph/edges/importEdges.js +12 -0
  16. package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
  17. package/dist/graph/edges/inheritanceEdges.js +73 -0
  18. package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
  19. package/dist/graph/edges/inheritanceEdges.test.js +140 -0
  20. package/dist/graph/index.js +4 -0
  21. package/dist/parser/classes.test.d.ts +1 -0
  22. package/dist/parser/classes.test.js +360 -0
  23. package/dist/parser/extractors/classes.d.ts +5 -0
  24. package/dist/parser/extractors/classes.js +241 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +7 -1
  27. package/dist/pipeline/index.d.ts +2 -1
  28. package/dist/pipeline/index.js +30 -42
  29. package/dist/scoring/index.js +8 -0
  30. package/dist/scoring/index.test.js +43 -0
  31. package/dist/scoring/nodeScorer.js +2 -0
  32. package/dist/scoring/pruneDisconnected.d.ts +8 -0
  33. package/dist/scoring/pruneDisconnected.js +66 -0
  34. package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
  35. package/dist/scoring/pruneDisconnected.test.js +123 -0
  36. package/dist/summarizer/prompts.d.ts +1 -1
  37. package/dist/types.d.ts +5 -5
  38. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  39. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  40. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  41. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  42. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  43. package/extractors/go/build.mjs +43 -0
  44. package/extractors/go/calls.go +289 -0
  45. package/extractors/go/contract.go +140 -0
  46. package/extractors/go/extractor.go +161 -0
  47. package/extractors/go/fingerprint.go +167 -0
  48. package/extractors/go/go.mod +3 -0
  49. package/extractors/go/imports.go +90 -0
  50. package/extractors/go/inheritance.go +138 -0
  51. package/extractors/go/lookup.go +106 -0
  52. package/extractors/go/main.go +57 -0
  53. package/extractors/go/nodes.go +177 -0
  54. package/extractors/go/orm_edges.go +277 -0
  55. package/extractors/go/parser.go +469 -0
  56. package/extractors/go/routes.go +521 -0
  57. package/extractors/go/tests.go +45 -0
  58. package/extractors/go/thirdparty.go +138 -0
  59. package/extractors/go/typeload.go +178 -0
  60. package/extractors/go/walker.go +67 -0
  61. package/extractors/java/build.mjs +90 -0
  62. package/extractors/java/devlens_java_extractor.jar +0 -0
  63. package/extractors/java/src/devlens/extractor/Contract.java +171 -0
  64. package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
  65. package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
  66. package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
  67. package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
  68. package/extractors/java/src/devlens/extractor/Main.java +66 -0
  69. package/extractors/java/src/devlens/extractor/Parser.java +522 -0
  70. package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
  71. package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
  72. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
  73. package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
  74. package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
  75. package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
  76. package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
  77. package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
  78. package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
  79. package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
  80. package/extractors/python/devlens_extractors_python/__init__.py +3 -0
  81. package/extractors/python/devlens_extractors_python/__main__.py +33 -0
  82. package/extractors/python/devlens_extractors_python/contract.py +94 -0
  83. package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
  84. package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
  85. package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
  86. package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
  87. package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
  88. package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
  89. package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
  90. package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
  91. package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
  92. package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
  93. package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
  94. package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
  95. package/extractors/python/devlens_extractors_python/extractor.py +107 -0
  96. package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
  97. package/extractors/python/devlens_extractors_python/lookup.py +72 -0
  98. package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
  99. package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
  100. package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
  101. package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
  102. package/extractors/python/devlens_extractors_python/third_party.py +103 -0
  103. package/extractors/python/pyproject.toml +16 -0
  104. package/extractors/python/setup.mjs +54 -0
  105. package/extractors/rust/Cargo.toml +29 -0
  106. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  107. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  108. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  109. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  110. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  111. package/extractors/rust/build.mjs +85 -0
  112. package/extractors/rust/src/calls.rs +295 -0
  113. package/extractors/rust/src/contract.rs +236 -0
  114. package/extractors/rust/src/enrich.rs +107 -0
  115. package/extractors/rust/src/extractor.rs +199 -0
  116. package/extractors/rust/src/fingerprint.rs +238 -0
  117. package/extractors/rust/src/imports.rs +101 -0
  118. package/extractors/rust/src/inheritance.rs +233 -0
  119. package/extractors/rust/src/lookup.rs +226 -0
  120. package/extractors/rust/src/main.rs +53 -0
  121. package/extractors/rust/src/module_map.rs +131 -0
  122. package/extractors/rust/src/nodes.rs +174 -0
  123. package/extractors/rust/src/orm_edges.rs +125 -0
  124. package/extractors/rust/src/parser.rs +859 -0
  125. package/extractors/rust/src/routes.rs +1265 -0
  126. package/extractors/rust/src/tests.rs +109 -0
  127. package/extractors/rust/src/thirdparty.rs +114 -0
  128. package/extractors/rust/src/walker.rs +82 -0
  129. package/package.json +22 -4
@@ -0,0 +1,469 @@
1
+ // parser.go — parse-time FACT collection (the only AST pass).
2
+ //
3
+ // Per .go file: package name, imports, functions/methods (Recv nil|non-nil),
4
+ // structs (fields, tags, embedded types), interfaces (method names), iota
5
+ // const blocks (enum pattern), and body CALLS. Everything downstream (edges)
6
+ // resolves from these facts via lookup maps — never re-walks ASTs.
7
+
8
+ package main
9
+
10
+ import (
11
+ "go/ast"
12
+ "go/parser"
13
+ "go/token"
14
+ "os"
15
+ "path/filepath"
16
+ "sort"
17
+ "strings"
18
+ )
19
+
20
+ // ── fact types ──
21
+
22
+ type CallSite struct {
23
+ Str string // call string: "f" or "a.b.c" (contract metadata.calls)
24
+ Sel string // method name for selector form ("Find" in db.Find)
25
+ Receiver string // receiver expr text for selector form ("db")
26
+ IsSel bool
27
+ Node ast.Expr // the callee node (Ident or SelectorExpr) — types lookup
28
+ Pos token.Pos
29
+ }
30
+
31
+ type ParsedFunc struct {
32
+ Name string
33
+ IsMethod bool
34
+ RecvType string // receiver type name (no *), e.g. "User"
35
+ PkgPath string
36
+ Start int
37
+ End int
38
+ RawCode string
39
+ File string
40
+ Calls []CallSite
41
+ IsTest bool
42
+ Decl *ast.FuncDecl // the decl node — routes.go needs it for body walks
43
+ }
44
+
45
+ type FieldInfo struct {
46
+ Name string
47
+ Type string
48
+ Tag string
49
+ }
50
+
51
+ type ParsedStruct struct {
52
+ Name string
53
+ PkgPath string
54
+ Fields []FieldInfo
55
+ Embedded []string
56
+ Start int
57
+ End int
58
+ RawCode string
59
+ File string
60
+ IsEnum bool
61
+ Constants []string
62
+ }
63
+
64
+ type ParsedInterface struct {
65
+ Name string
66
+ PkgPath string
67
+ Methods []string
68
+ Embedded []string // embedded interfaces (no method names in AST field)
69
+ Start int
70
+ End int
71
+ RawCode string
72
+ File string
73
+ }
74
+
75
+ type ImportSpec struct {
76
+ Path string // import path, unquoted
77
+ Alias string // explicit alias or "" (use package name)
78
+ }
79
+
80
+ type ParsedFile struct {
81
+ RelPath string
82
+ PkgName string
83
+ IsTest bool
84
+ Imports []ImportSpec
85
+ Funcs []*ParsedFunc
86
+ Structs []*ParsedStruct
87
+ Interfaces []*ParsedInterface
88
+ EndLine int
89
+ AST *ast.File // kept for go/types check (typeload.go)
90
+ // iota const blocks on named numeric types (enum pattern, stretch)
91
+ numericEnums []numericEnum
92
+ }
93
+
94
+ type ParsedPackage struct {
95
+ ImportPath string // module path + rel dir
96
+ Dir string // rel dir from repo root ("" or "." for root)
97
+ Name string // package name
98
+ Files []*ParsedFile
99
+ fset *token.FileSet // shared per package (positions must share one fset)
100
+ }
101
+
102
+ type parsedRepo struct {
103
+ files []*ParsedFile // ALL .go files (tests included), walk order
104
+ packages []*ParsedPackage
105
+ fileByPath map[string]*ParsedFile
106
+ mod *modFile
107
+ repoPath string
108
+ skipped int
109
+ }
110
+
111
+ type errFn func(file, msg string)
112
+
113
+ // parseRepo — walk → group into packages → parse each file into facts.
114
+ func parseRepo(repoPath string, mf *modFile, addError errFn) *parsedRepo {
115
+ pr := &parsedRepo{fileByPath: map[string]*ParsedFile{}, mod: mf, repoPath: repoPath}
116
+ all := goFiles(repoPath)
117
+ groups := groupByDir(all)
118
+ for _, dir := range sortedKeys(groups) {
119
+ group := groups[dir]
120
+ pkg := &ParsedPackage{
121
+ Dir: dir,
122
+ ImportPath: importPathForDir(mf, dir),
123
+ fset: token.NewFileSet(),
124
+ }
125
+ for _, rel := range group {
126
+ pf := parseFile(repoPath, rel, pkg, addError)
127
+ if pf == nil {
128
+ pr.skipped++
129
+ continue
130
+ }
131
+ if pkg.Name == "" {
132
+ pkg.Name = pf.PkgName
133
+ }
134
+ pkg.Files = append(pkg.Files, pf)
135
+ pr.files = append(pr.files, pf)
136
+ pr.fileByPath[rel] = pf
137
+ }
138
+ if len(pkg.Files) > 0 {
139
+ pr.packages = append(pr.packages, pkg)
140
+ }
141
+ }
142
+ return pr
143
+ }
144
+
145
+ // importPathForDir — module path + rel dir (root → module path itself). For
146
+ // repos without go.mod, the rel dir stands in as the pseudo import path.
147
+ func importPathForDir(mf *modFile, dir string) string {
148
+ base := mf.ModulePath
149
+ if base == "" {
150
+ return dir
151
+ }
152
+ if dir == "." || dir == "" {
153
+ return base
154
+ }
155
+ return base + "/" + strings.TrimPrefix(dir, "./")
156
+ }
157
+
158
+ // parseFile — one file → facts. nil on parse error (caller counts skipped).
159
+ func parseFile(repoPath, rel string, pkg *ParsedPackage, addError errFn) *ParsedFile {
160
+ abs := filepath.Join(repoPath, rel)
161
+ src, err := os.ReadFile(abs)
162
+ if err != nil {
163
+ addError(rel, "read: "+err.Error())
164
+ return nil
165
+ }
166
+ fset := pkg.fset
167
+ f, err := parser.ParseFile(fset, rel, src, parser.ParseComments)
168
+ if err != nil {
169
+ addError(rel, "parse: "+err.Error())
170
+ return nil
171
+ }
172
+ pf := &ParsedFile{
173
+ RelPath: rel,
174
+ PkgName: f.Name.Name,
175
+ IsTest: strings.HasSuffix(rel, "_test.go"),
176
+ AST: f,
177
+ }
178
+ if pf.EndLine = fset.Position(f.End()).Line; pf.EndLine == 0 {
179
+ pf.EndLine = 1
180
+ }
181
+ // imports
182
+ for _, imp := range f.Imports {
183
+ p, _ := strconvUnquote(imp.Path)
184
+ alias := ""
185
+ if imp.Name != nil {
186
+ alias = imp.Name.Name
187
+ }
188
+ pf.Imports = append(pf.Imports, ImportSpec{Path: p, Alias: alias})
189
+ }
190
+ // named numeric types (the iota-enum substrate)
191
+ numericTypes := map[string]bool{}
192
+ // const specs: type → constants
193
+ constBlocks := map[string][]string{}
194
+
195
+ for _, decl := range f.Decls {
196
+ switch d := decl.(type) {
197
+ case *ast.FuncDecl:
198
+ fn := parseFuncDecl(fset, src, rel, pkg, d, pf.IsTest)
199
+ if fn != nil {
200
+ pf.Funcs = append(pf.Funcs, fn)
201
+ }
202
+ case *ast.GenDecl:
203
+ switch d.Tok {
204
+ case token.TYPE:
205
+ for _, spec := range d.Specs {
206
+ ts, ok := spec.(*ast.TypeSpec)
207
+ if !ok {
208
+ continue
209
+ }
210
+ start := fset.Position(d.Pos()).Line
211
+ end := fset.Position(d.End()).Line
212
+ raw := string(src[fset.Position(d.Pos()).Offset:fset.Position(d.End()).Offset])
213
+ switch t := ts.Type.(type) {
214
+ case *ast.StructType:
215
+ st := &ParsedStruct{
216
+ Name: ts.Name.Name, PkgPath: pkg.ImportPath,
217
+ Start: start, End: end, RawCode: raw, File: rel,
218
+ }
219
+ for _, field := range t.Fields.List {
220
+ ftype := exprText(field.Type)
221
+ tag := ""
222
+ if field.Tag != nil {
223
+ tag, _ = strconvUnquote(field.Tag)
224
+ }
225
+ if len(field.Names) == 0 {
226
+ // embedded field
227
+ st.Embedded = append(st.Embedded, ftype)
228
+ st.Fields = append(st.Fields, FieldInfo{Name: "", Type: ftype, Tag: tag})
229
+ } else {
230
+ for _, n := range field.Names {
231
+ st.Fields = append(st.Fields, FieldInfo{Name: n.Name, Type: ftype, Tag: tag})
232
+ }
233
+ }
234
+ }
235
+ pf.Structs = append(pf.Structs, st)
236
+ case *ast.InterfaceType:
237
+ it := &ParsedInterface{
238
+ Name: ts.Name.Name, PkgPath: pkg.ImportPath,
239
+ Start: start, End: end, RawCode: raw, File: rel,
240
+ }
241
+ for _, m := range t.Methods.List {
242
+ if len(m.Names) > 0 {
243
+ it.Methods = append(it.Methods, m.Names[0].Name)
244
+ } else {
245
+ // embedded interface
246
+ it.Embedded = append(it.Embedded, exprText(m.Type))
247
+ }
248
+ }
249
+ pf.Interfaces = append(pf.Interfaces, it)
250
+ case *ast.Ident, *ast.StarExpr, *ast.ArrayType, *ast.SelectorExpr:
251
+ // plain named type (type X int / type X = Other) —
252
+ // the enum substrate if numeric
253
+ if isNumericType(t) {
254
+ numericTypes[ts.Name.Name] = true
255
+ }
256
+ }
257
+ }
258
+ case token.CONST:
259
+ // Go const blocks inherit type + value from the previous spec
260
+ // (`Red Color = iota; Green; Blue`). Two passes: resolve names
261
+ // with their inherited type, then check if the block uses iota.
262
+ type constEntry struct{ name, typeName string }
263
+ var entries []constEntry
264
+ lastType := ""
265
+ blockIota := false
266
+ for _, spec := range d.Specs {
267
+ cs, ok := spec.(*ast.ValueSpec)
268
+ if !ok || len(cs.Names) == 0 {
269
+ continue
270
+ }
271
+ if cs.Type != nil {
272
+ lastType = exprText(cs.Type)
273
+ }
274
+ hasIota := false
275
+ ast.Inspect(cs, func(n ast.Node) bool {
276
+ if id, ok := n.(*ast.Ident); ok && id.Name == "iota" {
277
+ hasIota = true
278
+ }
279
+ return true
280
+ })
281
+ if hasIota {
282
+ blockIota = true
283
+ }
284
+ if lastType != "" {
285
+ for _, n := range cs.Names {
286
+ entries = append(entries, constEntry{n.Name, lastType})
287
+ }
288
+ }
289
+ }
290
+ if blockIota {
291
+ for _, e := range entries {
292
+ constBlocks[e.typeName] = append(constBlocks[e.typeName], e.name)
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+ // enum pattern: named numeric type + iota const block → ENUM (stretch)
299
+ for typeName, consts := range constBlocks {
300
+ if numericTypes[typeName] {
301
+ for _, st := range pf.Structs {
302
+ if st.Name == typeName {
303
+ st.IsEnum = true
304
+ st.Constants = consts
305
+ }
306
+ }
307
+ // numeric types aren't Structs — record them on the file for
308
+ // node emission via a synthetic entry
309
+ pf.numericEnums = append(pf.numericEnums, numericEnum{TypeName: typeName, Constants: consts})
310
+ }
311
+ }
312
+ return pf
313
+ }
314
+
315
+ type numericEnum struct {
316
+ TypeName string
317
+ Constants []string
318
+ }
319
+
320
+ // parseFuncDecl — FuncDecl → facts. Test funcs (TestXxx in *_test.go) keep
321
+ // their name; node emission treats test files as leaves (nodes=[]).
322
+ func parseFuncDecl(fset *token.FileSet, src []byte, rel string, pkg *ParsedPackage, d *ast.FuncDecl, isTestFile bool) *ParsedFunc {
323
+ fn := &ParsedFunc{
324
+ Name: d.Name.Name,
325
+ PkgPath: pkg.ImportPath,
326
+ Start: fset.Position(d.Pos()).Line,
327
+ End: fset.Position(d.End()).Line,
328
+ RawCode: string(src[fset.Position(d.Pos()).Offset:fset.Position(d.End()).Offset]),
329
+ File: rel,
330
+ IsMethod: d.Recv != nil && len(d.Recv.List) > 0,
331
+ Decl: d,
332
+ }
333
+ if fn.IsMethod {
334
+ fn.RecvType = receiverTypeName(d.Recv.List[0].Type)
335
+ }
336
+ if isTestFile && strings.HasPrefix(d.Name.Name, "Test") {
337
+ fn.IsTest = true
338
+ }
339
+ if d.Body != nil {
340
+ fn.Calls = collectCalls(d.Body)
341
+ }
342
+ return fn
343
+ }
344
+
345
+ // receiverTypeName — "(u User)" / "(u *User)" / "(u *User[T])" → "User".
346
+ func receiverTypeName(e ast.Expr) string {
347
+ switch t := e.(type) {
348
+ case *ast.StarExpr:
349
+ return receiverTypeName(t.X)
350
+ case *ast.Ident:
351
+ return t.Name
352
+ case *ast.IndexExpr:
353
+ return receiverTypeName(t.X)
354
+ case *ast.IndexListExpr:
355
+ return receiverTypeName(t.X)
356
+ case *ast.ParenExpr:
357
+ return receiverTypeName(t.X)
358
+ }
359
+ return exprText(e)
360
+ }
361
+
362
+ // collectCalls — all CallExprs in a body (scope rule: closures inside the
363
+ // body attribute to the enclosing function — Go has no nested func decls).
364
+ func collectCalls(body ast.Node) []CallSite {
365
+ var calls []CallSite
366
+ ast.Inspect(body, func(n ast.Node) bool {
367
+ call, ok := n.(*ast.CallExpr)
368
+ if !ok {
369
+ return true
370
+ }
371
+ fun := call.Fun
372
+ // unwrap generic calls: f[T](...)
373
+ if idx, ok := fun.(*ast.IndexExpr); ok {
374
+ fun = idx.X
375
+ } else if idx, ok := fun.(*ast.IndexListExpr); ok {
376
+ fun = idx.X
377
+ }
378
+ switch f := fun.(type) {
379
+ case *ast.Ident:
380
+ calls = append(calls, CallSite{Str: f.Name, Node: f, Pos: call.Pos()})
381
+ case *ast.SelectorExpr:
382
+ calls = append(calls, CallSite{
383
+ Str: exprText(f),
384
+ Sel: f.Sel.Name,
385
+ Receiver: identName(f.X), // root ident survives call chains: DB.Model(...).Updates → "DB"
386
+ IsSel: true,
387
+ Node: f,
388
+ Pos: call.Pos(),
389
+ })
390
+ case *ast.FuncLit:
391
+ // inline anonymous invocation — no symbol to resolve
392
+ }
393
+ return true
394
+ })
395
+ return calls
396
+ }
397
+
398
+ // exprText — renders common AST expressions to source text.
399
+ func exprText(e ast.Expr) string {
400
+ switch t := e.(type) {
401
+ case *ast.Ident:
402
+ return t.Name
403
+ case *ast.SelectorExpr:
404
+ return exprText(t.X) + "." + t.Sel.Name
405
+ case *ast.StarExpr:
406
+ return "*" + exprText(t.X)
407
+ case *ast.IndexExpr:
408
+ return exprText(t.X)
409
+ case *ast.IndexListExpr:
410
+ return exprText(t.X)
411
+ case *ast.ParenExpr:
412
+ return exprText(t.X)
413
+ case *ast.BasicLit:
414
+ return t.Value
415
+ case *ast.CallExpr:
416
+ return exprText(t.Fun) // chains render as the callee path: DB.Model(...).Updates → "DB.Model.Updates"
417
+ case *ast.ArrayType:
418
+ if t.Len == nil {
419
+ return "[]" + exprText(t.Elt)
420
+ }
421
+ return "[" + exprText(t.Len) + "]" + exprText(t.Elt)
422
+ case *ast.MapType:
423
+ return "map[" + exprText(t.Key) + "]" + exprText(t.Value)
424
+ case *ast.FuncType:
425
+ return "func"
426
+ case *ast.ChanType:
427
+ return "chan"
428
+ case *ast.InterfaceType:
429
+ return "interface{}"
430
+ case *ast.StructType:
431
+ return "struct{}"
432
+ }
433
+ return "?"
434
+ }
435
+
436
+ func isNumericType(e ast.Expr) bool {
437
+ switch t := e.(type) {
438
+ case *ast.Ident:
439
+ switch t.Name {
440
+ case "int", "int8", "int16", "int32", "int64", "uint", "uint8",
441
+ "uint16", "uint32", "uint64", "uintptr", "float32", "float64",
442
+ "complex64", "complex128", "byte", "rune":
443
+ return true
444
+ }
445
+ case *ast.SelectorExpr:
446
+ return isNumericType(t.X) // e.g. time.Duration — treat as numeric-ish
447
+ }
448
+ return false
449
+ }
450
+
451
+ func strconvUnquote(lit *ast.BasicLit) (string, bool) {
452
+ if lit == nil {
453
+ return "", false
454
+ }
455
+ s := lit.Value
456
+ if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
457
+ return s[1 : len(s)-1], true
458
+ }
459
+ return s, false
460
+ }
461
+
462
+ func sortedKeys(m map[string][]string) []string {
463
+ keys := make([]string, 0, len(m))
464
+ for k := range m {
465
+ keys = append(keys, k)
466
+ }
467
+ sort.Strings(keys)
468
+ return keys
469
+ }