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,167 @@
|
|
|
1
|
+
// fingerprint.go — go.mod parsing → framework / projectType / databases /
|
|
2
|
+
// rawDependencies. Hand-rolled (bufio.Scanner): the binary must stay stdlib-
|
|
3
|
+
// only, so golang.org/x/mod/modfile is out. Never executes the manifest
|
|
4
|
+
// (same security rule as setup.py / pom.xml).
|
|
5
|
+
|
|
6
|
+
package main
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"bufio"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"strings"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// modFile holds everything the extractor needs from go.mod.
|
|
16
|
+
type modFile struct {
|
|
17
|
+
ModulePath string
|
|
18
|
+
GoVersion string
|
|
19
|
+
Requires map[string]string // module path → version
|
|
20
|
+
Replaces map[string]string // old module path → new module path (replace directives)
|
|
21
|
+
HasMod bool
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// parseGoMod reads <repo>/go.mod. Missing file → empty modFile (HasMod=false),
|
|
25
|
+
// which the rest of the extractor treats as "not a Go module" gracefully.
|
|
26
|
+
func parseGoMod(repoPath string) *modFile {
|
|
27
|
+
mf := &modFile{Requires: map[string]string{}, Replaces: map[string]string{}}
|
|
28
|
+
f, err := os.Open(filepath.Join(repoPath, "go.mod"))
|
|
29
|
+
if err != nil {
|
|
30
|
+
return mf
|
|
31
|
+
}
|
|
32
|
+
defer f.Close()
|
|
33
|
+
|
|
34
|
+
sc := bufio.NewScanner(f)
|
|
35
|
+
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
|
|
36
|
+
inRequire := false
|
|
37
|
+
for sc.Scan() {
|
|
38
|
+
line := strings.TrimSpace(stripGoModComment(sc.Text()))
|
|
39
|
+
if line == "" {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if strings.HasPrefix(line, "require (") {
|
|
43
|
+
inRequire = true
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if inRequire {
|
|
47
|
+
if line == ")" {
|
|
48
|
+
inRequire = false
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if mod, ver, ok := splitModuleVersion(line); ok {
|
|
52
|
+
mf.Requires[mod] = ver
|
|
53
|
+
}
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
switch {
|
|
57
|
+
case strings.HasPrefix(line, "module "):
|
|
58
|
+
mf.ModulePath = strings.TrimSpace(strings.TrimPrefix(line, "module "))
|
|
59
|
+
mf.HasMod = true
|
|
60
|
+
case strings.HasPrefix(line, "go "):
|
|
61
|
+
mf.GoVersion = strings.TrimSpace(strings.TrimPrefix(line, "go "))
|
|
62
|
+
case strings.HasPrefix(line, "require "):
|
|
63
|
+
if mod, ver, ok := splitModuleVersion(strings.TrimPrefix(line, "require ")); ok {
|
|
64
|
+
mf.Requires[mod] = ver
|
|
65
|
+
}
|
|
66
|
+
case strings.HasPrefix(line, "replace "):
|
|
67
|
+
// replace old [ver] => new [ver]
|
|
68
|
+
rest := strings.TrimPrefix(line, "replace ")
|
|
69
|
+
if i := strings.Index(rest, "=>"); i >= 0 {
|
|
70
|
+
old := strings.TrimSpace(rest[:i])
|
|
71
|
+
newp := strings.TrimSpace(rest[i+2:])
|
|
72
|
+
// strip versions from the old side: "old v1.2.3" → "old"
|
|
73
|
+
old = strings.Fields(old)[0]
|
|
74
|
+
// new side may be "mod v1.2.3" or a filesystem path
|
|
75
|
+
fields := strings.Fields(newp)
|
|
76
|
+
if len(fields) > 0 {
|
|
77
|
+
mf.Replaces[old] = fields[0]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return mf
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// stripGoModComment removes // comments (not inside strings — go.mod has no
|
|
86
|
+
// string literals in require/module lines, so a plain cut is safe here).
|
|
87
|
+
func stripGoModComment(line string) string {
|
|
88
|
+
if i := strings.Index(line, "//"); i >= 0 {
|
|
89
|
+
return line[:i]
|
|
90
|
+
}
|
|
91
|
+
return line
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// splitModuleVersion splits "mod/path v1.2.3" → (mod/path, v1.2.3).
|
|
95
|
+
func splitModuleVersion(s string) (string, string, bool) {
|
|
96
|
+
fields := strings.Fields(s)
|
|
97
|
+
if len(fields) < 2 {
|
|
98
|
+
return "", "", false
|
|
99
|
+
}
|
|
100
|
+
return fields[0], fields[1], true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── framework / database detection (documented dep-name conventions) ──
|
|
104
|
+
|
|
105
|
+
var frameworkDeps = []struct {
|
|
106
|
+
dep string
|
|
107
|
+
fw string
|
|
108
|
+
}{
|
|
109
|
+
{"github.com/gin-gonic/gin", "gin"},
|
|
110
|
+
{"github.com/labstack/echo", "echo"},
|
|
111
|
+
{"github.com/gofiber/fiber", "fiber"},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var databaseDeps = []struct {
|
|
115
|
+
prefix string
|
|
116
|
+
db string
|
|
117
|
+
}{
|
|
118
|
+
{"gorm.io/gorm", "gorm"},
|
|
119
|
+
{"github.com/jackc/pgx", "postgresql"},
|
|
120
|
+
{"github.com/lib/pq", "postgresql"},
|
|
121
|
+
{"github.com/go-sql-driver/mysql", "mysql"},
|
|
122
|
+
{"modernc.org/sqlite", "sqlite"},
|
|
123
|
+
{"github.com/mattn/go-sqlite3", "sqlite"},
|
|
124
|
+
{"go.mongodb.org/mongo-driver", "mongo"},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// fingerprintFromMod — manifest-only detection. web-framework / database list
|
|
128
|
+
// derived from the require set; net-http and database/sql (both stdlib) are
|
|
129
|
+
// upgraded later by extractor.go once imports are known.
|
|
130
|
+
func fingerprintFromMod(mf *modFile) *Fingerprint {
|
|
131
|
+
fp := newFingerprint()
|
|
132
|
+
if !mf.HasMod {
|
|
133
|
+
return fp
|
|
134
|
+
}
|
|
135
|
+
fp.RawDependencies = sortedCopy(mf.Requires)
|
|
136
|
+
for _, fw := range frameworkDeps {
|
|
137
|
+
if hasPrefixKey(mf.Requires, fw.dep) {
|
|
138
|
+
fp.Framework = fw.fw
|
|
139
|
+
fp.ProjectType = "backend"
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
seen := map[string]bool{}
|
|
144
|
+
for _, dd := range databaseDeps {
|
|
145
|
+
if hasPrefixKey(mf.Requires, dd.prefix) && !seen[dd.db] {
|
|
146
|
+
fp.Databases = append(fp.Databases, dd.db)
|
|
147
|
+
seen[dd.db] = true
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return fp
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// hasPrefixKey — require keys can be "gorm.io/gorm" or "github.com/gofiber/fiber/v2".
|
|
154
|
+
func hasPrefixKey(m map[string]string, prefix string) bool {
|
|
155
|
+
for k := range m {
|
|
156
|
+
if k == prefix || strings.HasPrefix(k, prefix+"/") {
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func sortedCopy(m map[string]string) map[string]string {
|
|
164
|
+
// map ordering doesn't matter for JSON (Go marshals maps sorted), but keep
|
|
165
|
+
// the helper honest.
|
|
166
|
+
return m
|
|
167
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// imports.go — IMPORTS edges + [mod] third-party nodes + the local-package
|
|
2
|
+
// membership index. No PACKAGE nodes (removed 2026-08-17 — engine-wide
|
|
3
|
+
// connected-only pruning makes package stubs obsolete; import edges now
|
|
4
|
+
// target the imported package's FILES, matching JS/Java/Rust).
|
|
5
|
+
//
|
|
6
|
+
// Resolution (documented Go semantics): an import path is internal when it
|
|
7
|
+
// starts with the module path (honoring `replace` directives, including
|
|
8
|
+
// filesystem replaces) — otherwise external → [mod]/... node (gated). std
|
|
9
|
+
// packages (no dot in first path element) → no node, no edge. Go imports
|
|
10
|
+
// PACKAGES, so the IMPORTS edge fans out from the importing FILE to every
|
|
11
|
+
// non-test FILE of the target package (one edge per file; package identity
|
|
12
|
+
// rides in each file's metadata.package).
|
|
13
|
+
|
|
14
|
+
package main
|
|
15
|
+
|
|
16
|
+
import (
|
|
17
|
+
"path/filepath"
|
|
18
|
+
"strings"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// detectImports — returns IMPORTS edges + seeds the shared ThirdPartyRegistry
|
|
22
|
+
// (imports/calls/inheritance share one gated registry per extraction) and the
|
|
23
|
+
// PkgNodesByPath membership index (local-vs-external checks in
|
|
24
|
+
// calls/routes/orm/inheritance — key presence only, values unused).
|
|
25
|
+
func detectImports(pr *parsedRepo, l *LookupMaps, opts *Options, fp *Fingerprint) []map[string]any {
|
|
26
|
+
edges := []map[string]any{}
|
|
27
|
+
|
|
28
|
+
l.tp = newThirdPartyRegistry(opts.IncludeThirdPartyLibs, fp, l)
|
|
29
|
+
|
|
30
|
+
// Index: import path → non-test files of that package (FILE→FILE targets),
|
|
31
|
+
// plus import path → dir for filesystem-replace resolution.
|
|
32
|
+
filesByImportPath := map[string][]string{}
|
|
33
|
+
dirByImportPath := map[string]string{}
|
|
34
|
+
for _, pkg := range pr.packages {
|
|
35
|
+
l.PkgNodesByPath[pkg.ImportPath] = nil // membership marker only
|
|
36
|
+
relFiles := []string{}
|
|
37
|
+
for _, pf := range pkg.Files {
|
|
38
|
+
if pf.IsTest {
|
|
39
|
+
continue // test files are never import targets (leaf rule)
|
|
40
|
+
}
|
|
41
|
+
relFiles = append(relFiles, pf.RelPath)
|
|
42
|
+
}
|
|
43
|
+
filesByImportPath[pkg.ImportPath] = relFiles
|
|
44
|
+
dirByImportPath[pkg.ImportPath] = pkg.Dir
|
|
45
|
+
}
|
|
46
|
+
// replace-directive aliases: import path → local dir (filesystem replaces)
|
|
47
|
+
fsReplaceDir := map[string]string{}
|
|
48
|
+
for old, newp := range pr.mod.Replaces {
|
|
49
|
+
if strings.HasPrefix(newp, "./") || strings.HasPrefix(newp, "../") || strings.HasPrefix(newp, "/") {
|
|
50
|
+
fsReplaceDir[old] = strings.TrimPrefix(filepath.ToSlash(newp), "./")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for _, pf := range pr.files {
|
|
55
|
+
fileID := "file::" + pf.RelPath
|
|
56
|
+
for _, imp := range pf.Imports {
|
|
57
|
+
p := imp.Path
|
|
58
|
+
if isStdImport(p) {
|
|
59
|
+
continue // std: no node, no edge
|
|
60
|
+
}
|
|
61
|
+
// internal? (a) module-prefixed → package files, (b) replace →
|
|
62
|
+
// filesystem dir → that package's files
|
|
63
|
+
targets, ok := filesByImportPath[p]
|
|
64
|
+
if !ok {
|
|
65
|
+
if dir, ok := fsReplaceDir[p]; ok {
|
|
66
|
+
for ip, d := range dirByImportPath {
|
|
67
|
+
if strings.TrimPrefix(filepath.ToSlash(d), "./") == dir {
|
|
68
|
+
targets = filesByImportPath[ip]
|
|
69
|
+
ok = true
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if ok {
|
|
76
|
+
for _, rel := range targets {
|
|
77
|
+
edges = append(edges, edgeWithMeta(fileID, "file::"+rel, EdgeImports,
|
|
78
|
+
map[string]any{"importPath": p}))
|
|
79
|
+
}
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
// external → [mod]/... (gated)
|
|
83
|
+
if n := l.tp.packageNode(p); n != nil {
|
|
84
|
+
edges = append(edges, edgeWithMeta(fileID, n["id"].(string), EdgeImports,
|
|
85
|
+
map[string]any{"importPath": p, "isThirdParty": true}))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return edges
|
|
90
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// inheritance.go — EXTENDS (struct embedding / interface embedding) +
|
|
2
|
+
// IMPLEMENTS (go/types.Implements — the gold standard: exact method-set
|
|
3
|
+
// satisfaction, pointer receivers included, no guessing).
|
|
4
|
+
|
|
5
|
+
package main
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"go/types"
|
|
9
|
+
"strings"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func detectInheritance(pr *parsedRepo, l *LookupMaps, ti *TypeInfo, opts *Options) []map[string]any {
|
|
13
|
+
edges := []map[string]any{}
|
|
14
|
+
|
|
15
|
+
// ── EXTENDS: embedding ──
|
|
16
|
+
for _, pf := range pr.files {
|
|
17
|
+
for _, st := range pf.Structs {
|
|
18
|
+
from := structNodeID(pf.RelPath, st.Name)
|
|
19
|
+
for _, emb := range st.Embedded {
|
|
20
|
+
target := resolveTypeRef(emb, pf, st.PkgPath, l, opts)
|
|
21
|
+
if target != "" {
|
|
22
|
+
edges = append(edges, edgeWithMeta(from, target, EdgeExtends, map[string]any{"embedded": emb}))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for _, it := range pf.Interfaces {
|
|
27
|
+
from := interfaceNodeID(pf.RelPath, it.Name)
|
|
28
|
+
for _, emb := range it.Embedded {
|
|
29
|
+
target := resolveTypeRef(emb, pf, it.PkgPath, l, opts)
|
|
30
|
+
if target != "" {
|
|
31
|
+
edges = append(edges, edgeWithMeta(from, target, EdgeExtends, map[string]any{"embedded": emb}))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── IMPLEMENTS: exact via go/types ──
|
|
38
|
+
if ti != nil {
|
|
39
|
+
for _, pkg := range pr.packages {
|
|
40
|
+
tp, ok := ti.Pkgs[pkg.ImportPath]
|
|
41
|
+
if !ok {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
scope := tp.Scope()
|
|
45
|
+
// local interfaces of this package
|
|
46
|
+
type ifaceInfo struct {
|
|
47
|
+
id string
|
|
48
|
+
typ *types.Interface
|
|
49
|
+
}
|
|
50
|
+
var ifaces []ifaceInfo
|
|
51
|
+
for _, pf := range pkg.Files {
|
|
52
|
+
for _, it := range pf.Interfaces {
|
|
53
|
+
obj := scope.Lookup(it.Name)
|
|
54
|
+
if obj == nil {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
tn, ok := obj.(*types.TypeName)
|
|
58
|
+
if !ok {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if i, ok := tn.Type().Underlying().(*types.Interface); ok {
|
|
62
|
+
ifaces = append(ifaces, ifaceInfo{id: interfaceNodeID(pf.RelPath, it.Name), typ: i})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if len(ifaces) == 0 {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
// every named type in the package (structs + enums; types without
|
|
70
|
+
// nodes can't get edges — documented)
|
|
71
|
+
for _, name := range scope.Names() {
|
|
72
|
+
obj := scope.Lookup(name)
|
|
73
|
+
tn, ok := obj.(*types.TypeName)
|
|
74
|
+
if !ok {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if _, isIface := tn.Type().Underlying().(*types.Interface); isIface {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
id := typeNodeID(l, pkg.ImportPath, name)
|
|
81
|
+
if id == "" {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
for _, ifc := range ifaces {
|
|
85
|
+
if types.Implements(tn.Type(), ifc.typ) || types.Implements(types.NewPointer(tn.Type()), ifc.typ) {
|
|
86
|
+
edges = append(edges, edgeWithMeta(id, ifc.id, EdgeImplements, map[string]any{}))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return edges
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// resolveTypeRef — embedded type text → node id (local struct/interface node,
|
|
96
|
+
// or gated [mod]/pkg::Member for external embeds like gorm.Model).
|
|
97
|
+
func resolveTypeRef(emb string, pf *ParsedFile, pkgPath string, l *LookupMaps, opts *Options) string {
|
|
98
|
+
name := strings.TrimPrefix(emb, "*")
|
|
99
|
+
// qualified: alias.Type
|
|
100
|
+
if i := strings.Index(name, "."); i >= 0 {
|
|
101
|
+
alias := name[:i]
|
|
102
|
+
member := name[i+1:]
|
|
103
|
+
if impPath, ok := l.SymbolMaps[pf.RelPath][alias]; ok {
|
|
104
|
+
if _, local := l.PkgNodesByPath[impPath]; local {
|
|
105
|
+
if id, ok := l.StructNodeByPkgName[impPath+"::"+member]; ok {
|
|
106
|
+
return id
|
|
107
|
+
}
|
|
108
|
+
if id, ok := l.InterfaceNodeByPkgName[impPath+"::"+member]; ok {
|
|
109
|
+
return id
|
|
110
|
+
}
|
|
111
|
+
return ""
|
|
112
|
+
}
|
|
113
|
+
// external (e.g. gorm.Model) → gated [mod] member node
|
|
114
|
+
if n := l.tp.memberNode(impPath, member); n != nil {
|
|
115
|
+
return n["id"].(string)
|
|
116
|
+
}
|
|
117
|
+
return ""
|
|
118
|
+
}
|
|
119
|
+
return ""
|
|
120
|
+
}
|
|
121
|
+
// unqualified: same package
|
|
122
|
+
if id, ok := l.StructNodeByPkgName[pkgPath+"::"+name]; ok {
|
|
123
|
+
return id
|
|
124
|
+
}
|
|
125
|
+
if id, ok := l.InterfaceNodeByPkgName[pkgPath+"::"+name]; ok {
|
|
126
|
+
return id
|
|
127
|
+
}
|
|
128
|
+
return ""
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// typeNodeID — node id for a named type (struct), or "" if the type has no
|
|
132
|
+
// node (plain aliases, iota enums — documented; enums dropped 2026-08-17).
|
|
133
|
+
func typeNodeID(l *LookupMaps, pkgPath, name string) string {
|
|
134
|
+
if id, ok := l.StructNodeByPkgName[pkgPath+"::"+name]; ok {
|
|
135
|
+
return id
|
|
136
|
+
}
|
|
137
|
+
return ""
|
|
138
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// lookup.go — LookupMaps: ONE shared index built once from parsed facts,
|
|
2
|
+
// consumed by every edge detector. Edge resolution is dict lookups only —
|
|
3
|
+
// never re-walks ASTs (playbook rule).
|
|
4
|
+
|
|
5
|
+
package main
|
|
6
|
+
|
|
7
|
+
import "strings"
|
|
8
|
+
|
|
9
|
+
type LookupMaps struct {
|
|
10
|
+
// node id → node map (all emitted nodes register here)
|
|
11
|
+
NodeByID map[string]map[string]any
|
|
12
|
+
// file node per rel path
|
|
13
|
+
FileNodesByPath map[string]map[string]any
|
|
14
|
+
// PACKAGE node per import path
|
|
15
|
+
PkgNodesByPath map[string]map[string]any
|
|
16
|
+
// symbol maps: rel file → import alias → import path (the bridge for
|
|
17
|
+
// Calls/Routes/Tests — mirrors python's symbol_maps)
|
|
18
|
+
SymbolMaps map[string]map[string]string
|
|
19
|
+
// pkgPath::Name → node id (exact per-package resolution; Go type names
|
|
20
|
+
// are unique per package, so no closest_by_path needed)
|
|
21
|
+
FuncNodeByPkgName map[string]string
|
|
22
|
+
MethodNodeByPkgTypeName map[string]string
|
|
23
|
+
StructNodeByPkgName map[string]string
|
|
24
|
+
InterfaceNodeByPkgName map[string]string
|
|
25
|
+
// third-party registry output (gated) — [mod]/... nodes
|
|
26
|
+
thirdPartyNodes []map[string]any
|
|
27
|
+
thirdPartyByID map[string]map[string]any
|
|
28
|
+
// gated [mod] node factory (imports/calls/inheritance share ONE registry)
|
|
29
|
+
tp *ThirdPartyRegistry
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func buildLookupMaps(pr *parsedRepo, ti *TypeInfo) *LookupMaps {
|
|
33
|
+
l := &LookupMaps{
|
|
34
|
+
NodeByID: map[string]map[string]any{},
|
|
35
|
+
FileNodesByPath: map[string]map[string]any{},
|
|
36
|
+
PkgNodesByPath: map[string]map[string]any{},
|
|
37
|
+
SymbolMaps: map[string]map[string]string{},
|
|
38
|
+
FuncNodeByPkgName: map[string]string{},
|
|
39
|
+
MethodNodeByPkgTypeName: map[string]string{},
|
|
40
|
+
StructNodeByPkgName: map[string]string{},
|
|
41
|
+
InterfaceNodeByPkgName: map[string]string{},
|
|
42
|
+
thirdPartyByID: map[string]map[string]any{},
|
|
43
|
+
}
|
|
44
|
+
pkgNameByPath := map[string]string{}
|
|
45
|
+
for _, pkg := range pr.packages {
|
|
46
|
+
pkgNameByPath[pkg.ImportPath] = pkg.Name
|
|
47
|
+
}
|
|
48
|
+
for _, pkg := range pr.packages {
|
|
49
|
+
for _, pf := range pkg.Files {
|
|
50
|
+
// symbol map for this file
|
|
51
|
+
syms := map[string]string{}
|
|
52
|
+
for _, imp := range pf.Imports {
|
|
53
|
+
if imp.Alias == "_" || imp.Alias == "." {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
alias := imp.Alias
|
|
57
|
+
if alias == "" {
|
|
58
|
+
if name, ok := pkgNameByPath[imp.Path]; ok {
|
|
59
|
+
alias = name
|
|
60
|
+
} else {
|
|
61
|
+
alias = lastPathElem(imp.Path)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
syms[alias] = imp.Path
|
|
65
|
+
}
|
|
66
|
+
l.SymbolMaps[pf.RelPath] = syms
|
|
67
|
+
|
|
68
|
+
// code-node id maps
|
|
69
|
+
for _, fn := range pf.Funcs {
|
|
70
|
+
if fn.IsMethod {
|
|
71
|
+
l.MethodNodeByPkgTypeName[fn.PkgPath+"::"+fn.RecvType+"."+fn.Name] = methodNodeID(pf.RelPath, fn)
|
|
72
|
+
} else {
|
|
73
|
+
l.FuncNodeByPkgName[fn.PkgPath+"::"+fn.Name] = funcNodeID(pf.RelPath, fn.Name)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for _, st := range pf.Structs {
|
|
77
|
+
if st.IsEnum {
|
|
78
|
+
continue // iota enums dropped (2026-08-17) — no node, no edge
|
|
79
|
+
}
|
|
80
|
+
id := structNodeID(pf.RelPath, st.Name)
|
|
81
|
+
l.StructNodeByPkgName[st.PkgPath+"::"+st.Name] = id
|
|
82
|
+
}
|
|
83
|
+
for _, it := range pf.Interfaces {
|
|
84
|
+
l.InterfaceNodeByPkgName[it.PkgPath+"::"+it.Name] = interfaceNodeID(pf.RelPath, it.Name)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return l
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── node id schemes (deterministic, file-scoped) ──
|
|
92
|
+
|
|
93
|
+
func funcNodeID(rel, name string) string { return rel + "::" + name }
|
|
94
|
+
func methodNodeID(rel string, fn *ParsedFunc) string {
|
|
95
|
+
return rel + "::" + fn.RecvType + "." + fn.Name
|
|
96
|
+
}
|
|
97
|
+
func structNodeID(rel, name string) string { return rel + "::" + name }
|
|
98
|
+
func interfaceNodeID(rel, name string) string { return rel + "::" + name }
|
|
99
|
+
func thirdPartyID(importPath string) string { return "[mod]/" + importPath }
|
|
100
|
+
|
|
101
|
+
func lastPathElem(p string) string {
|
|
102
|
+
if i := strings.LastIndex(p, "/"); i >= 0 {
|
|
103
|
+
return p[i+1:]
|
|
104
|
+
}
|
|
105
|
+
return p
|
|
106
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// main.go — entry point: stdin JSON {repoPath, options} → stdout JSON
|
|
2
|
+
// ExtractorResult. JSON only on stdout; logs go to stderr.
|
|
3
|
+
|
|
4
|
+
package main
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"os"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// Input mirrors ExtractorInput from src/extractors/types.ts.
|
|
14
|
+
type Input struct {
|
|
15
|
+
RepoPath string `json:"repoPath"`
|
|
16
|
+
Options json.RawMessage `json:"options"`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Options — the engine forwards includeThirdPartyLibs (both spellings are
|
|
20
|
+
// accepted: engine truth + contract.html docs alias).
|
|
21
|
+
type Options struct {
|
|
22
|
+
IncludeThirdPartyLibs []string `json:"includeThirdPartyLibs"`
|
|
23
|
+
IncludedThirdPartyLibs []string `json:"includedThirdPartyLibs"`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func main() {
|
|
27
|
+
raw, err := io.ReadAll(os.Stdin)
|
|
28
|
+
if err != nil {
|
|
29
|
+
fmt.Fprintln(os.Stderr, "error reading stdin:", err)
|
|
30
|
+
os.Exit(1)
|
|
31
|
+
}
|
|
32
|
+
var in Input
|
|
33
|
+
if err := json.Unmarshal(raw, &in); err != nil {
|
|
34
|
+
fmt.Fprintln(os.Stderr, "bad input json:", err)
|
|
35
|
+
os.Exit(1)
|
|
36
|
+
}
|
|
37
|
+
if in.RepoPath == "" {
|
|
38
|
+
fmt.Fprintln(os.Stderr, "repoPath is required")
|
|
39
|
+
os.Exit(1)
|
|
40
|
+
}
|
|
41
|
+
var opts Options
|
|
42
|
+
if len(in.Options) > 0 {
|
|
43
|
+
_ = json.Unmarshal(in.Options, &opts) // malformed options → defaults, non-fatal
|
|
44
|
+
}
|
|
45
|
+
if len(opts.IncludeThirdPartyLibs) == 0 {
|
|
46
|
+
opts.IncludeThirdPartyLibs = opts.IncludedThirdPartyLibs
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
result := runExtractor(in.RepoPath, &opts)
|
|
50
|
+
|
|
51
|
+
out, err := json.Marshal(result)
|
|
52
|
+
if err != nil {
|
|
53
|
+
fmt.Fprintln(os.Stderr, "error marshaling result:", err)
|
|
54
|
+
os.Exit(1)
|
|
55
|
+
}
|
|
56
|
+
os.Stdout.Write(out)
|
|
57
|
+
}
|