devlensio 0.6.2 → 1.0.0

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 +116 -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,177 @@
1
+ // nodes.go — FILE/TEST nodes + code nodes (FUNCTION/METHOD/STRUCT/INTERFACE/
2
+ // ENUM). Test files are LEAF nodes: their test funcs feed metadata.testCases
3
+ // only — never emitted into the graph (JS parity, python bug #7).
4
+
5
+ package main
6
+
7
+ import (
8
+ "unicode"
9
+ "unicode/utf8"
10
+ )
11
+
12
+ // ── file / test nodes ──
13
+
14
+ func collectFileNodes(pr *parsedRepo, l *LookupMaps) []map[string]any {
15
+ nodes := []map[string]any{}
16
+ pkgByFile := map[string]string{}
17
+ for _, pkg := range pr.packages {
18
+ for _, pf := range pkg.Files {
19
+ pkgByFile[pf.RelPath] = pkg.ImportPath
20
+ }
21
+ }
22
+ for _, pf := range pr.files {
23
+ nodeType := NodeFile
24
+ if pf.IsTest {
25
+ nodeType = NodeTest
26
+ }
27
+ n := fileNode(pf.RelPath, pf.EndLine, nodeType)
28
+ meta := n["metadata"].(map[string]any)
29
+ if pf.IsTest {
30
+ meta["testCases"] = testCaseNames(pf)
31
+ } else {
32
+ ids := []string{}
33
+ for _, fn := range pf.Funcs {
34
+ ids = append(ids, nodeIDForFunc(pf.RelPath, fn))
35
+ }
36
+ for _, st := range pf.Structs {
37
+ if st.IsEnum {
38
+ continue // iota enums dropped — no node, no child
39
+ }
40
+ ids = append(ids, structNodeID(pf.RelPath, st.Name))
41
+ }
42
+ for _, it := range pf.Interfaces {
43
+ ids = append(ids, interfaceNodeID(pf.RelPath, it.Name))
44
+ }
45
+ meta["nodeCount"] = len(ids)
46
+ meta["childNodeIds"] = ids
47
+ }
48
+ meta["package"] = pkgByFile[pf.RelPath]
49
+ nodes = append(nodes, n)
50
+ l.FileNodesByPath[pf.RelPath] = n
51
+ l.NodeByID[n["id"].(string)] = n
52
+ }
53
+ return nodes
54
+ }
55
+
56
+ func testCaseNames(pf *ParsedFile) []string {
57
+ var names []string
58
+ for _, fn := range pf.Funcs {
59
+ if fn.IsTest {
60
+ names = append(names, fn.Name)
61
+ }
62
+ }
63
+ return names
64
+ }
65
+
66
+ // ── code nodes ──
67
+
68
+ func collectCodeNodes(pr *parsedRepo, l *LookupMaps, ti *TypeInfo) []map[string]any {
69
+ nodes := []map[string]any{}
70
+ for _, pf := range pr.files {
71
+ if pf.IsTest {
72
+ continue // leaf rule
73
+ }
74
+ for _, fn := range pf.Funcs {
75
+ meta := map[string]any{
76
+ "package": fn.PkgPath,
77
+ "isExported": isExported(fn.Name),
78
+ }
79
+ if fn.IsMethod {
80
+ meta["parentStruct"] = fn.RecvType
81
+ }
82
+ id := nodeIDForFunc(pf.RelPath, fn)
83
+ n := codeNode(id, pf.RelPath, fn.Name, nodeTypeForFunc(fn), fn.Start, fn.End, fn.RawCode, meta)
84
+ nodes = append(nodes, n)
85
+ l.NodeByID[id] = n
86
+ }
87
+ for _, st := range pf.Structs {
88
+ if st.IsEnum {
89
+ continue // iota enums dropped (2026-08-17) — no node, no edge
90
+ }
91
+ meta := map[string]any{
92
+ "package": st.PkgPath,
93
+ "isExported": isExported(st.Name),
94
+ }
95
+ fields := make([]map[string]any, 0, len(st.Fields))
96
+ for _, f := range st.Fields {
97
+ fields = append(fields, map[string]any{"name": f.Name, "type": f.Type, "tag": f.Tag})
98
+ }
99
+ meta["fields"] = fields
100
+ meta["embedded"] = st.Embedded
101
+ id := structNodeID(pf.RelPath, st.Name)
102
+ n := codeNode(id, pf.RelPath, st.Name, NodeStruct, st.Start, st.End, st.RawCode, meta)
103
+ nodes = append(nodes, n)
104
+ l.NodeByID[id] = n
105
+ }
106
+ for _, it := range pf.Interfaces {
107
+ meta := map[string]any{
108
+ "package": it.PkgPath,
109
+ "isExported": isExported(it.Name),
110
+ "methods": it.Methods,
111
+ "embedded": it.Embedded,
112
+ }
113
+ id := interfaceNodeID(pf.RelPath, it.Name)
114
+ n := codeNode(id, pf.RelPath, it.Name, NodeInterface, it.Start, it.End, it.RawCode, meta)
115
+ nodes = append(nodes, n)
116
+ l.NodeByID[id] = n
117
+ }
118
+ }
119
+ return nodes
120
+ }
121
+
122
+ func nodeTypeForFunc(fn *ParsedFunc) string {
123
+ if fn.IsMethod {
124
+ return NodeMethod
125
+ }
126
+ return NodeFunction
127
+ }
128
+
129
+ func isExported(name string) bool {
130
+ r, _ := utf8.DecodeRuneInString(name)
131
+ return unicode.IsUpper(r)
132
+ }
133
+
134
+ // ── fingerprint helpers (net-http / database/sql / main) ──
135
+
136
+ func importsNetHTTP(pr *parsedRepo) bool {
137
+ for _, pf := range pr.files {
138
+ for _, imp := range pf.Imports {
139
+ if imp.Path == "net/http" {
140
+ return true
141
+ }
142
+ }
143
+ }
144
+ return false
145
+ }
146
+
147
+ func importsDatabaseSQL(pr *parsedRepo) bool {
148
+ for _, pf := range pr.files {
149
+ for _, imp := range pf.Imports {
150
+ if imp.Path == "database/sql" {
151
+ return true
152
+ }
153
+ }
154
+ }
155
+ return false
156
+ }
157
+
158
+ func hasMainPackage(pr *parsedRepo) bool {
159
+ for _, pkg := range pr.packages {
160
+ if pkg.Name == "main" {
161
+ return true
162
+ }
163
+ }
164
+ return false
165
+ }
166
+
167
+ // pkgPathOf — package import path for a parsed file (tests.go uses it).
168
+ func pkgPathOf(pr *parsedRepo, pf *ParsedFile) string {
169
+ for _, pkg := range pr.packages {
170
+ for _, f := range pkg.Files {
171
+ if f == pf {
172
+ return pkg.ImportPath
173
+ }
174
+ }
175
+ }
176
+ return ""
177
+ }
@@ -0,0 +1,277 @@
1
+ // orm_edges.go — GORM + database/sql → READS_FROM/WRITES_TO (consumer → store,
2
+ // mirrors stateEdges.ts and python's orm_edges). Go's type info resolves the
3
+ // model argument EXACTLY (Mode A); Mode B falls back to receiver-name
4
+ // heuristics flagged `heuristic: true`.
5
+
6
+ package main
7
+
8
+ import (
9
+ "go/ast"
10
+ "go/token"
11
+ "go/types"
12
+ "strings"
13
+ )
14
+
15
+ // GORM method grammar (documented GORM API).
16
+ var gormReadVerbs = map[string]bool{
17
+ "Find": true, "First": true, "Take": true, "Last": true, "Count": true,
18
+ "Pluck": true, "Scan": true, "Joins": true, "Preload": true, "Where": true,
19
+ "Model": true, "Limit": true, "Offset": true, "Order": true, "Select": true,
20
+ "Group": true, "Having": true, "Distinct": true, "Raw": true,
21
+ "FindInBatches": true,
22
+ }
23
+
24
+ var gormWriteVerbs = map[string]bool{
25
+ "Create": true, "Save": true, "Updates": true, "Update": true,
26
+ "Delete": true, "Exec": true, "CreateInBatches": true,
27
+ }
28
+
29
+ // database/sql: Scan targets the model; Exec takes an optional model.
30
+ var sqlReadVerbs = map[string]bool{"Query": true, "QueryRow": true, "Scan": true}
31
+ var sqlWriteVerbs = map[string]bool{"Exec": true}
32
+
33
+ // common receiver names for the Mode-B heuristic (documented as heuristic).
34
+ var dbReceiverNames = map[string]bool{
35
+ "db": true, "tx": true, "d": true, "conn": true, "pool": true,
36
+ "database": true, "sqlDB": true, "gormDB": true, "store": true, "repo": true,
37
+ }
38
+
39
+ func detectOrmEdges(pr *parsedRepo, l *LookupMaps, ti *TypeInfo, opts *Options) []map[string]any {
40
+ edges := []map[string]any{}
41
+ dbVars := dbVarKinds(pr) // pkgPath → varName → "gorm" | "sql" (from var decls)
42
+
43
+ // ── model detection (GORM): embeds gorm.Model OR has gorm:"..." tags ──
44
+ for _, pf := range pr.files {
45
+ for _, st := range pf.Structs {
46
+ isModel := false
47
+ for _, emb := range st.Embedded {
48
+ if strings.Contains(emb, "gorm.Model") {
49
+ isModel = true
50
+ break
51
+ }
52
+ }
53
+ if !isModel {
54
+ for _, f := range st.Fields {
55
+ if strings.Contains(f.Tag, "gorm:") {
56
+ isModel = true
57
+ break
58
+ }
59
+ }
60
+ }
61
+ if isModel {
62
+ if n, ok := l.NodeByID[structNodeID(pf.RelPath, st.Name)]; ok {
63
+ m := n["metadata"].(map[string]any)
64
+ m["isModel"] = true
65
+ m["modelType"] = "gorm"
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // ── R/W edges: consumer (func) → model STRUCT ──
72
+ for _, pf := range pr.files {
73
+ info := ti.Infos[pf.RelPath]
74
+ for _, fn := range pf.Funcs {
75
+ if fn.IsTest {
76
+ continue
77
+ }
78
+ consumer := nodeIDForFunc(pf.RelPath, fn)
79
+ for _, c := range fn.Calls {
80
+ if !c.IsSel {
81
+ continue
82
+ }
83
+ receiverType, heuristic := dbReceiverType(c, info, dbVars, fn.PkgPath)
84
+ if receiverType == "" {
85
+ continue
86
+ }
87
+ var edgeType string
88
+ switch {
89
+ case gormReadVerbs[c.Sel] || sqlReadVerbs[c.Sel]:
90
+ edgeType = EdgeReadsFrom
91
+ case gormWriteVerbs[c.Sel] || sqlWriteVerbs[c.Sel]:
92
+ edgeType = EdgeWritesTo
93
+ default:
94
+ continue
95
+ }
96
+ modelID := modelArgID(c, info, l, fn)
97
+ if modelID == "" {
98
+ continue
99
+ }
100
+ edges = append(edges, edgeWithMeta(consumer, modelID, edgeType, map[string]any{
101
+ "method": c.Sel,
102
+ "receiverVar": firstSegment(c.Receiver),
103
+ "heuristic": heuristic,
104
+ "modelType": receiverType,
105
+ }))
106
+ }
107
+ }
108
+ }
109
+ return edges
110
+ }
111
+
112
+ // dbReceiverType — "gorm" | "sql" | "" — resolution ladder:
113
+ // 1. types.Info (exact, local types)
114
+ // 2. the receiver's `var` DECLARATION type (e.g. `var DB *gorm.DB` — exact,
115
+ // works for external types the checker can't resolve)
116
+ // 3. receiver-name heuristic (common db var names — flagged heuristic).
117
+ func dbReceiverType(c CallSite, info *types.Info, dbVars map[string]map[string]string, pkgPath string) (string, bool) {
118
+ recvName := firstSegment(c.Receiver)
119
+ if info != nil {
120
+ if selExpr, ok := c.Node.(*ast.SelectorExpr); ok {
121
+ if recvIdent, ok := selExpr.X.(*ast.Ident); ok {
122
+ if tv, ok := info.Types[recvIdent]; ok && tv.Type != nil {
123
+ switch t := deref(tv.Type).(type) {
124
+ case *types.Named:
125
+ if t.Obj() != nil && t.Obj().Pkg() != nil {
126
+ switch t.Obj().Pkg().Path() {
127
+ case "gorm.io/gorm":
128
+ return "gorm", false
129
+ case "database/sql":
130
+ return "sql", false
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ // var declaration scan (exact: the declared type names the package)
139
+ if m, ok := dbVars[pkgPath]; ok {
140
+ if kind, ok := m[recvName]; ok {
141
+ return kind, false
142
+ }
143
+ }
144
+ // name heuristic (Mode B)
145
+ if dbReceiverNames[strings.ToLower(recvName)] {
146
+ return "gorm", true
147
+ }
148
+ return "", false
149
+ }
150
+
151
+ // dbVarKinds — package-level vars whose declared type mentions gorm/sql.
152
+ func dbVarKinds(pr *parsedRepo) map[string]map[string]string {
153
+ out := map[string]map[string]string{}
154
+ for _, pkg := range pr.packages {
155
+ m := map[string]string{}
156
+ for _, pf := range pkg.Files {
157
+ for _, decl := range pf.AST.Decls {
158
+ gd, ok := decl.(*ast.GenDecl)
159
+ if !ok || gd.Tok != token.VAR {
160
+ continue
161
+ }
162
+ for _, spec := range gd.Specs {
163
+ vs, ok := spec.(*ast.ValueSpec)
164
+ if !ok || vs.Type == nil {
165
+ continue
166
+ }
167
+ t := exprText(vs.Type)
168
+ kind := ""
169
+ switch {
170
+ case strings.Contains(t, "gorm"):
171
+ kind = "gorm"
172
+ case strings.Contains(t, "sql"):
173
+ kind = "sql"
174
+ }
175
+ if kind != "" {
176
+ for _, n := range vs.Names {
177
+ m[n.Name] = kind
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ if len(m) > 0 {
184
+ out[pkg.ImportPath] = m
185
+ }
186
+ }
187
+ return out
188
+ }
189
+
190
+ func deref(t types.Type) types.Type {
191
+ for {
192
+ switch tt := t.(type) {
193
+ case *types.Pointer:
194
+ t = tt.Elem()
195
+ case *types.Slice:
196
+ t = tt.Elem()
197
+ default:
198
+ return t
199
+ }
200
+ }
201
+ }
202
+
203
+ // modelArgID — the model struct node from the method's model argument.
204
+ // Handles the documented GORM chain form `DB.Model(&user).Updates(map...)`
205
+ // where the model lives in the inner Model() call.
206
+ func modelArgID(c CallSite, info *types.Info, l *LookupMaps, fn *ParsedFunc) string {
207
+ sel, ok := c.Node.(*ast.SelectorExpr)
208
+ if !ok {
209
+ return ""
210
+ }
211
+ call := selParentCall(c, fn)
212
+ if call == nil || len(call.Args) == 0 {
213
+ return ""
214
+ }
215
+ // direct: arg0 is the model
216
+ if id := typeExprToStructID(call.Args[0], info, l); id != "" {
217
+ return id
218
+ }
219
+ // chain: receiver is a CallExpr — its args may carry the model
220
+ if chain, ok := sel.X.(*ast.CallExpr); ok && len(chain.Args) > 0 {
221
+ if id := typeExprToStructID(chain.Args[0], info, l); id != "" {
222
+ return id
223
+ }
224
+ }
225
+ return ""
226
+ }
227
+
228
+ // selParentCall — the CallExpr containing the selector (we have the callee
229
+ // node; climb to the call itself via the fn's AST). Since CallSite only
230
+ // carries the callee expr, locate the call by walking the function body.
231
+ func selParentCall(c CallSite, fn *ParsedFunc) *ast.CallExpr {
232
+ if fn.Decl == nil || fn.Decl.Body == nil {
233
+ return nil
234
+ }
235
+ var found *ast.CallExpr
236
+ ast.Inspect(fn.Decl.Body, func(n ast.Node) bool {
237
+ if found != nil {
238
+ return false
239
+ }
240
+ call, ok := n.(*ast.CallExpr)
241
+ if !ok {
242
+ return true
243
+ }
244
+ if call.Fun == c.Node {
245
+ found = call
246
+ return false
247
+ }
248
+ return true
249
+ })
250
+ return found
251
+ }
252
+
253
+ // typeExprToStructID — resolve an argument expr to a local struct node id.
254
+ func typeExprToStructID(arg ast.Expr, info *types.Info, l *LookupMaps) string {
255
+ // unwrap &x
256
+ if un, ok := arg.(*ast.UnaryExpr); ok && un.Op.String() == "&" {
257
+ arg = un.X
258
+ }
259
+ if lit, ok := arg.(*ast.CompositeLit); ok {
260
+ arg = lit.Type
261
+ }
262
+ if info != nil {
263
+ if tv, ok := info.Types[arg]; ok && tv.Type != nil {
264
+ if named, ok := deref(tv.Type).(*types.Named); ok && named.Obj() != nil {
265
+ if named.Obj().Pkg() != nil {
266
+ pkgPath := named.Obj().Pkg().Path()
267
+ if _, local := l.PkgNodesByPath[pkgPath]; local {
268
+ if id, ok := l.StructNodeByPkgName[pkgPath+"::"+named.Obj().Name()]; ok {
269
+ return id
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ return ""
277
+ }