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,469 @@
|
|
|
1
|
+
// parser.go — parse-time FACT collection (the only AST pass).
|
|
2
|
+
//
|
|
3
|
+
// Per .go file: package name, imports, functions/methods (Recv nil|non-nil),
|
|
4
|
+
// structs (fields, tags, embedded types), interfaces (method names), iota
|
|
5
|
+
// const blocks (enum pattern), and body CALLS. Everything downstream (edges)
|
|
6
|
+
// resolves from these facts via lookup maps — never re-walks ASTs.
|
|
7
|
+
|
|
8
|
+
package main
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"go/ast"
|
|
12
|
+
"go/parser"
|
|
13
|
+
"go/token"
|
|
14
|
+
"os"
|
|
15
|
+
"path/filepath"
|
|
16
|
+
"sort"
|
|
17
|
+
"strings"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// ── fact types ──
|
|
21
|
+
|
|
22
|
+
type CallSite struct {
|
|
23
|
+
Str string // call string: "f" or "a.b.c" (contract metadata.calls)
|
|
24
|
+
Sel string // method name for selector form ("Find" in db.Find)
|
|
25
|
+
Receiver string // receiver expr text for selector form ("db")
|
|
26
|
+
IsSel bool
|
|
27
|
+
Node ast.Expr // the callee node (Ident or SelectorExpr) — types lookup
|
|
28
|
+
Pos token.Pos
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ParsedFunc struct {
|
|
32
|
+
Name string
|
|
33
|
+
IsMethod bool
|
|
34
|
+
RecvType string // receiver type name (no *), e.g. "User"
|
|
35
|
+
PkgPath string
|
|
36
|
+
Start int
|
|
37
|
+
End int
|
|
38
|
+
RawCode string
|
|
39
|
+
File string
|
|
40
|
+
Calls []CallSite
|
|
41
|
+
IsTest bool
|
|
42
|
+
Decl *ast.FuncDecl // the decl node — routes.go needs it for body walks
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type FieldInfo struct {
|
|
46
|
+
Name string
|
|
47
|
+
Type string
|
|
48
|
+
Tag string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ParsedStruct struct {
|
|
52
|
+
Name string
|
|
53
|
+
PkgPath string
|
|
54
|
+
Fields []FieldInfo
|
|
55
|
+
Embedded []string
|
|
56
|
+
Start int
|
|
57
|
+
End int
|
|
58
|
+
RawCode string
|
|
59
|
+
File string
|
|
60
|
+
IsEnum bool
|
|
61
|
+
Constants []string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type ParsedInterface struct {
|
|
65
|
+
Name string
|
|
66
|
+
PkgPath string
|
|
67
|
+
Methods []string
|
|
68
|
+
Embedded []string // embedded interfaces (no method names in AST field)
|
|
69
|
+
Start int
|
|
70
|
+
End int
|
|
71
|
+
RawCode string
|
|
72
|
+
File string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type ImportSpec struct {
|
|
76
|
+
Path string // import path, unquoted
|
|
77
|
+
Alias string // explicit alias or "" (use package name)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ParsedFile struct {
|
|
81
|
+
RelPath string
|
|
82
|
+
PkgName string
|
|
83
|
+
IsTest bool
|
|
84
|
+
Imports []ImportSpec
|
|
85
|
+
Funcs []*ParsedFunc
|
|
86
|
+
Structs []*ParsedStruct
|
|
87
|
+
Interfaces []*ParsedInterface
|
|
88
|
+
EndLine int
|
|
89
|
+
AST *ast.File // kept for go/types check (typeload.go)
|
|
90
|
+
// iota const blocks on named numeric types (enum pattern, stretch)
|
|
91
|
+
numericEnums []numericEnum
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type ParsedPackage struct {
|
|
95
|
+
ImportPath string // module path + rel dir
|
|
96
|
+
Dir string // rel dir from repo root ("" or "." for root)
|
|
97
|
+
Name string // package name
|
|
98
|
+
Files []*ParsedFile
|
|
99
|
+
fset *token.FileSet // shared per package (positions must share one fset)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type parsedRepo struct {
|
|
103
|
+
files []*ParsedFile // ALL .go files (tests included), walk order
|
|
104
|
+
packages []*ParsedPackage
|
|
105
|
+
fileByPath map[string]*ParsedFile
|
|
106
|
+
mod *modFile
|
|
107
|
+
repoPath string
|
|
108
|
+
skipped int
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type errFn func(file, msg string)
|
|
112
|
+
|
|
113
|
+
// parseRepo — walk → group into packages → parse each file into facts.
|
|
114
|
+
func parseRepo(repoPath string, mf *modFile, addError errFn) *parsedRepo {
|
|
115
|
+
pr := &parsedRepo{fileByPath: map[string]*ParsedFile{}, mod: mf, repoPath: repoPath}
|
|
116
|
+
all := goFiles(repoPath)
|
|
117
|
+
groups := groupByDir(all)
|
|
118
|
+
for _, dir := range sortedKeys(groups) {
|
|
119
|
+
group := groups[dir]
|
|
120
|
+
pkg := &ParsedPackage{
|
|
121
|
+
Dir: dir,
|
|
122
|
+
ImportPath: importPathForDir(mf, dir),
|
|
123
|
+
fset: token.NewFileSet(),
|
|
124
|
+
}
|
|
125
|
+
for _, rel := range group {
|
|
126
|
+
pf := parseFile(repoPath, rel, pkg, addError)
|
|
127
|
+
if pf == nil {
|
|
128
|
+
pr.skipped++
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if pkg.Name == "" {
|
|
132
|
+
pkg.Name = pf.PkgName
|
|
133
|
+
}
|
|
134
|
+
pkg.Files = append(pkg.Files, pf)
|
|
135
|
+
pr.files = append(pr.files, pf)
|
|
136
|
+
pr.fileByPath[rel] = pf
|
|
137
|
+
}
|
|
138
|
+
if len(pkg.Files) > 0 {
|
|
139
|
+
pr.packages = append(pr.packages, pkg)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return pr
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// importPathForDir — module path + rel dir (root → module path itself). For
|
|
146
|
+
// repos without go.mod, the rel dir stands in as the pseudo import path.
|
|
147
|
+
func importPathForDir(mf *modFile, dir string) string {
|
|
148
|
+
base := mf.ModulePath
|
|
149
|
+
if base == "" {
|
|
150
|
+
return dir
|
|
151
|
+
}
|
|
152
|
+
if dir == "." || dir == "" {
|
|
153
|
+
return base
|
|
154
|
+
}
|
|
155
|
+
return base + "/" + strings.TrimPrefix(dir, "./")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// parseFile — one file → facts. nil on parse error (caller counts skipped).
|
|
159
|
+
func parseFile(repoPath, rel string, pkg *ParsedPackage, addError errFn) *ParsedFile {
|
|
160
|
+
abs := filepath.Join(repoPath, rel)
|
|
161
|
+
src, err := os.ReadFile(abs)
|
|
162
|
+
if err != nil {
|
|
163
|
+
addError(rel, "read: "+err.Error())
|
|
164
|
+
return nil
|
|
165
|
+
}
|
|
166
|
+
fset := pkg.fset
|
|
167
|
+
f, err := parser.ParseFile(fset, rel, src, parser.ParseComments)
|
|
168
|
+
if err != nil {
|
|
169
|
+
addError(rel, "parse: "+err.Error())
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
pf := &ParsedFile{
|
|
173
|
+
RelPath: rel,
|
|
174
|
+
PkgName: f.Name.Name,
|
|
175
|
+
IsTest: strings.HasSuffix(rel, "_test.go"),
|
|
176
|
+
AST: f,
|
|
177
|
+
}
|
|
178
|
+
if pf.EndLine = fset.Position(f.End()).Line; pf.EndLine == 0 {
|
|
179
|
+
pf.EndLine = 1
|
|
180
|
+
}
|
|
181
|
+
// imports
|
|
182
|
+
for _, imp := range f.Imports {
|
|
183
|
+
p, _ := strconvUnquote(imp.Path)
|
|
184
|
+
alias := ""
|
|
185
|
+
if imp.Name != nil {
|
|
186
|
+
alias = imp.Name.Name
|
|
187
|
+
}
|
|
188
|
+
pf.Imports = append(pf.Imports, ImportSpec{Path: p, Alias: alias})
|
|
189
|
+
}
|
|
190
|
+
// named numeric types (the iota-enum substrate)
|
|
191
|
+
numericTypes := map[string]bool{}
|
|
192
|
+
// const specs: type → constants
|
|
193
|
+
constBlocks := map[string][]string{}
|
|
194
|
+
|
|
195
|
+
for _, decl := range f.Decls {
|
|
196
|
+
switch d := decl.(type) {
|
|
197
|
+
case *ast.FuncDecl:
|
|
198
|
+
fn := parseFuncDecl(fset, src, rel, pkg, d, pf.IsTest)
|
|
199
|
+
if fn != nil {
|
|
200
|
+
pf.Funcs = append(pf.Funcs, fn)
|
|
201
|
+
}
|
|
202
|
+
case *ast.GenDecl:
|
|
203
|
+
switch d.Tok {
|
|
204
|
+
case token.TYPE:
|
|
205
|
+
for _, spec := range d.Specs {
|
|
206
|
+
ts, ok := spec.(*ast.TypeSpec)
|
|
207
|
+
if !ok {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
start := fset.Position(d.Pos()).Line
|
|
211
|
+
end := fset.Position(d.End()).Line
|
|
212
|
+
raw := string(src[fset.Position(d.Pos()).Offset:fset.Position(d.End()).Offset])
|
|
213
|
+
switch t := ts.Type.(type) {
|
|
214
|
+
case *ast.StructType:
|
|
215
|
+
st := &ParsedStruct{
|
|
216
|
+
Name: ts.Name.Name, PkgPath: pkg.ImportPath,
|
|
217
|
+
Start: start, End: end, RawCode: raw, File: rel,
|
|
218
|
+
}
|
|
219
|
+
for _, field := range t.Fields.List {
|
|
220
|
+
ftype := exprText(field.Type)
|
|
221
|
+
tag := ""
|
|
222
|
+
if field.Tag != nil {
|
|
223
|
+
tag, _ = strconvUnquote(field.Tag)
|
|
224
|
+
}
|
|
225
|
+
if len(field.Names) == 0 {
|
|
226
|
+
// embedded field
|
|
227
|
+
st.Embedded = append(st.Embedded, ftype)
|
|
228
|
+
st.Fields = append(st.Fields, FieldInfo{Name: "", Type: ftype, Tag: tag})
|
|
229
|
+
} else {
|
|
230
|
+
for _, n := range field.Names {
|
|
231
|
+
st.Fields = append(st.Fields, FieldInfo{Name: n.Name, Type: ftype, Tag: tag})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
pf.Structs = append(pf.Structs, st)
|
|
236
|
+
case *ast.InterfaceType:
|
|
237
|
+
it := &ParsedInterface{
|
|
238
|
+
Name: ts.Name.Name, PkgPath: pkg.ImportPath,
|
|
239
|
+
Start: start, End: end, RawCode: raw, File: rel,
|
|
240
|
+
}
|
|
241
|
+
for _, m := range t.Methods.List {
|
|
242
|
+
if len(m.Names) > 0 {
|
|
243
|
+
it.Methods = append(it.Methods, m.Names[0].Name)
|
|
244
|
+
} else {
|
|
245
|
+
// embedded interface
|
|
246
|
+
it.Embedded = append(it.Embedded, exprText(m.Type))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
pf.Interfaces = append(pf.Interfaces, it)
|
|
250
|
+
case *ast.Ident, *ast.StarExpr, *ast.ArrayType, *ast.SelectorExpr:
|
|
251
|
+
// plain named type (type X int / type X = Other) —
|
|
252
|
+
// the enum substrate if numeric
|
|
253
|
+
if isNumericType(t) {
|
|
254
|
+
numericTypes[ts.Name.Name] = true
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
case token.CONST:
|
|
259
|
+
// Go const blocks inherit type + value from the previous spec
|
|
260
|
+
// (`Red Color = iota; Green; Blue`). Two passes: resolve names
|
|
261
|
+
// with their inherited type, then check if the block uses iota.
|
|
262
|
+
type constEntry struct{ name, typeName string }
|
|
263
|
+
var entries []constEntry
|
|
264
|
+
lastType := ""
|
|
265
|
+
blockIota := false
|
|
266
|
+
for _, spec := range d.Specs {
|
|
267
|
+
cs, ok := spec.(*ast.ValueSpec)
|
|
268
|
+
if !ok || len(cs.Names) == 0 {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
if cs.Type != nil {
|
|
272
|
+
lastType = exprText(cs.Type)
|
|
273
|
+
}
|
|
274
|
+
hasIota := false
|
|
275
|
+
ast.Inspect(cs, func(n ast.Node) bool {
|
|
276
|
+
if id, ok := n.(*ast.Ident); ok && id.Name == "iota" {
|
|
277
|
+
hasIota = true
|
|
278
|
+
}
|
|
279
|
+
return true
|
|
280
|
+
})
|
|
281
|
+
if hasIota {
|
|
282
|
+
blockIota = true
|
|
283
|
+
}
|
|
284
|
+
if lastType != "" {
|
|
285
|
+
for _, n := range cs.Names {
|
|
286
|
+
entries = append(entries, constEntry{n.Name, lastType})
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if blockIota {
|
|
291
|
+
for _, e := range entries {
|
|
292
|
+
constBlocks[e.typeName] = append(constBlocks[e.typeName], e.name)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// enum pattern: named numeric type + iota const block → ENUM (stretch)
|
|
299
|
+
for typeName, consts := range constBlocks {
|
|
300
|
+
if numericTypes[typeName] {
|
|
301
|
+
for _, st := range pf.Structs {
|
|
302
|
+
if st.Name == typeName {
|
|
303
|
+
st.IsEnum = true
|
|
304
|
+
st.Constants = consts
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// numeric types aren't Structs — record them on the file for
|
|
308
|
+
// node emission via a synthetic entry
|
|
309
|
+
pf.numericEnums = append(pf.numericEnums, numericEnum{TypeName: typeName, Constants: consts})
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return pf
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type numericEnum struct {
|
|
316
|
+
TypeName string
|
|
317
|
+
Constants []string
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// parseFuncDecl — FuncDecl → facts. Test funcs (TestXxx in *_test.go) keep
|
|
321
|
+
// their name; node emission treats test files as leaves (nodes=[]).
|
|
322
|
+
func parseFuncDecl(fset *token.FileSet, src []byte, rel string, pkg *ParsedPackage, d *ast.FuncDecl, isTestFile bool) *ParsedFunc {
|
|
323
|
+
fn := &ParsedFunc{
|
|
324
|
+
Name: d.Name.Name,
|
|
325
|
+
PkgPath: pkg.ImportPath,
|
|
326
|
+
Start: fset.Position(d.Pos()).Line,
|
|
327
|
+
End: fset.Position(d.End()).Line,
|
|
328
|
+
RawCode: string(src[fset.Position(d.Pos()).Offset:fset.Position(d.End()).Offset]),
|
|
329
|
+
File: rel,
|
|
330
|
+
IsMethod: d.Recv != nil && len(d.Recv.List) > 0,
|
|
331
|
+
Decl: d,
|
|
332
|
+
}
|
|
333
|
+
if fn.IsMethod {
|
|
334
|
+
fn.RecvType = receiverTypeName(d.Recv.List[0].Type)
|
|
335
|
+
}
|
|
336
|
+
if isTestFile && strings.HasPrefix(d.Name.Name, "Test") {
|
|
337
|
+
fn.IsTest = true
|
|
338
|
+
}
|
|
339
|
+
if d.Body != nil {
|
|
340
|
+
fn.Calls = collectCalls(d.Body)
|
|
341
|
+
}
|
|
342
|
+
return fn
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// receiverTypeName — "(u User)" / "(u *User)" / "(u *User[T])" → "User".
|
|
346
|
+
func receiverTypeName(e ast.Expr) string {
|
|
347
|
+
switch t := e.(type) {
|
|
348
|
+
case *ast.StarExpr:
|
|
349
|
+
return receiverTypeName(t.X)
|
|
350
|
+
case *ast.Ident:
|
|
351
|
+
return t.Name
|
|
352
|
+
case *ast.IndexExpr:
|
|
353
|
+
return receiverTypeName(t.X)
|
|
354
|
+
case *ast.IndexListExpr:
|
|
355
|
+
return receiverTypeName(t.X)
|
|
356
|
+
case *ast.ParenExpr:
|
|
357
|
+
return receiverTypeName(t.X)
|
|
358
|
+
}
|
|
359
|
+
return exprText(e)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// collectCalls — all CallExprs in a body (scope rule: closures inside the
|
|
363
|
+
// body attribute to the enclosing function — Go has no nested func decls).
|
|
364
|
+
func collectCalls(body ast.Node) []CallSite {
|
|
365
|
+
var calls []CallSite
|
|
366
|
+
ast.Inspect(body, func(n ast.Node) bool {
|
|
367
|
+
call, ok := n.(*ast.CallExpr)
|
|
368
|
+
if !ok {
|
|
369
|
+
return true
|
|
370
|
+
}
|
|
371
|
+
fun := call.Fun
|
|
372
|
+
// unwrap generic calls: f[T](...)
|
|
373
|
+
if idx, ok := fun.(*ast.IndexExpr); ok {
|
|
374
|
+
fun = idx.X
|
|
375
|
+
} else if idx, ok := fun.(*ast.IndexListExpr); ok {
|
|
376
|
+
fun = idx.X
|
|
377
|
+
}
|
|
378
|
+
switch f := fun.(type) {
|
|
379
|
+
case *ast.Ident:
|
|
380
|
+
calls = append(calls, CallSite{Str: f.Name, Node: f, Pos: call.Pos()})
|
|
381
|
+
case *ast.SelectorExpr:
|
|
382
|
+
calls = append(calls, CallSite{
|
|
383
|
+
Str: exprText(f),
|
|
384
|
+
Sel: f.Sel.Name,
|
|
385
|
+
Receiver: identName(f.X), // root ident survives call chains: DB.Model(...).Updates → "DB"
|
|
386
|
+
IsSel: true,
|
|
387
|
+
Node: f,
|
|
388
|
+
Pos: call.Pos(),
|
|
389
|
+
})
|
|
390
|
+
case *ast.FuncLit:
|
|
391
|
+
// inline anonymous invocation — no symbol to resolve
|
|
392
|
+
}
|
|
393
|
+
return true
|
|
394
|
+
})
|
|
395
|
+
return calls
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// exprText — renders common AST expressions to source text.
|
|
399
|
+
func exprText(e ast.Expr) string {
|
|
400
|
+
switch t := e.(type) {
|
|
401
|
+
case *ast.Ident:
|
|
402
|
+
return t.Name
|
|
403
|
+
case *ast.SelectorExpr:
|
|
404
|
+
return exprText(t.X) + "." + t.Sel.Name
|
|
405
|
+
case *ast.StarExpr:
|
|
406
|
+
return "*" + exprText(t.X)
|
|
407
|
+
case *ast.IndexExpr:
|
|
408
|
+
return exprText(t.X)
|
|
409
|
+
case *ast.IndexListExpr:
|
|
410
|
+
return exprText(t.X)
|
|
411
|
+
case *ast.ParenExpr:
|
|
412
|
+
return exprText(t.X)
|
|
413
|
+
case *ast.BasicLit:
|
|
414
|
+
return t.Value
|
|
415
|
+
case *ast.CallExpr:
|
|
416
|
+
return exprText(t.Fun) // chains render as the callee path: DB.Model(...).Updates → "DB.Model.Updates"
|
|
417
|
+
case *ast.ArrayType:
|
|
418
|
+
if t.Len == nil {
|
|
419
|
+
return "[]" + exprText(t.Elt)
|
|
420
|
+
}
|
|
421
|
+
return "[" + exprText(t.Len) + "]" + exprText(t.Elt)
|
|
422
|
+
case *ast.MapType:
|
|
423
|
+
return "map[" + exprText(t.Key) + "]" + exprText(t.Value)
|
|
424
|
+
case *ast.FuncType:
|
|
425
|
+
return "func"
|
|
426
|
+
case *ast.ChanType:
|
|
427
|
+
return "chan"
|
|
428
|
+
case *ast.InterfaceType:
|
|
429
|
+
return "interface{}"
|
|
430
|
+
case *ast.StructType:
|
|
431
|
+
return "struct{}"
|
|
432
|
+
}
|
|
433
|
+
return "?"
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
func isNumericType(e ast.Expr) bool {
|
|
437
|
+
switch t := e.(type) {
|
|
438
|
+
case *ast.Ident:
|
|
439
|
+
switch t.Name {
|
|
440
|
+
case "int", "int8", "int16", "int32", "int64", "uint", "uint8",
|
|
441
|
+
"uint16", "uint32", "uint64", "uintptr", "float32", "float64",
|
|
442
|
+
"complex64", "complex128", "byte", "rune":
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
case *ast.SelectorExpr:
|
|
446
|
+
return isNumericType(t.X) // e.g. time.Duration — treat as numeric-ish
|
|
447
|
+
}
|
|
448
|
+
return false
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
func strconvUnquote(lit *ast.BasicLit) (string, bool) {
|
|
452
|
+
if lit == nil {
|
|
453
|
+
return "", false
|
|
454
|
+
}
|
|
455
|
+
s := lit.Value
|
|
456
|
+
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
|
457
|
+
return s[1 : len(s)-1], true
|
|
458
|
+
}
|
|
459
|
+
return s, false
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
func sortedKeys(m map[string][]string) []string {
|
|
463
|
+
keys := make([]string, 0, len(m))
|
|
464
|
+
for k := range m {
|
|
465
|
+
keys = append(keys, k)
|
|
466
|
+
}
|
|
467
|
+
sort.Strings(keys)
|
|
468
|
+
return keys
|
|
469
|
+
}
|