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,521 @@
1
+ // routes.go — imperative route registration detection: net/http (stdlib +
2
+ // gorilla/mux), Gin, Echo, Fiber. One detector: registration calls are
3
+ // receiver-method + string-literal patterns; the receiver's TYPE (via
4
+ // types.Info) or the file's imports selects the framework.
5
+ //
6
+ // Handlers: named func → HANDLES to FUNCTION node · method value (h.GetUsers)
7
+ // → METHOD node · closure → HANDLES to the ENCLOSING function node
8
+ // (handlerKind="closure") — keeps the "0 broken HANDLES" guarantee.
9
+ //
10
+ // Group prefixes compose (r.Group("/api") → api.GET("/users") = /api/users)
11
+ // and can live across statements; the router map tracks construction-time
12
+ // prefixes per variable (python's APIRouter(prefix=) rule, imperative form).
13
+
14
+ package main
15
+
16
+ import (
17
+ "go/ast"
18
+ "go/types"
19
+ "strconv"
20
+ "strings"
21
+ )
22
+
23
+ // route registration state per function body
24
+ type routeState struct {
25
+ routers map[string]string // var name → framework
26
+ prefixes map[string]string // var name → accumulated prefix
27
+ // var name → enclosing fn node id — `apiHandler := func(c *gin.Context){}`
28
+ // then `r.GET("/x", apiHandler)` — the documented var-closure pattern.
29
+ funcLitVars map[string]string
30
+ info *types.Info
31
+ l *LookupMaps
32
+ // emission collections (state-owned so appends survive — Go slice gotcha)
33
+ routeNodes []map[string]any
34
+ edges []map[string]any
35
+ backendRoutes []map[string]any
36
+ seen map[string]bool // route id → true (dedupe)
37
+ }
38
+
39
+ var httpVerbSet = map[string]bool{
40
+ "GET": true, "POST": true, "PUT": true, "DELETE": true,
41
+ "PATCH": true, "OPTIONS": true, "HEAD": true,
42
+ }
43
+
44
+ // detectRoutes — ROUTE nodes + HANDLES edges + BackendRouteNode entries.
45
+ func detectRoutes(pr *parsedRepo, l *LookupMaps, ti *TypeInfo, fp *Fingerprint) ([]map[string]any, []map[string]any, []map[string]any) {
46
+ routeNodes := []map[string]any{}
47
+ edges := []map[string]any{}
48
+ backendRoutes := []map[string]any{}
49
+
50
+ for _, pf := range pr.files {
51
+ if pf.IsTest {
52
+ continue
53
+ }
54
+ info := ti.Infos[pf.RelPath]
55
+ fwByImport := fileFrameworks(pf)
56
+ for _, fn := range pf.Funcs {
57
+ if fn.IsTest || fn.Decl == nil || fn.Decl.Body == nil {
58
+ continue
59
+ }
60
+ state := &routeState{
61
+ routers: map[string]string{},
62
+ prefixes: map[string]string{},
63
+ funcLitVars: map[string]string{},
64
+ info: info,
65
+ l: l,
66
+ seen: map[string]bool{},
67
+ }
68
+ ast.Inspect(fn.Decl.Body, func(n ast.Node) bool {
69
+ // router creation / group prefix / var-closure tracking
70
+ if assign, ok := n.(*ast.AssignStmt); ok {
71
+ state.trackAssign(assign, fn, pf)
72
+ }
73
+ // route registration
74
+ if call, ok := n.(*ast.CallExpr); ok {
75
+ state.detectRegistration(call, pf, fn, fwByImport, l)
76
+ }
77
+ return true
78
+ })
79
+ routeNodes = append(routeNodes, state.routeNodes...)
80
+ edges = append(edges, state.edges...)
81
+ backendRoutes = append(backendRoutes, state.backendRoutes...)
82
+ }
83
+ }
84
+ return routeNodes, edges, backendRoutes
85
+ }
86
+
87
+ // trackAssign — `r := gin.Default()` / `api := r.Group("/api")` /
88
+ // `apiHandler := func(...)` (var-closure handler).
89
+ func (s *routeState) trackAssign(a *ast.AssignStmt, fn *ParsedFunc, pf *ParsedFile) {
90
+ if len(a.Lhs) != 1 || len(a.Rhs) != 1 {
91
+ return
92
+ }
93
+ lhs, ok := a.Lhs[0].(*ast.Ident)
94
+ if !ok || lhs.Name == "_" {
95
+ return
96
+ }
97
+ // var holding a func literal → handler reference
98
+ if _, ok := a.Rhs[0].(*ast.FuncLit); ok {
99
+ s.funcLitVars[lhs.Name] = nodeIDForFunc(pf.RelPath, fn)
100
+ }
101
+ call, ok := a.Rhs[0].(*ast.CallExpr)
102
+ if !ok {
103
+ return
104
+ }
105
+ sel, ok := call.Fun.(*ast.SelectorExpr)
106
+ if !ok {
107
+ return
108
+ }
109
+ recvName := identName(sel.X)
110
+ method := sel.Sel.Name
111
+
112
+ // root router creation
113
+ switch method {
114
+ case "Default", "New":
115
+ switch {
116
+ case strings.HasPrefix(s.frameworkOf(recvName, sel), "gin") || recvName == "gin":
117
+ s.routers[lhs.Name] = "gin"
118
+ case recvName == "echo":
119
+ s.routers[lhs.Name] = "echo"
120
+ case recvName == "fiber":
121
+ s.routers[lhs.Name] = "fiber"
122
+ case recvName == "http":
123
+ s.routers[lhs.Name] = "net-http"
124
+ case recvName == "mux":
125
+ s.routers[lhs.Name] = "net-http"
126
+ }
127
+ case "NewServeMux", "NewRouter":
128
+ s.routers[lhs.Name] = "net-http"
129
+ case "Group":
130
+ if parentFw, ok := s.routers[recvName]; ok {
131
+ prefix := s.prefixes[recvName] + pathLiteral(call, 0)
132
+ s.routers[lhs.Name] = parentFw
133
+ s.prefixes[lhs.Name] = prefix
134
+ }
135
+ }
136
+ }
137
+
138
+ // detectRegistration — a route registration call (verb method / Handle /
139
+ // HandleFunc / Add). Emits one ROUTE node + HANDLES edge + backend route.
140
+ func (s *routeState) detectRegistration(call *ast.CallExpr, pf *ParsedFile, fn *ParsedFunc,
141
+ fwByImport map[string]bool, l *LookupMaps) {
142
+
143
+ sel, ok := call.Fun.(*ast.SelectorExpr)
144
+ if !ok {
145
+ return
146
+ }
147
+ method := sel.Sel.Name
148
+ recvName := identName(sel.X)
149
+
150
+ // package-level net/http: http.HandleFunc / http.Handle / mux.HandleFunc
151
+ if recvName == "http" || recvName == "mux" {
152
+ if method != "HandleFunc" && method != "Handle" {
153
+ return
154
+ }
155
+ if !fwByImport["net-http"] && !fwByImport["gin"] {
156
+ return
157
+ }
158
+ s.emit(call, "net-http", "GET", 0, pf, fn, l)
159
+ return
160
+ }
161
+
162
+ // receiver-method forms
163
+ var verb string
164
+ var pathIdx int
165
+ switch {
166
+ case httpVerbSet[method]:
167
+ verb, pathIdx = method, 0
168
+ case method == "Any":
169
+ verb, pathIdx = "ANY", 0
170
+ case method == "Handle" || method == "HandleFunc":
171
+ // gin: r.Handle("POST", "/x", h); echo: e.Handle? (echo uses Add);
172
+ // net-http mux: mux.Handle("/x", h) — but that's covered above when
173
+ // receiver is literally http/mux; here it's a mux VARIABLE.
174
+ if s.routers[recvName] == "net-http" {
175
+ verb, pathIdx = "GET", 0
176
+ } else {
177
+ // gin/echo Handle(method, path, handler)
178
+ verb = pathLiteral(call, 0)
179
+ if verb == "" {
180
+ return
181
+ }
182
+ verb = strings.ToUpper(verb)
183
+ pathIdx = 1
184
+ }
185
+ case method == "Add" && s.routers[recvName] == "echo":
186
+ verb = pathLiteral(call, 0)
187
+ if verb == "" {
188
+ return
189
+ }
190
+ verb = strings.ToUpper(verb)
191
+ pathIdx = 1
192
+ default:
193
+ return
194
+ }
195
+
196
+ fw := s.frameworkOf(recvName, sel)
197
+ if fw == "" {
198
+ fw = fwByImportFramework(fwByImport)
199
+ }
200
+ if fw == "" {
201
+ return
202
+ }
203
+ s.emit(call, fw, verb, pathIdx, pf, fn, l)
204
+ }
205
+
206
+ // emit — one route: ROUTE node + HANDLES edge + BackendRouteNode.
207
+ func (s *routeState) emit(call *ast.CallExpr, fw, verb string, pathIdx int,
208
+ pf *ParsedFile, fn *ParsedFunc, l *LookupMaps) {
209
+
210
+ if verb == "" || len(call.Args) <= pathIdx {
211
+ return
212
+ }
213
+ rawPath := pathLiteral(call, pathIdx)
214
+ if rawPath == "" {
215
+ return
216
+ }
217
+ fullPath := s.prefixes[identNameOfReceiver(call)] + rawPath
218
+ if fullPath == "" {
219
+ fullPath = rawPath
220
+ }
221
+ // collapse doubled slashes from prefix+path composition (java incident #3:
222
+ // group "/" + path "/stream" → "//stream" → "/stream")
223
+ for strings.Contains(fullPath, "//") {
224
+ fullPath = strings.ReplaceAll(fullPath, "//", "/")
225
+ }
226
+
227
+ norm, params := normalizePath(fullPath)
228
+ isDynamic := len(params) > 0
229
+
230
+ handlerExpr := lastArg(call, pathIdx)
231
+ handlerID, handlerName, kind := s.resolveHandler(handlerExpr, pf, fn, l)
232
+
233
+ id := pf.RelPath + "::" + verb + " " + norm
234
+ if s.seen[id] {
235
+ return
236
+ }
237
+ s.seen[id] = true
238
+
239
+ meta := map[string]any{
240
+ "urlPath": norm,
241
+ "httpMethod": verb,
242
+ "isDynamic": isDynamic,
243
+ "params": params,
244
+ "framework": fw,
245
+ "handlerName": handlerName,
246
+ "routeKind": "backend",
247
+ "rawPath": rawPath,
248
+ }
249
+ node := map[string]any{
250
+ "id": id,
251
+ "name": verb + " " + norm,
252
+ "type": NodeRoute,
253
+ "filePath": pf.RelPath,
254
+ "startLine": fn.Start,
255
+ "endLine": fn.End,
256
+ "parentFile": "file::" + pf.RelPath,
257
+ "metadata": meta,
258
+ }
259
+ s.routeNodes = append(s.routeNodes, node)
260
+ l.NodeByID[id] = node
261
+
262
+ if handlerID != "" {
263
+ s.edges = append(s.edges, edgeWithMeta(id, handlerID, EdgeHandles, map[string]any{
264
+ "urlPath": norm,
265
+ "httpMethod": verb,
266
+ "framework": fw,
267
+ "handlerKind": kind,
268
+ }))
269
+ // mark the handler node (isHandler enrichment — nodes exist already)
270
+ if hn, ok := l.NodeByID[handlerID]; ok {
271
+ if hm, ok := hn["metadata"].(map[string]any); ok {
272
+ hm["isHandler"] = true
273
+ if hm["handlerKind"] == nil {
274
+ hm["handlerKind"] = kind
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ // BackendRouteNode for the `routes` array
281
+ s.backendRoutes = append(s.backendRoutes, map[string]any{
282
+ "type": "BACKEND_ROUTE",
283
+ "urlPath": norm,
284
+ "filePath": pf.RelPath,
285
+ "httpMethod": verb,
286
+ "framework": fw,
287
+ "isDynamic": isDynamic,
288
+ "params": params,
289
+ "handlerName": handlerName,
290
+ "nodeId": id,
291
+ })
292
+ }
293
+
294
+ // resolveHandler — handler arg → (node id, name, kind).
295
+ func (s *routeState) resolveHandler(expr ast.Expr, pf *ParsedFile, fn *ParsedFunc, l *LookupMaps) (string, string, string) {
296
+ switch h := expr.(type) {
297
+ case *ast.Ident:
298
+ // named function in this package
299
+ if id, ok := l.FuncNodeByPkgName[fn.PkgPath+"::"+h.Name]; ok {
300
+ return id, h.Name, "function"
301
+ }
302
+ // var holding a func literal (apiHandler := func(...))
303
+ if id, ok := s.funcLitVars[h.Name]; ok {
304
+ return id, h.Name, "closure"
305
+ }
306
+ return "", h.Name, "unresolved"
307
+ case *ast.SelectorExpr:
308
+ // method value (h.GetUsers) or package-qualified func (handlers.GetUsers)
309
+ recv := identName(h.X)
310
+ if impPath, ok := s.importAlias(pf, recv); ok {
311
+ if id, ok := l.FuncNodeByPkgName[impPath+"::"+h.Sel.Name]; ok {
312
+ return id, h.Sel.Name, "function"
313
+ }
314
+ if id, ok := l.MethodNodeByPkgTypeName[impPath+"::"+h.Sel.Name]; ok {
315
+ return id, h.Sel.Name, "method"
316
+ }
317
+ return "", h.Sel.Name, "unresolved"
318
+ }
319
+ // receiver var → its type's method (types info)
320
+ if id, class := s.methodForReceiver(h, fn); id != "" {
321
+ return id, h.Sel.Name, "method"
322
+ } else if class != "" {
323
+ return "", h.Sel.Name, "unresolved"
324
+ }
325
+ return "", h.Sel.Name, "unresolved"
326
+ case *ast.FuncLit:
327
+ // closure → enclosing function node
328
+ return nodeIDForFunc(pf.RelPath, fn), "<closure>", "closure"
329
+ case *ast.IndexExpr:
330
+ return s.resolveHandler(h.X, pf, fn, l)
331
+ }
332
+ return "", "<unknown>", "unknown"
333
+ }
334
+
335
+ func (s *routeState) methodForReceiver(h *ast.SelectorExpr, fn *ParsedFunc) (string, string) {
336
+ if s.info == nil {
337
+ return "", ""
338
+ }
339
+ if sel, ok := s.info.Selections[h]; ok {
340
+ if obj, ok := sel.Obj().(*types.Func); ok && obj != nil && obj.Pkg() != nil {
341
+ pkgPath := obj.Pkg().Path()
342
+ if _, local := s.l.PkgNodesByPath[pkgPath]; local {
343
+ typeName := recvTypeNameFromTypes(sel.Recv())
344
+ if typeName == "" {
345
+ return "", ""
346
+ }
347
+ if id, ok := s.l.MethodNodeByPkgTypeName[pkgPath+"::"+typeName+"."+obj.Name()]; ok {
348
+ return id, typeName
349
+ }
350
+ }
351
+ }
352
+ }
353
+ return "", ""
354
+ }
355
+
356
+ func (s *routeState) importAlias(pf *ParsedFile, alias string) (string, bool) {
357
+ for _, imp := range pf.Imports {
358
+ a := imp.Alias
359
+ if a == "" {
360
+ a = lastPathElem(imp.Path)
361
+ }
362
+ if a == alias {
363
+ return imp.Path, true
364
+ }
365
+ }
366
+ return "", false
367
+ }
368
+
369
+ func (s *routeState) frameworkOf(recvName string, sel *ast.SelectorExpr) string {
370
+ // type info first: receiver's package path → framework
371
+ if s.info != nil {
372
+ if ident, ok := sel.X.(*ast.Ident); ok {
373
+ if tv, ok := s.info.Types[ident]; ok && tv.Type != nil {
374
+ if named, ok := tv.Type.(*types.Named); ok && named.Obj() != nil && named.Obj().Pkg() != nil {
375
+ switch named.Obj().Pkg().Path() {
376
+ case "net/http":
377
+ return "net-http"
378
+ case "github.com/gorilla/mux":
379
+ return "net-http"
380
+ case "github.com/gin-gonic/gin":
381
+ return "gin"
382
+ case "github.com/labstack/echo", "github.com/labstack/echo/v4":
383
+ return "echo"
384
+ case "github.com/gofiber/fiber/v2", "github.com/gofiber/fiber":
385
+ return "fiber"
386
+ }
387
+ }
388
+ if ptr, ok := tv.Type.(*types.Pointer); ok {
389
+ if named, ok := ptr.Elem().(*types.Named); ok && named.Obj() != nil && named.Obj().Pkg() != nil {
390
+ switch named.Obj().Pkg().Path() {
391
+ case "net/http":
392
+ return "net-http"
393
+ case "github.com/gorilla/mux":
394
+ return "net-http"
395
+ case "github.com/gin-gonic/gin":
396
+ return "gin"
397
+ case "github.com/labstack/echo", "github.com/labstack/echo/v4":
398
+ return "echo"
399
+ case "github.com/gofiber/fiber/v2", "github.com/gofiber/fiber":
400
+ return "fiber"
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ return s.routers[recvName]
408
+ }
409
+
410
+ func fwByImportFramework(fwByImport map[string]bool) string {
411
+ for _, fw := range []string{"gin", "echo", "fiber", "net-http"} {
412
+ if fwByImport[fw] {
413
+ return fw
414
+ }
415
+ }
416
+ return ""
417
+ }
418
+
419
+ func fileFrameworks(pf *ParsedFile) map[string]bool {
420
+ fws := map[string]bool{}
421
+ for _, imp := range pf.Imports {
422
+ switch {
423
+ case imp.Path == "net/http" || imp.Path == "github.com/gorilla/mux":
424
+ fws["net-http"] = true
425
+ case imp.Path == "github.com/gin-gonic/gin":
426
+ fws["gin"] = true
427
+ case imp.Path == "github.com/labstack/echo" || imp.Path == "github.com/labstack/echo/v4":
428
+ fws["echo"] = true
429
+ case imp.Path == "github.com/gofiber/fiber/v2" || imp.Path == "github.com/gofiber/fiber":
430
+ fws["fiber"] = true
431
+ }
432
+ }
433
+ return fws
434
+ }
435
+
436
+ // ── path helpers ──
437
+
438
+ // pathLiteral — the string literal at args[idx] (or "" if not a literal).
439
+ func pathLiteral(call *ast.CallExpr, idx int) string {
440
+ if idx >= len(call.Args) {
441
+ return ""
442
+ }
443
+ lit, ok := call.Args[idx].(*ast.BasicLit)
444
+ if !ok || lit.Kind.String() != "STRING" {
445
+ return ""
446
+ }
447
+ s, err := strconv.Unquote(lit.Value)
448
+ if err != nil {
449
+ return ""
450
+ }
451
+ return s
452
+ }
453
+
454
+ func lastArg(call *ast.CallExpr, pathIdx int) ast.Expr {
455
+ if len(call.Args) == 0 {
456
+ return nil
457
+ }
458
+ // handler is the arg after the path (verb forms) or the LAST arg
459
+ // (gin/echo Handle with middleware chains)
460
+ if len(call.Args) > pathIdx+1 {
461
+ return call.Args[len(call.Args)-1]
462
+ }
463
+ return nil
464
+ }
465
+
466
+ func identName(e ast.Expr) string {
467
+ switch t := e.(type) {
468
+ case *ast.Ident:
469
+ return t.Name
470
+ case *ast.SelectorExpr:
471
+ return identName(t.X)
472
+ case *ast.StarExpr:
473
+ return identName(t.X)
474
+ case *ast.CallExpr:
475
+ return identName(t.Fun)
476
+ case *ast.ParenExpr:
477
+ return identName(t.X)
478
+ }
479
+ return ""
480
+ }
481
+
482
+ func identNameOfReceiver(call *ast.CallExpr) string {
483
+ if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
484
+ return identName(sel.X)
485
+ }
486
+ return ""
487
+ }
488
+
489
+ // normalizePath — :param / *wildcard → {param} (consistent with python/java
490
+ // route ids); returns (normalized, params).
491
+ func normalizePath(p string) (string, []string) {
492
+ var params []string
493
+ var b strings.Builder
494
+ i := 0
495
+ for i < len(p) {
496
+ c := p[i]
497
+ if c == ':' || c == '*' {
498
+ j := i + 1
499
+ for j < len(p) && (isAlphaNum(p[j]) || p[j] == '_') {
500
+ j++
501
+ }
502
+ name := p[i+1 : j]
503
+ if name == "" {
504
+ b.WriteByte(c)
505
+ i++
506
+ continue
507
+ }
508
+ params = append(params, name)
509
+ b.WriteString("{" + name + "}")
510
+ i = j
511
+ continue
512
+ }
513
+ b.WriteByte(c)
514
+ i++
515
+ }
516
+ return b.String(), params
517
+ }
518
+
519
+ func isAlphaNum(c byte) bool {
520
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
521
+ }
@@ -0,0 +1,45 @@
1
+ // tests.go — TESTS edges: *_test.go (TEST leaf nodes) → the production
2
+ // symbol they test. Go naming convention: TestXxx tests Xxx; TestType_Method
3
+ // tests Type.Method. Never dangling (no edge when the target can't resolve).
4
+
5
+ package main
6
+
7
+ import "strings"
8
+
9
+ func detectTests(pr *parsedRepo, l *LookupMaps) []map[string]any {
10
+ edges := []map[string]any{}
11
+ for _, pf := range pr.files {
12
+ if !pf.IsTest {
13
+ continue
14
+ }
15
+ from := "file::" + pf.RelPath
16
+ pkgPath := pkgPathOf(pr, pf)
17
+ for _, fn := range pf.Funcs {
18
+ if !fn.IsTest {
19
+ continue
20
+ }
21
+ target := testTarget(fn.Name, pkgPath, l)
22
+ if target != "" {
23
+ edges = append(edges, edgeWithMeta(from, target, EdgeTests, map[string]any{"testCase": fn.Name}))
24
+ }
25
+ }
26
+ }
27
+ return edges
28
+ }
29
+
30
+ // testTarget — TestFoo → Foo; TestUser_GetName → User.GetName (method).
31
+ func testTarget(name, pkgPath string, l *LookupMaps) string {
32
+ rest := strings.TrimPrefix(name, "Test")
33
+ if rest == "" || rest == name {
34
+ return ""
35
+ }
36
+ if i := strings.Index(rest, "_"); i > 0 {
37
+ if id, ok := l.MethodNodeByPkgTypeName[pkgPath+"::"+rest[:i]+"."+rest[i+1:]]; ok {
38
+ return id
39
+ }
40
+ }
41
+ if id, ok := l.FuncNodeByPkgName[pkgPath+"::"+rest]; ok {
42
+ return id
43
+ }
44
+ return ""
45
+ }
@@ -0,0 +1,138 @@
1
+ // thirdparty.go — [mod]/... node creation + allowed gating.
2
+ //
3
+ // Mirrors the JS side's [npm]/... convention and python's [pip]/... registry:
4
+ // one package node per import, per-name member nodes for named access, all
5
+ // deduped in a single registry. `allowed` gates inclusion — empty set → ZERO
6
+ // third-party nodes (the engine controls this via options.includeThirdPartyLibs).
7
+
8
+ package main
9
+
10
+ import "strings"
11
+
12
+ // std packages never contain a dot in the first path element (documented
13
+ // go.dev module-path convention: external module paths always do). This is
14
+ // the same rule `go list std` encodes.
15
+ func isStdImport(path string) bool {
16
+ first := path
17
+ if i := strings.Index(path, "/"); i >= 0 {
18
+ first = path[:i]
19
+ }
20
+ return !strings.Contains(first, ".")
21
+ }
22
+
23
+ // runtime/devtool categories (mirrors python's RUNTIME_PACKAGES/DEVTOOL_PACKAGES —
24
+ // frontend uses this for the library panel).
25
+ var runtimePkgs = []string{
26
+ "github.com/gin-gonic/gin", "github.com/labstack/echo", "github.com/gofiber/fiber",
27
+ "gorm.io/gorm", "github.com/jackc/pgx", "github.com/lib/pq",
28
+ "github.com/go-sql-driver/mysql", "go.mongodb.org/mongo-driver",
29
+ "modernc.org/sqlite", "github.com/mattn/go-sqlite3", "github.com/redis/go-redis",
30
+ "github.com/golang-jwt/jwt", "github.com/dgrijalva/jwt-go", "golang.org/x/crypto",
31
+ "github.com/google/uuid", "github.com/gorilla/mux", "github.com/gorilla/handlers",
32
+ "github.com/gorilla/sessions", "github.com/stretchr/testify/assert",
33
+ }
34
+
35
+ var devtoolPkgs = []string{
36
+ "github.com/stretchr/testify/mock", "github.com/stretchr/testify/require",
37
+ "github.com/golang/mock", "github.com/go-cmp/cmp", "github.com/google/go-cmp",
38
+ "github.com/onsi/ginkgo", "github.com/onsi/gomega", "go.uber.org/mock",
39
+ "github.com/vektra/mockery", "github.com/boumenot/gocover-cobertura",
40
+ }
41
+
42
+ type ThirdPartyRegistry struct {
43
+ allowed map[string]bool
44
+ rawDeps map[string]string
45
+ lookup *LookupMaps
46
+ }
47
+
48
+ func newThirdPartyRegistry(allowed []string, fp *Fingerprint, l *LookupMaps) *ThirdPartyRegistry {
49
+ set := map[string]bool{}
50
+ for _, a := range allowed {
51
+ set[a] = true
52
+ }
53
+ return &ThirdPartyRegistry{allowed: set, rawDeps: fp.RawDependencies, lookup: l}
54
+ }
55
+
56
+ func (t *ThirdPartyRegistry) permitted(pkg string) bool { return t.allowed[pkg] }
57
+
58
+ func (t *ThirdPartyRegistry) baseMetadata(pkg string) map[string]any {
59
+ version, ok := t.rawDeps[pkg]
60
+ if !ok {
61
+ version = "unknown"
62
+ }
63
+ category := "unknown"
64
+ for _, r := range runtimePkgs {
65
+ if pkg == r || strings.HasPrefix(pkg, r+"/") {
66
+ category = "runtime"
67
+ break
68
+ }
69
+ }
70
+ if category == "unknown" {
71
+ for _, d := range devtoolPkgs {
72
+ if pkg == d || strings.HasPrefix(pkg, d+"/") {
73
+ category = "devtool"
74
+ break
75
+ }
76
+ }
77
+ }
78
+ return map[string]any{"isThirdParty": true, "packageVersion": version, "category": category}
79
+ }
80
+
81
+ // packageNode — [mod]/pkg node, or nil when not permitted. Cached per id.
82
+ func (t *ThirdPartyRegistry) packageNode(pkg string) map[string]any {
83
+ if !t.permitted(pkg) {
84
+ return nil
85
+ }
86
+ id := thirdPartyID(pkg)
87
+ if n, ok := t.lookup.thirdPartyByID[id]; ok {
88
+ return n
89
+ }
90
+ n := map[string]any{
91
+ "id": id,
92
+ "name": pkg,
93
+ "type": "THIRD_PARTY",
94
+ "filePath": id,
95
+ "startLine": 0,
96
+ "endLine": 0,
97
+ "metadata": t.baseMetadata(pkg),
98
+ }
99
+ t.lookup.thirdPartyByID[id] = n
100
+ t.lookup.thirdPartyNodes = append(t.lookup.thirdPartyNodes, n)
101
+ return n
102
+ }
103
+
104
+ // memberNode — [mod]/pkg::member (lazy member access), or nil when not
105
+ // permitted. The package node always exists first (parent).
106
+ func (t *ThirdPartyRegistry) memberNode(pkg, name string) map[string]any {
107
+ if !t.permitted(pkg) {
108
+ return nil
109
+ }
110
+ pkgNode := t.packageNode(pkg)
111
+ if pkgNode == nil {
112
+ return nil
113
+ }
114
+ id := thirdPartyID(pkg) + "::" + name
115
+ if n, ok := t.lookup.thirdPartyByID[id]; ok {
116
+ return n
117
+ }
118
+ pkgMeta := pkgNode["metadata"].(map[string]any)
119
+ meta := map[string]any{
120
+ "isThirdParty": true,
121
+ "packageVersion": pkgMeta["packageVersion"],
122
+ "category": pkgMeta["category"],
123
+ "parentPackageId": pkgNode["id"],
124
+ "methodName": name,
125
+ }
126
+ n := map[string]any{
127
+ "id": id,
128
+ "name": pkg + "." + name,
129
+ "type": "THIRD_PARTY",
130
+ "filePath": thirdPartyID(pkg),
131
+ "startLine": 0,
132
+ "endLine": 0,
133
+ "metadata": meta,
134
+ }
135
+ t.lookup.thirdPartyByID[id] = n
136
+ t.lookup.thirdPartyNodes = append(t.lookup.thirdPartyNodes, n)
137
+ return n
138
+ }