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.
- package/README.md +89 -31
- package/dist/extractors/detectLanguage.d.ts +2 -0
- package/dist/extractors/detectLanguage.js +33 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.js +155 -0
- package/dist/extractors/runner.d.ts +7 -0
- package/dist/extractors/runner.js +194 -0
- package/dist/extractors/types.d.ts +37 -0
- package/dist/extractors/types.js +8 -0
- package/dist/graph/buildLookup.d.ts +1 -0
- package/dist/graph/buildLookup.js +2 -1
- package/dist/graph/edges/callEdges.js +19 -5
- package/dist/graph/edges/callEdges.test.d.ts +1 -0
- package/dist/graph/edges/callEdges.test.js +200 -0
- package/dist/graph/edges/importEdges.js +12 -0
- package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
- package/dist/graph/edges/inheritanceEdges.js +73 -0
- package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
- package/dist/graph/edges/inheritanceEdges.test.js +140 -0
- package/dist/graph/index.js +4 -0
- package/dist/parser/classes.test.d.ts +1 -0
- package/dist/parser/classes.test.js +360 -0
- package/dist/parser/extractors/classes.d.ts +5 -0
- package/dist/parser/extractors/classes.js +241 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +7 -1
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.js +30 -42
- package/dist/scoring/index.js +8 -0
- package/dist/scoring/index.test.js +43 -0
- package/dist/scoring/nodeScorer.js +2 -0
- package/dist/scoring/pruneDisconnected.d.ts +8 -0
- package/dist/scoring/pruneDisconnected.js +66 -0
- package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
- package/dist/scoring/pruneDisconnected.test.js +123 -0
- package/dist/summarizer/prompts.d.ts +1 -1
- package/dist/types.d.ts +5 -5
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/build.mjs +43 -0
- package/extractors/go/calls.go +289 -0
- package/extractors/go/contract.go +140 -0
- package/extractors/go/extractor.go +161 -0
- package/extractors/go/fingerprint.go +167 -0
- package/extractors/go/go.mod +3 -0
- package/extractors/go/imports.go +90 -0
- package/extractors/go/inheritance.go +138 -0
- package/extractors/go/lookup.go +106 -0
- package/extractors/go/main.go +57 -0
- package/extractors/go/nodes.go +177 -0
- package/extractors/go/orm_edges.go +277 -0
- package/extractors/go/parser.go +469 -0
- package/extractors/go/routes.go +521 -0
- package/extractors/go/tests.go +45 -0
- package/extractors/go/thirdparty.go +138 -0
- package/extractors/go/typeload.go +178 -0
- package/extractors/go/walker.go +67 -0
- package/extractors/java/build.mjs +90 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/Contract.java +171 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
- package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
- package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
- package/extractors/java/src/devlens/extractor/Main.java +66 -0
- package/extractors/java/src/devlens/extractor/Parser.java +522 -0
- package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
- package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
- package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
- package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
- package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
- package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
- package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
- package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
- package/extractors/python/devlens_extractors_python/__init__.py +3 -0
- package/extractors/python/devlens_extractors_python/__main__.py +33 -0
- package/extractors/python/devlens_extractors_python/contract.py +94 -0
- package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
- package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
- package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
- package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
- package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
- package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
- package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
- package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
- package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
- package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
- package/extractors/python/devlens_extractors_python/extractor.py +107 -0
- package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
- package/extractors/python/devlens_extractors_python/lookup.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
- package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
- package/extractors/python/devlens_extractors_python/third_party.py +103 -0
- package/extractors/python/pyproject.toml +16 -0
- package/extractors/python/setup.mjs +54 -0
- package/extractors/rust/Cargo.toml +29 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/build.mjs +85 -0
- package/extractors/rust/src/calls.rs +295 -0
- package/extractors/rust/src/contract.rs +236 -0
- package/extractors/rust/src/enrich.rs +107 -0
- package/extractors/rust/src/extractor.rs +199 -0
- package/extractors/rust/src/fingerprint.rs +238 -0
- package/extractors/rust/src/imports.rs +101 -0
- package/extractors/rust/src/inheritance.rs +233 -0
- package/extractors/rust/src/lookup.rs +226 -0
- package/extractors/rust/src/main.rs +53 -0
- package/extractors/rust/src/module_map.rs +131 -0
- package/extractors/rust/src/nodes.rs +174 -0
- package/extractors/rust/src/orm_edges.rs +125 -0
- package/extractors/rust/src/parser.rs +859 -0
- package/extractors/rust/src/routes.rs +1265 -0
- package/extractors/rust/src/tests.rs +109 -0
- package/extractors/rust/src/thirdparty.rs +114 -0
- package/extractors/rust/src/walker.rs +82 -0
- 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
|
+
}
|