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