devlensio 0.6.1 → 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.
- 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 +116 -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 +36 -48
- 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,289 @@
|
|
|
1
|
+
// calls.go — CALLS resolution ladder.
|
|
2
|
+
//
|
|
3
|
+
// 1. types.Info.Selections[selExpr] → *types.Func → local package → exact
|
|
4
|
+
// METHOD/FUNCTION node; std → skip; external → lazy [mod]/pkg::member
|
|
5
|
+
// (gated), receiver package from the selection's receiver type.
|
|
6
|
+
// 2. types.Info.Uses[ident] → *types.Func → same by name.
|
|
7
|
+
// 3. Name-based fallback (no type info / external receivers): plain name →
|
|
8
|
+
// same-package func; alias.member → symbol maps → local method/func or
|
|
9
|
+
// [mod] lazy member (python's proven chain rule).
|
|
10
|
+
//
|
|
11
|
+
// metadata.calls (sorted unique strings) + metadata.resolvedCalls written on
|
|
12
|
+
// every caller node (contract compliance + LLM context).
|
|
13
|
+
|
|
14
|
+
package main
|
|
15
|
+
|
|
16
|
+
import (
|
|
17
|
+
"go/ast"
|
|
18
|
+
"go/token"
|
|
19
|
+
"go/types"
|
|
20
|
+
"sort"
|
|
21
|
+
"strings"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
type resolvedCall struct {
|
|
25
|
+
Target string `json:"target"`
|
|
26
|
+
Method string `json:"method,omitempty"`
|
|
27
|
+
Class string `json:"class,omitempty"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// detectCalls — CALLS edges for every non-test function/method body.
|
|
31
|
+
func detectCalls(pr *parsedRepo, ti *TypeInfo, l *LookupMaps, opts *Options) []map[string]any {
|
|
32
|
+
edges := []map[string]any{}
|
|
33
|
+
|
|
34
|
+
for _, pf := range pr.files {
|
|
35
|
+
info := ti.Infos[pf.RelPath]
|
|
36
|
+
for _, fn := range pf.Funcs {
|
|
37
|
+
if fn.IsTest {
|
|
38
|
+
continue // test funcs are leaf metadata, not callers
|
|
39
|
+
}
|
|
40
|
+
callerID := nodeIDForFunc(pf.RelPath, fn)
|
|
41
|
+
callStrings := make([]string, 0, len(fn.Calls))
|
|
42
|
+
var details []resolvedCall
|
|
43
|
+
for _, c := range fn.Calls {
|
|
44
|
+
callStrings = append(callStrings, c.Str)
|
|
45
|
+
target, method, class, e := resolveCall(c, fn, pf, info, l)
|
|
46
|
+
if e != nil {
|
|
47
|
+
edges = append(edges, e)
|
|
48
|
+
}
|
|
49
|
+
if target != "" {
|
|
50
|
+
details = append(details, resolvedCall{Target: target, Method: method, Class: class})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// metadata enrichment on the (already created) caller node
|
|
54
|
+
if n, ok := l.NodeByID[callerID]; ok {
|
|
55
|
+
m := n["metadata"].(map[string]any)
|
|
56
|
+
m["calls"] = sortedUnique(callStrings)
|
|
57
|
+
if len(details) > 0 {
|
|
58
|
+
m["resolvedCalls"] = details
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return edges
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// nodeIDForFunc — matches the id scheme in lookup.go.
|
|
67
|
+
func nodeIDForFunc(rel string, fn *ParsedFunc) string {
|
|
68
|
+
if fn.IsMethod {
|
|
69
|
+
return methodNodeID(rel, fn)
|
|
70
|
+
}
|
|
71
|
+
return funcNodeID(rel, fn.Name)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// resolveCall → (target node id, method, class, edge) — edge nil when
|
|
75
|
+
// unresolvable (documented: builtins/locals/dynamic stay metadata-only).
|
|
76
|
+
func resolveCall(c CallSite, fn *ParsedFunc, pf *ParsedFile, info *types.Info,
|
|
77
|
+
l *LookupMaps) (string, string, string, map[string]any) {
|
|
78
|
+
|
|
79
|
+
pkgPath := fn.PkgPath
|
|
80
|
+
callerID := nodeIDForFunc(pf.RelPath, fn)
|
|
81
|
+
|
|
82
|
+
// ── 1. type-info resolution (exact) ──
|
|
83
|
+
if info != nil {
|
|
84
|
+
if selExpr, ok := c.Node.(*ast.SelectorExpr); ok {
|
|
85
|
+
if sel, ok := info.Selections[selExpr]; ok {
|
|
86
|
+
if obj, ok := sel.Obj().(*types.Func); ok && obj != nil {
|
|
87
|
+
if id, class, ok := funcToNode(obj, l); ok {
|
|
88
|
+
return id, c.Sel, class, callEdge(callerID, id, c.Sel, class)
|
|
89
|
+
}
|
|
90
|
+
if obj.Pkg() != nil {
|
|
91
|
+
path := obj.Pkg().Path()
|
|
92
|
+
if !isStdImport(path) {
|
|
93
|
+
if n := l.tp.memberNode(path, c.Sel); n != nil {
|
|
94
|
+
return n["id"].(string), c.Sel, "", callEdge(callerID, n["id"].(string), c.Sel, "")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if idExpr, ok := c.Node.(*ast.Ident); ok {
|
|
101
|
+
if obj, ok := info.Uses[idExpr].(*types.Func); ok && obj != nil {
|
|
102
|
+
if id, class, ok := funcToNode(obj, l); ok {
|
|
103
|
+
return id, "", class, callEdge(callerID, id, "", class)
|
|
104
|
+
}
|
|
105
|
+
if obj.Pkg() != nil {
|
|
106
|
+
path := obj.Pkg().Path()
|
|
107
|
+
if !isStdImport(path) {
|
|
108
|
+
if n := l.tp.memberNode(path, obj.Name()); n != nil {
|
|
109
|
+
return n["id"].(string), obj.Name(), "", callEdge(callerID, n["id"].(string), obj.Name(), "")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 1.5 receiver-type-from-declaration (params / typed vars) ──
|
|
118
|
+
// Handlers receive framework types by signature: func(c *gin.Context) —
|
|
119
|
+
// the checker can't resolve external types, but the DECLARED type can.
|
|
120
|
+
if c.IsSel {
|
|
121
|
+
if impPath, typeName, local := receiverDeclaredType(c, fn, pf, l); typeName != "" {
|
|
122
|
+
if local {
|
|
123
|
+
if id, ok := l.MethodNodeByPkgTypeName[impPath+"::"+typeName+"."+c.Sel]; ok {
|
|
124
|
+
return id, c.Sel, typeName, callEdge(callerID, id, c.Sel, typeName)
|
|
125
|
+
}
|
|
126
|
+
} else if n := l.tp.memberNode(impPath, typeName+"."+c.Sel); n != nil {
|
|
127
|
+
return n["id"].(string), c.Sel, typeName, callEdge(callerID, n["id"].(string), c.Sel, typeName)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── 2. name-based fallback ──
|
|
133
|
+
if c.IsSel {
|
|
134
|
+
// alias.member: receiver is an import alias → local member or [mod]
|
|
135
|
+
alias := firstSegment(c.Receiver)
|
|
136
|
+
if impPath, ok := l.SymbolMaps[pf.RelPath][alias]; ok {
|
|
137
|
+
if _, local := l.PkgNodesByPath[impPath]; local {
|
|
138
|
+
if id, ok := l.MethodNodeByPkgTypeName[impPath+"::"+c.Sel]; ok {
|
|
139
|
+
return id, c.Sel, "", callEdge(callerID, id, c.Sel, "")
|
|
140
|
+
}
|
|
141
|
+
if id, ok := l.FuncNodeByPkgName[impPath+"::"+c.Sel]; ok {
|
|
142
|
+
return id, c.Sel, "", callEdge(callerID, id, c.Sel, "")
|
|
143
|
+
}
|
|
144
|
+
return "", "", "", nil
|
|
145
|
+
}
|
|
146
|
+
if n := l.tp.memberNode(impPath, c.Sel); n != nil {
|
|
147
|
+
return n["id"].(string), c.Sel, "", callEdge(callerID, n["id"].(string), c.Sel, "")
|
|
148
|
+
}
|
|
149
|
+
return "", "", "", nil
|
|
150
|
+
}
|
|
151
|
+
// receiver names a local TYPE (User.Create) → method on that type
|
|
152
|
+
if id, ok := l.MethodNodeByPkgTypeName[pkgPath+"::"+c.Receiver+"."+c.Sel]; ok {
|
|
153
|
+
return id, c.Sel, c.Receiver, callEdge(callerID, id, c.Sel, c.Receiver)
|
|
154
|
+
}
|
|
155
|
+
// receiver is a plain variable whose type we can't see — unresolvable
|
|
156
|
+
// tier (documented; type info covers the exact cases)
|
|
157
|
+
return "", "", "", nil
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// plain name: same package first
|
|
161
|
+
if id, ok := l.FuncNodeByPkgName[pkgPath+"::"+c.Str]; ok {
|
|
162
|
+
return id, "", "", callEdge(callerID, id, "", "")
|
|
163
|
+
}
|
|
164
|
+
return "", "", "", nil
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func callEdge(from, to, method, class string) map[string]any {
|
|
168
|
+
meta := map[string]any{}
|
|
169
|
+
if method != "" {
|
|
170
|
+
meta["method"] = method
|
|
171
|
+
}
|
|
172
|
+
if class != "" {
|
|
173
|
+
meta["class"] = class
|
|
174
|
+
}
|
|
175
|
+
return edgeWithMeta(from, to, EdgeCalls, meta)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// receiverDeclaredType — the receiver's type from its DECLARATION: function
|
|
179
|
+
// parameters first (func(c *gin.Context) — the documented handler signature),
|
|
180
|
+
// then package-level typed vars (var DB *gorm.DB). Returns (importPath,
|
|
181
|
+
// typeName, local) — local=false means external (memberNode candidate).
|
|
182
|
+
func receiverDeclaredType(c CallSite, fn *ParsedFunc, pf *ParsedFile, l *LookupMaps) (string, string, bool) {
|
|
183
|
+
recvName := firstSegment(c.Receiver)
|
|
184
|
+
if recvName == "" {
|
|
185
|
+
return "", "", false
|
|
186
|
+
}
|
|
187
|
+
// 1. function parameters
|
|
188
|
+
if fn.Decl != nil && fn.Decl.Type != nil && fn.Decl.Type.Params != nil {
|
|
189
|
+
for _, field := range fn.Decl.Type.Params.List {
|
|
190
|
+
for _, name := range field.Names {
|
|
191
|
+
if name.Name == recvName {
|
|
192
|
+
return declaredTypeInfo(field.Type, fn.PkgPath, pf, l)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 2. package-level typed vars in this file (var DB *gorm.DB)
|
|
198
|
+
for _, decl := range pf.AST.Decls {
|
|
199
|
+
gd, ok := decl.(*ast.GenDecl)
|
|
200
|
+
if !ok || gd.Tok != token.VAR {
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
for _, spec := range gd.Specs {
|
|
204
|
+
vs, ok := spec.(*ast.ValueSpec)
|
|
205
|
+
if !ok || vs.Type == nil {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
for _, name := range vs.Names {
|
|
209
|
+
if name.Name == recvName {
|
|
210
|
+
return declaredTypeInfo(vs.Type, fn.PkgPath, pf, l)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return "", "", false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// declaredTypeInfo — a declared type expr → (importPath, typeName, local).
|
|
219
|
+
// Ident "User" → local; SelectorExpr "gin.Context" → alias → external path.
|
|
220
|
+
func declaredTypeInfo(expr ast.Expr, pkgPath string, pf *ParsedFile, l *LookupMaps) (string, string, bool) {
|
|
221
|
+
switch t := expr.(type) {
|
|
222
|
+
case *ast.StarExpr:
|
|
223
|
+
return declaredTypeInfo(t.X, pkgPath, pf, l)
|
|
224
|
+
case *ast.Ident:
|
|
225
|
+
return pkgPath, t.Name, true // same package — local type
|
|
226
|
+
case *ast.SelectorExpr:
|
|
227
|
+
alias := identName(t.X)
|
|
228
|
+
if impPath, ok := l.SymbolMaps[pf.RelPath][alias]; ok {
|
|
229
|
+
return impPath, t.Sel.Name, false
|
|
230
|
+
}
|
|
231
|
+
case *ast.IndexExpr:
|
|
232
|
+
return declaredTypeInfo(t.X, pkgPath, pf, l)
|
|
233
|
+
case *ast.IndexListExpr:
|
|
234
|
+
return declaredTypeInfo(t.X, pkgPath, pf, l)
|
|
235
|
+
}
|
|
236
|
+
return "", "", false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// funcToNode — *types.Func declared in a local package → (node id, typeName).
|
|
240
|
+
func funcToNode(obj *types.Func, l *LookupMaps) (string, string, bool) {
|
|
241
|
+
if obj.Pkg() == nil {
|
|
242
|
+
return "", "", false
|
|
243
|
+
}
|
|
244
|
+
pkgPath := obj.Pkg().Path()
|
|
245
|
+
if _, local := l.PkgNodesByPath[pkgPath]; !local {
|
|
246
|
+
return "", "", false
|
|
247
|
+
}
|
|
248
|
+
if recv := obj.Type().(*types.Signature).Recv(); recv != nil {
|
|
249
|
+
typeName := recvTypeNameFromTypes(recv.Type())
|
|
250
|
+
if typeName == "" {
|
|
251
|
+
return "", "", false
|
|
252
|
+
}
|
|
253
|
+
id, ok := l.MethodNodeByPkgTypeName[pkgPath+"::"+typeName+"."+obj.Name()]
|
|
254
|
+
return id, typeName, ok
|
|
255
|
+
}
|
|
256
|
+
id, ok := l.FuncNodeByPkgName[pkgPath+"::"+obj.Name()]
|
|
257
|
+
return id, "", ok
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// recvTypeNameFromTypes — receiver type → simple name (strips * and pkg path).
|
|
261
|
+
func recvTypeNameFromTypes(t types.Type) string {
|
|
262
|
+
switch tt := t.(type) {
|
|
263
|
+
case *types.Pointer:
|
|
264
|
+
return recvTypeNameFromTypes(tt.Elem())
|
|
265
|
+
case *types.Named:
|
|
266
|
+
return tt.Obj().Name()
|
|
267
|
+
}
|
|
268
|
+
return ""
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func firstSegment(s string) string {
|
|
272
|
+
if i := strings.Index(s, "."); i >= 0 {
|
|
273
|
+
return s[:i]
|
|
274
|
+
}
|
|
275
|
+
return s
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
func sortedUnique(list []string) []string {
|
|
279
|
+
seen := map[string]bool{}
|
|
280
|
+
out := []string{}
|
|
281
|
+
for _, s := range list {
|
|
282
|
+
if !seen[s] {
|
|
283
|
+
seen[s] = true
|
|
284
|
+
out = append(out, s)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
sort.Strings(out)
|
|
288
|
+
return out
|
|
289
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// contract.go — the JSON shape DevLens Engine expects.
|
|
2
|
+
//
|
|
3
|
+
// Every key here is camelCase to match the TypeScript interfaces in the
|
|
4
|
+
// devlens-engine (src/types.ts, src/extractors/types.ts). This file is the
|
|
5
|
+
// single source of truth for the output shape; other modules never spell
|
|
6
|
+
// keys by hand.
|
|
7
|
+
|
|
8
|
+
package main
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"crypto/sha256"
|
|
12
|
+
"encoding/hex"
|
|
13
|
+
"path"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// ── Node types (subset of the engine's NodeType union used by Go) ──
|
|
17
|
+
const (
|
|
18
|
+
NodeFile = "FILE"
|
|
19
|
+
NodeFunction = "FUNCTION"
|
|
20
|
+
NodeMethod = "METHOD"
|
|
21
|
+
NodeStruct = "STRUCT"
|
|
22
|
+
NodeInterface = "INTERFACE"
|
|
23
|
+
NodePackage = "PACKAGE"
|
|
24
|
+
NodeTest = "TEST"
|
|
25
|
+
NodeRoute = "ROUTE"
|
|
26
|
+
NodeThirdParty = "THIRD_PARTY"
|
|
27
|
+
NodeEnum = "ENUM"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// ── Edge types (subset of the engine's EdgeType union used by Go) ──
|
|
31
|
+
const (
|
|
32
|
+
EdgeCalls = "CALLS"
|
|
33
|
+
EdgeImports = "IMPORTS"
|
|
34
|
+
EdgeExtends = "EXTENDS"
|
|
35
|
+
EdgeImplements = "IMPLEMENTS"
|
|
36
|
+
EdgeHandles = "HANDLES"
|
|
37
|
+
EdgeTests = "TESTS"
|
|
38
|
+
EdgeReadsFrom = "READS_FROM"
|
|
39
|
+
EdgeWritesTo = "WRITES_TO"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// codeHash = sha256(rawCode) hex-encoded, first 16 chars (contract rule).
|
|
43
|
+
func codeHash(raw string) string {
|
|
44
|
+
sum := sha256.Sum256([]byte(raw))
|
|
45
|
+
return hex.EncodeToString(sum[:])[:16]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Fingerprint ──
|
|
49
|
+
|
|
50
|
+
type Fingerprint struct {
|
|
51
|
+
Language string `json:"language"`
|
|
52
|
+
ProjectType string `json:"projectType"`
|
|
53
|
+
Framework string `json:"framework"`
|
|
54
|
+
Router string `json:"router"`
|
|
55
|
+
StateManagement []string `json:"stateManagement"`
|
|
56
|
+
DataFetching []string `json:"dataFetching"`
|
|
57
|
+
Databases []string `json:"databases"`
|
|
58
|
+
RawDependencies map[string]string `json:"rawDependencies"`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func newFingerprint() *Fingerprint {
|
|
62
|
+
return &Fingerprint{
|
|
63
|
+
Language: "go",
|
|
64
|
+
ProjectType: "unknown",
|
|
65
|
+
Framework: "unknown",
|
|
66
|
+
Router: "none",
|
|
67
|
+
StateManagement: []string{},
|
|
68
|
+
DataFetching: []string{},
|
|
69
|
+
Databases: []string{},
|
|
70
|
+
RawDependencies: map[string]string{},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Stats / Errors / Result ──
|
|
75
|
+
|
|
76
|
+
type Stats struct {
|
|
77
|
+
TotalFiles int `json:"totalFiles"`
|
|
78
|
+
TotalNodes int `json:"totalNodes"`
|
|
79
|
+
SkippedFiles int `json:"skippedFiles"`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type ExtractorError struct {
|
|
83
|
+
File string `json:"file,omitempty"`
|
|
84
|
+
Error string `json:"error"`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type ExtractorResult struct {
|
|
88
|
+
Fingerprint *Fingerprint `json:"fingerprint"`
|
|
89
|
+
Nodes []map[string]any `json:"nodes"`
|
|
90
|
+
Edges []map[string]any `json:"edges"`
|
|
91
|
+
Routes []map[string]any `json:"routes"`
|
|
92
|
+
Stats Stats `json:"stats"`
|
|
93
|
+
Errors []ExtractorError `json:"errors"`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Node / edge builders ──
|
|
97
|
+
|
|
98
|
+
// fileNode — FILE (or TEST) node. Id format: file::rel/path.go (matches
|
|
99
|
+
// src/parser/index.ts). File nodes parent themselves.
|
|
100
|
+
func fileNode(relPath string, endLine int, nodeType string) map[string]any {
|
|
101
|
+
return map[string]any{
|
|
102
|
+
"id": "file::" + relPath,
|
|
103
|
+
"name": path.Base(relPath),
|
|
104
|
+
"type": nodeType,
|
|
105
|
+
"filePath": relPath,
|
|
106
|
+
"startLine": 1,
|
|
107
|
+
"endLine": endLine,
|
|
108
|
+
"parentFile": "file::" + relPath,
|
|
109
|
+
"metadata": map[string]any{
|
|
110
|
+
"nodeCount": 0,
|
|
111
|
+
"childNodeIds": []any{},
|
|
112
|
+
"language": "go",
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// codeNode — FUNCTION/METHOD/STRUCT/INTERFACE/ENUM/PACKAGE/THIRD_PARTY/ROUTE.
|
|
118
|
+
// rawCode on every code node (summarizer input — engine discards after).
|
|
119
|
+
func codeNode(id, relPath, name, nodeType string, startLine, endLine int, rawCode string, metadata map[string]any) map[string]any {
|
|
120
|
+
return map[string]any{
|
|
121
|
+
"id": id,
|
|
122
|
+
"name": name,
|
|
123
|
+
"type": nodeType,
|
|
124
|
+
"filePath": relPath,
|
|
125
|
+
"startLine": startLine,
|
|
126
|
+
"endLine": endLine,
|
|
127
|
+
"parentFile": "file::" + relPath,
|
|
128
|
+
"codeHash": codeHash(rawCode),
|
|
129
|
+
"rawCode": rawCode,
|
|
130
|
+
"metadata": metadata,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func edge(from, to, etype string) map[string]any {
|
|
135
|
+
return map[string]any{"from": from, "to": to, "type": etype}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func edgeWithMeta(from, to, etype string, metadata map[string]any) map[string]any {
|
|
139
|
+
return map[string]any{"from": from, "to": to, "type": etype, "metadata": metadata}
|
|
140
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// extractor.go — pipeline orchestration.
|
|
2
|
+
//
|
|
3
|
+
// Order (mirrors the python/java extractors):
|
|
4
|
+
// walk → parse (facts) → typeload (Mode A/B) → lookup → imports (+PACKAGE
|
|
5
|
+
// nodes) → calls → routes → orm → inheritance → tests → enrich →
|
|
6
|
+
// dedupe → deterministic sort → result.
|
|
7
|
+
// Edge detectors resolve from facts via lookup maps — never re-walk ASTs.
|
|
8
|
+
|
|
9
|
+
package main
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"sort"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func runExtractor(repoPath string, opts *Options) *ExtractorResult {
|
|
16
|
+
if opts == nil {
|
|
17
|
+
opts = &Options{}
|
|
18
|
+
}
|
|
19
|
+
result := &ExtractorResult{
|
|
20
|
+
Fingerprint: newFingerprint(),
|
|
21
|
+
Nodes: []map[string]any{},
|
|
22
|
+
Edges: []map[string]any{},
|
|
23
|
+
Routes: []map[string]any{},
|
|
24
|
+
Errors: []ExtractorError{},
|
|
25
|
+
}
|
|
26
|
+
addError := func(file, msg string) {
|
|
27
|
+
result.Errors = append(result.Errors, ExtractorError{File: file, Error: msg})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── fingerprint (go.mod only; net-http/sql upgrades come after walk) ──
|
|
31
|
+
mf := parseGoMod(repoPath)
|
|
32
|
+
fp := fingerprintFromMod(mf)
|
|
33
|
+
result.Fingerprint = fp
|
|
34
|
+
|
|
35
|
+
// ── walk + parse ──
|
|
36
|
+
parsed := parseRepo(repoPath, mf, addError)
|
|
37
|
+
|
|
38
|
+
// stdlib database/sql + net-http presence upgrade the fingerprint
|
|
39
|
+
// (both are stdlib — invisible in go.mod requires).
|
|
40
|
+
if fp.Framework == "unknown" && importsNetHTTP(parsed) {
|
|
41
|
+
fp.Framework = "net-http"
|
|
42
|
+
fp.ProjectType = "backend"
|
|
43
|
+
}
|
|
44
|
+
if importsDatabaseSQL(parsed) && !contains(fp.Databases, "sql") {
|
|
45
|
+
fp.Databases = append(fp.Databases, "sql")
|
|
46
|
+
}
|
|
47
|
+
if hasMainPackage(parsed) {
|
|
48
|
+
fp.ProjectType = "backend"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── type load (in-process go/types; local + std, external degraded) ──
|
|
52
|
+
ti := loadTypes(parsed, addError)
|
|
53
|
+
|
|
54
|
+
// ── lookup maps — ONE shared index, consumed by all detectors ──
|
|
55
|
+
lookup := buildLookupMaps(parsed, ti)
|
|
56
|
+
|
|
57
|
+
// ── edges, in dependency order ──
|
|
58
|
+
allEdges := []map[string]any{}
|
|
59
|
+
nodes := []map[string]any{}
|
|
60
|
+
routes := []map[string]any{}
|
|
61
|
+
|
|
62
|
+
// file + test nodes first (parents exist before children)
|
|
63
|
+
files := collectFileNodes(parsed, lookup)
|
|
64
|
+
nodes = append(nodes, files...)
|
|
65
|
+
|
|
66
|
+
// IMPORTS edges + [mod] third-party nodes (no PACKAGE nodes — connected-only
|
|
67
|
+
// pruning is engine-side; imports target the package's FILES, JS parity)
|
|
68
|
+
allEdges = append(allEdges, detectImports(parsed, lookup, opts, fp)...)
|
|
69
|
+
|
|
70
|
+
// code nodes (functions/methods/structs/interfaces/enums)
|
|
71
|
+
codeNodes := collectCodeNodes(parsed, lookup, ti)
|
|
72
|
+
nodes = append(nodes, codeNodes...)
|
|
73
|
+
|
|
74
|
+
// CALLS edges
|
|
75
|
+
allEdges = append(allEdges, detectCalls(parsed, ti, lookup, opts)...)
|
|
76
|
+
|
|
77
|
+
// ROUTE nodes + HANDLES edges + BackendRouteNode entries
|
|
78
|
+
routeNodes, handlesEdges, backendRoutes := detectRoutes(parsed, lookup, ti, fp)
|
|
79
|
+
nodes = append(nodes, routeNodes...)
|
|
80
|
+
allEdges = append(allEdges, handlesEdges...)
|
|
81
|
+
routes = append(routes, backendRoutes...)
|
|
82
|
+
|
|
83
|
+
// EXTENDS / IMPLEMENTS
|
|
84
|
+
allEdges = append(allEdges, detectInheritance(parsed, lookup, ti, opts)...)
|
|
85
|
+
|
|
86
|
+
// READS_FROM / WRITES_TO
|
|
87
|
+
allEdges = append(allEdges, detectOrmEdges(parsed, lookup, ti, opts)...)
|
|
88
|
+
|
|
89
|
+
// TESTS edges
|
|
90
|
+
allEdges = append(allEdges, detectTests(parsed, lookup)...)
|
|
91
|
+
|
|
92
|
+
// third-party nodes (gated) — created during imports/calls, collected last
|
|
93
|
+
nodes = append(nodes, lookup.thirdPartyNodes...)
|
|
94
|
+
|
|
95
|
+
// ── dedupe + deterministic sort ──
|
|
96
|
+
allEdges = dedupeEdges(allEdges)
|
|
97
|
+
sort.SliceStable(allEdges, func(i, j int) bool {
|
|
98
|
+
a, b := allEdges[i], allEdges[j]
|
|
99
|
+
return edgeKey(a) < edgeKey(b)
|
|
100
|
+
})
|
|
101
|
+
sort.SliceStable(nodes, func(i, j int) bool {
|
|
102
|
+
return nodeKey(nodes[i]) < nodeKey(nodes[j])
|
|
103
|
+
})
|
|
104
|
+
sort.SliceStable(routes, func(i, j int) bool {
|
|
105
|
+
return routeKey(routes[i]) < routeKey(routes[j])
|
|
106
|
+
})
|
|
107
|
+
// keep errors in insertion order (they're already deterministic: file walk order)
|
|
108
|
+
|
|
109
|
+
result.Nodes = nodes
|
|
110
|
+
result.Edges = allEdges
|
|
111
|
+
result.Routes = routes
|
|
112
|
+
result.Stats = Stats{
|
|
113
|
+
TotalFiles: len(parsed.files),
|
|
114
|
+
TotalNodes: len(nodes),
|
|
115
|
+
SkippedFiles: parsed.skipped,
|
|
116
|
+
}
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── deterministic key helpers ──
|
|
121
|
+
|
|
122
|
+
func nodeKey(n map[string]any) string {
|
|
123
|
+
s, _ := n["id"].(string)
|
|
124
|
+
return s
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func edgeKey(e map[string]any) string {
|
|
128
|
+
from, _ := e["from"].(string)
|
|
129
|
+
to, _ := e["to"].(string)
|
|
130
|
+
t, _ := e["type"].(string)
|
|
131
|
+
return from + "\x00" + t + "\x00" + to
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func routeKey(r map[string]any) string {
|
|
135
|
+
m, _ := r["method"].(string)
|
|
136
|
+
p, _ := r["path"].(string)
|
|
137
|
+
return m + "\x00" + p
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func dedupeEdges(edges []map[string]any) []map[string]any {
|
|
141
|
+
seen := map[string]bool{}
|
|
142
|
+
out := []map[string]any{}
|
|
143
|
+
for _, e := range edges {
|
|
144
|
+
k := edgeKey(e)
|
|
145
|
+
if seen[k] {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
seen[k] = true
|
|
149
|
+
out = append(out, e)
|
|
150
|
+
}
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func contains(list []string, s string) bool {
|
|
155
|
+
for _, v := range list {
|
|
156
|
+
if v == s {
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false
|
|
161
|
+
}
|