devlensio 1.0.3 → 1.1.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/dist/config/backcompat.test.d.ts +1 -0
- package/dist/config/backcompat.test.js +188 -0
- package/dist/config/index.d.ts +5 -3
- package/dist/config/index.js +37 -54
- package/dist/config/providers/file.d.ts +3 -1
- package/dist/config/providers/file.js +47 -53
- package/dist/config/providers/providers.default.js +0 -1
- package/dist/config/providers/request.js +1 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/config/types.js +2 -18
- package/dist/graph/edges/callEdges.js +97 -5
- package/dist/graph/edges/callEdges.test.js +154 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jobs/queue/memory.js +12 -1
- package/dist/parser/extractors/classes.js +23 -4
- package/dist/parser/extractors/functions.d.ts +10 -3
- package/dist/parser/extractors/functions.js +135 -65
- package/dist/parser/extractors/hooks.js +5 -29
- package/dist/parser/extractors/objectMethods.js +3 -4
- package/dist/parser/index.js +11 -6
- package/dist/parser/overloads.d.ts +2 -0
- package/dist/parser/overloads.js +96 -0
- package/dist/parser/overloads.test.d.ts +1 -0
- package/dist/parser/overloads.test.js +175 -0
- package/dist/parser/typeUtils.d.ts +2 -0
- package/dist/parser/typeUtils.js +2 -0
- package/dist/summarizer/providers/index.js +4 -4
- package/dist/summarizer/providers/openai.js +1 -1
- package/dist/types.d.ts +6 -0
- 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/calls.go +134 -2
- package/extractors/go/contract.go +1 -0
- package/extractors/go/exports.go +107 -0
- package/extractors/go/exports_test.go +64 -0
- package/extractors/go/extractor.go +1 -0
- package/extractors/go/lookup.go +63 -7
- package/extractors/go/nodes.go +28 -0
- package/extractors/go/parser.go +51 -8
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
- package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
- package/extractors/java/src/devlens/extractor/Parser.java +39 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
- package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
- package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
- package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
- package/extractors/python/devlens_extractors_python/extractor.py +7 -1
- package/extractors/python/devlens_extractors_python/lookup.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
- package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -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/src/calls.rs +27 -16
- package/extractors/rust/src/contract.rs +2 -0
- package/extractors/rust/src/exports.rs +387 -0
- package/extractors/rust/src/exports_test.rs +105 -0
- package/extractors/rust/src/extractor.rs +2 -0
- package/extractors/rust/src/lookup.rs +60 -0
- package/extractors/rust/src/main.rs +4 -0
- package/extractors/rust/src/nodes.rs +19 -0
- package/extractors/rust/src/parser.rs +66 -1
- package/extractors/rust/src/routes.rs +2 -1
- package/package.json +10 -3
package/extractors/go/lookup.go
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
package main
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import (
|
|
8
|
+
"go/ast"
|
|
9
|
+
"strings"
|
|
10
|
+
)
|
|
8
11
|
|
|
9
12
|
type LookupMaps struct {
|
|
10
13
|
// node id → node map (all emitted nodes register here)
|
|
@@ -22,6 +25,12 @@ type LookupMaps struct {
|
|
|
22
25
|
MethodNodeByPkgTypeName map[string]string
|
|
23
26
|
StructNodeByPkgName map[string]string
|
|
24
27
|
InterfaceNodeByPkgName map[string]string
|
|
28
|
+
// bare-name candidates across packages — same-name funcs/methods on
|
|
29
|
+
// different receivers/types. Go forbids same-name within one package, so
|
|
30
|
+
// ambiguity only arises across packages or unknown receiver types; the
|
|
31
|
+
// CALLS resolver filters these by arity/type before path proximity.
|
|
32
|
+
FuncCandidatesByName map[string][]funcCandidate
|
|
33
|
+
MethodCandidatesByName map[string][]funcCandidate
|
|
25
34
|
// third-party registry output (gated) — [mod]/... nodes
|
|
26
35
|
thirdPartyNodes []map[string]any
|
|
27
36
|
thirdPartyByID map[string]map[string]any
|
|
@@ -39,6 +48,8 @@ func buildLookupMaps(pr *parsedRepo, ti *TypeInfo) *LookupMaps {
|
|
|
39
48
|
MethodNodeByPkgTypeName: map[string]string{},
|
|
40
49
|
StructNodeByPkgName: map[string]string{},
|
|
41
50
|
InterfaceNodeByPkgName: map[string]string{},
|
|
51
|
+
FuncCandidatesByName: map[string][]funcCandidate{},
|
|
52
|
+
MethodCandidatesByName: map[string][]funcCandidate{},
|
|
42
53
|
thirdPartyByID: map[string]map[string]any{},
|
|
43
54
|
}
|
|
44
55
|
pkgNameByPath := map[string]string{}
|
|
@@ -67,10 +78,17 @@ func buildLookupMaps(pr *parsedRepo, ti *TypeInfo) *LookupMaps {
|
|
|
67
78
|
|
|
68
79
|
// code-node id maps
|
|
69
80
|
for _, fn := range pf.Funcs {
|
|
81
|
+
if fn.IsTest {
|
|
82
|
+
continue // test files are leaf nodes — never edge targets
|
|
83
|
+
}
|
|
70
84
|
if fn.IsMethod {
|
|
71
|
-
|
|
85
|
+
id := methodNodeID(pf.RelPath, fn)
|
|
86
|
+
l.MethodNodeByPkgTypeName[fn.PkgPath+"::"+fn.RecvType+"."+fn.Name] = id
|
|
87
|
+
l.MethodCandidatesByName[fn.Name] = append(l.MethodCandidatesByName[fn.Name], newFuncCandidate(id, pf.RelPath, fn))
|
|
72
88
|
} else {
|
|
73
|
-
|
|
89
|
+
id := funcNodeID(pf.RelPath, fn.Name)
|
|
90
|
+
l.FuncNodeByPkgName[fn.PkgPath+"::"+fn.Name] = id
|
|
91
|
+
l.FuncCandidatesByName[fn.Name] = append(l.FuncCandidatesByName[fn.Name], newFuncCandidate(id, pf.RelPath, fn))
|
|
74
92
|
}
|
|
75
93
|
}
|
|
76
94
|
for _, st := range pf.Structs {
|
|
@@ -90,13 +108,51 @@ func buildLookupMaps(pr *parsedRepo, ti *TypeInfo) *LookupMaps {
|
|
|
90
108
|
|
|
91
109
|
// ── node id schemes (deterministic, file-scoped) ──
|
|
92
110
|
|
|
93
|
-
func funcNodeID(rel, name string) string
|
|
111
|
+
func funcNodeID(rel, name string) string { return rel + "::" + name }
|
|
94
112
|
func methodNodeID(rel string, fn *ParsedFunc) string {
|
|
95
113
|
return rel + "::" + fn.RecvType + "." + fn.Name
|
|
96
114
|
}
|
|
97
|
-
func structNodeID(rel, name string) string
|
|
98
|
-
func interfaceNodeID(rel, name string) string
|
|
99
|
-
func thirdPartyID(importPath string) string
|
|
115
|
+
func structNodeID(rel, name string) string { return rel + "::" + name }
|
|
116
|
+
func interfaceNodeID(rel, name string) string { return rel + "::" + name }
|
|
117
|
+
func thirdPartyID(importPath string) string { return "[mod]/" + importPath }
|
|
118
|
+
|
|
119
|
+
// ── bare-name candidates (arity-aware CALLS resolution) ──
|
|
120
|
+
|
|
121
|
+
// funcCandidate — one same-name declaration with its declared parameter
|
|
122
|
+
// shape, captured once at lookup build time (never re-walks ASTs).
|
|
123
|
+
type funcCandidate struct {
|
|
124
|
+
ID string
|
|
125
|
+
RelPath string
|
|
126
|
+
Required int // minimum arg count (the ...T slot accepts zero)
|
|
127
|
+
Variadic bool // last param is ...T — accepts any extra count
|
|
128
|
+
ParamTypes []string // declared param type texts
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func newFuncCandidate(id, relPath string, fn *ParsedFunc) funcCandidate {
|
|
132
|
+
c := funcCandidate{ID: id, RelPath: relPath}
|
|
133
|
+
if fn.Decl == nil || fn.Decl.Type == nil || fn.Decl.Type.Params == nil {
|
|
134
|
+
return c // unknown shape — permissive in the arity filter
|
|
135
|
+
}
|
|
136
|
+
fields := fn.Decl.Type.Params.List
|
|
137
|
+
c.ParamTypes = make([]string, 0, len(fields))
|
|
138
|
+
for i, field := range fields {
|
|
139
|
+
if field.Type == nil {
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
n := len(field.Names)
|
|
143
|
+
if n == 0 {
|
|
144
|
+
n = 1 // unnamed param still occupies one slot
|
|
145
|
+
}
|
|
146
|
+
_, isVariadic := field.Type.(*ast.Ellipsis)
|
|
147
|
+
c.ParamTypes = append(c.ParamTypes, exprText(field.Type))
|
|
148
|
+
if i == len(fields)-1 && isVariadic {
|
|
149
|
+
c.Variadic = true
|
|
150
|
+
break // ...T accepts zero or more — does not raise Required
|
|
151
|
+
}
|
|
152
|
+
c.Required += n
|
|
153
|
+
}
|
|
154
|
+
return c
|
|
155
|
+
}
|
|
100
156
|
|
|
101
157
|
func lastPathElem(p string) string {
|
|
102
158
|
if i := strings.LastIndex(p, "/"); i >= 0 {
|
package/extractors/go/nodes.go
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
package main
|
|
6
6
|
|
|
7
7
|
import (
|
|
8
|
+
"strconv"
|
|
9
|
+
"strings"
|
|
8
10
|
"unicode"
|
|
9
11
|
"unicode/utf8"
|
|
10
12
|
)
|
|
@@ -75,6 +77,7 @@ func collectCodeNodes(pr *parsedRepo, l *LookupMaps, ti *TypeInfo) []map[string]
|
|
|
75
77
|
meta := map[string]any{
|
|
76
78
|
"package": fn.PkgPath,
|
|
77
79
|
"isExported": isExported(fn.Name),
|
|
80
|
+
"callSites": callSitesMeta(fn.Calls),
|
|
78
81
|
}
|
|
79
82
|
if fn.IsMethod {
|
|
80
83
|
meta["parentStruct"] = fn.RecvType
|
|
@@ -126,6 +129,31 @@ func nodeTypeForFunc(fn *ParsedFunc) string {
|
|
|
126
129
|
return NodeFunction
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
// callSitesMeta — structured call-site records from the parse-time facts:
|
|
133
|
+
// {name, argCount, argTypes, hasSpread?}. One record per (name, arity,
|
|
134
|
+
// argTypes) triple; order follows the AST walk (deterministic).
|
|
135
|
+
func callSitesMeta(calls []CallSite) []map[string]any {
|
|
136
|
+
sites := make([]map[string]any, 0, len(calls))
|
|
137
|
+
seen := map[string]bool{}
|
|
138
|
+
for _, c := range calls {
|
|
139
|
+
key := c.Str + "/" + strconv.Itoa(c.ArgCount) + "/" + strings.Join(c.ArgTypes, ",")
|
|
140
|
+
if seen[key] {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
seen[key] = true
|
|
144
|
+
site := map[string]any{
|
|
145
|
+
"name": c.Str,
|
|
146
|
+
"argCount": c.ArgCount,
|
|
147
|
+
"argTypes": c.ArgTypes,
|
|
148
|
+
}
|
|
149
|
+
if c.HasSpread {
|
|
150
|
+
site["hasSpread"] = true
|
|
151
|
+
}
|
|
152
|
+
sites = append(sites, site)
|
|
153
|
+
}
|
|
154
|
+
return sites
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
func isExported(name string) bool {
|
|
130
158
|
r, _ := utf8.DecodeRuneInString(name)
|
|
131
159
|
return unicode.IsUpper(r)
|
package/extractors/go/parser.go
CHANGED
|
@@ -20,12 +20,15 @@ import (
|
|
|
20
20
|
// ── fact types ──
|
|
21
21
|
|
|
22
22
|
type CallSite struct {
|
|
23
|
-
Str
|
|
24
|
-
Sel
|
|
25
|
-
Receiver
|
|
26
|
-
IsSel
|
|
27
|
-
Node
|
|
28
|
-
Pos
|
|
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
|
+
ArgCount int // number of arguments at the call site
|
|
30
|
+
ArgTypes []string // per-arg type tags (Go type vocabulary, "unknown" when not cheaply inferable)
|
|
31
|
+
HasSpread bool // f(xs...) — variadic spread, runtime arity unknowable
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
type ParsedFunc struct {
|
|
@@ -361,6 +364,8 @@ func receiverTypeName(e ast.Expr) string {
|
|
|
361
364
|
|
|
362
365
|
// collectCalls — all CallExprs in a body (scope rule: closures inside the
|
|
363
366
|
// body attribute to the enclosing function — Go has no nested func decls).
|
|
367
|
+
// Each call also records its argument shape (count, cheap type tags, spread)
|
|
368
|
+
// for arity-aware CALLS resolution.
|
|
364
369
|
func collectCalls(body ast.Node) []CallSite {
|
|
365
370
|
var calls []CallSite
|
|
366
371
|
ast.Inspect(body, func(n ast.Node) bool {
|
|
@@ -368,8 +373,14 @@ func collectCalls(body ast.Node) []CallSite {
|
|
|
368
373
|
if !ok {
|
|
369
374
|
return true
|
|
370
375
|
}
|
|
376
|
+
argCount := len(call.Args)
|
|
377
|
+
hasSpread := call.Ellipsis != token.NoPos
|
|
378
|
+
argTypes := make([]string, 0, argCount)
|
|
379
|
+
for _, arg := range call.Args {
|
|
380
|
+
argTypes = append(argTypes, argTypeTag(arg))
|
|
381
|
+
}
|
|
371
382
|
fun := call.Fun
|
|
372
|
-
// unwrap generic calls: f[T](...)
|
|
383
|
+
// unwrap generic calls: f[T](...)
|
|
373
384
|
if idx, ok := fun.(*ast.IndexExpr); ok {
|
|
374
385
|
fun = idx.X
|
|
375
386
|
} else if idx, ok := fun.(*ast.IndexListExpr); ok {
|
|
@@ -377,7 +388,7 @@ func collectCalls(body ast.Node) []CallSite {
|
|
|
377
388
|
}
|
|
378
389
|
switch f := fun.(type) {
|
|
379
390
|
case *ast.Ident:
|
|
380
|
-
calls = append(calls, CallSite{Str: f.Name, Node: f, Pos: call.Pos()})
|
|
391
|
+
calls = append(calls, CallSite{Str: f.Name, Node: f, Pos: call.Pos(), ArgCount: argCount, ArgTypes: argTypes, HasSpread: hasSpread})
|
|
381
392
|
case *ast.SelectorExpr:
|
|
382
393
|
calls = append(calls, CallSite{
|
|
383
394
|
Str: exprText(f),
|
|
@@ -386,6 +397,7 @@ func collectCalls(body ast.Node) []CallSite {
|
|
|
386
397
|
IsSel: true,
|
|
387
398
|
Node: f,
|
|
388
399
|
Pos: call.Pos(),
|
|
400
|
+
ArgCount: argCount, ArgTypes: argTypes, HasSpread: hasSpread,
|
|
389
401
|
})
|
|
390
402
|
case *ast.FuncLit:
|
|
391
403
|
// inline anonymous invocation — no symbol to resolve
|
|
@@ -395,6 +407,37 @@ func collectCalls(body ast.Node) []CallSite {
|
|
|
395
407
|
return calls
|
|
396
408
|
}
|
|
397
409
|
|
|
410
|
+
// argTypeTag — cheap per-argument type tag in Go's own vocabulary: literals
|
|
411
|
+
// and composite literals carry their type, everything else (idents, calls,
|
|
412
|
+
// expressions) degrades to "unknown". No go/types dependency at parse time.
|
|
413
|
+
func argTypeTag(arg ast.Expr) string {
|
|
414
|
+
switch t := arg.(type) {
|
|
415
|
+
case *ast.BasicLit:
|
|
416
|
+
switch t.Kind {
|
|
417
|
+
case token.STRING:
|
|
418
|
+
return "string"
|
|
419
|
+
case token.INT:
|
|
420
|
+
return "int"
|
|
421
|
+
case token.FLOAT:
|
|
422
|
+
return "float64"
|
|
423
|
+
case token.CHAR:
|
|
424
|
+
return "rune"
|
|
425
|
+
}
|
|
426
|
+
case *ast.Ident:
|
|
427
|
+
if t.Name == "true" || t.Name == "false" {
|
|
428
|
+
return "bool"
|
|
429
|
+
}
|
|
430
|
+
case *ast.CompositeLit:
|
|
431
|
+
if t.Type != nil {
|
|
432
|
+
return exprText(t.Type)
|
|
433
|
+
}
|
|
434
|
+
return "unknown"
|
|
435
|
+
case *ast.FuncLit:
|
|
436
|
+
return "func"
|
|
437
|
+
}
|
|
438
|
+
return "unknown"
|
|
439
|
+
}
|
|
440
|
+
|
|
398
441
|
// exprText — renders common AST expressions to source text.
|
|
399
442
|
func exprText(e ast.Expr) string {
|
|
400
443
|
switch t := e.(type) {
|
|
Binary file
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package devlens.extractor;
|
|
2
|
+
|
|
3
|
+
import java.util.ArrayList;
|
|
4
|
+
import java.util.LinkedHashMap;
|
|
5
|
+
import java.util.List;
|
|
6
|
+
import java.util.Map;
|
|
7
|
+
import java.util.TreeMap;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Exports Map — the flattened public surface of a Java repo, shipped in the
|
|
11
|
+
* ExtractorResult for cross-repo linking (docs/EXPORTS_MAP.txt).
|
|
12
|
+
*
|
|
13
|
+
* Java-specific concerns:
|
|
14
|
+
* - Java has no re-export chains: the importable surface of a package is
|
|
15
|
+
* exactly its PUBLIC top-level (and public nested) types. The map key is
|
|
16
|
+
* the FQCN the importer writes (`import com.foo.Bar;`), under the single
|
|
17
|
+
* subpath "." (Java packages are not subpaths; the FQCN is the door).
|
|
18
|
+
* - Public methods of public types are emitted as "FQCN.method" keys —
|
|
19
|
+
* what consumer CALLS edges resolve against. Overload groups surface as
|
|
20
|
+
* multi-element arrays (ids carry the #arity/#sig8 suffixes from
|
|
21
|
+
* Overloads.disambiguate).
|
|
22
|
+
* - Nested types are included only when EVERY enclosing type is public
|
|
23
|
+
* (Outer must be importable for Outer.Inner to be reachable).
|
|
24
|
+
* - Package-private types are excluded — not importable cross-package.
|
|
25
|
+
* - Java rejects duplicate FQCNs at compile time, so ambiguousNames is a
|
|
26
|
+
* safety net that should stay empty.
|
|
27
|
+
* - Values are ALWAYS arrays of proper nodeIds as they appear in the nodes
|
|
28
|
+
* array (built AFTER Overloads.disambiguate).
|
|
29
|
+
*/
|
|
30
|
+
public final class ExportsMapBuilder {
|
|
31
|
+
|
|
32
|
+
private ExportsMapBuilder() {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static Map<String, Object> build(List<Parser.ParsedFile> parsedFiles, List<Map<String, Object>> nodes) {
|
|
36
|
+
Map<String, List<String>> surface = new TreeMap<>();
|
|
37
|
+
List<String> ambiguous = new ArrayList<>();
|
|
38
|
+
|
|
39
|
+
for (Parser.ParsedFile pf : parsedFiles) {
|
|
40
|
+
if (pf.isTest || pf.types.isEmpty()) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (Parser.TypeInfo t : pf.types) {
|
|
44
|
+
if (!t.isPublic) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
addType(pf, t, null, nodes, surface, ambiguous);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (surface.isEmpty()) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Map<String, Object> out = new LinkedHashMap<>();
|
|
56
|
+
Map<String, Object> exports = new LinkedHashMap<>();
|
|
57
|
+
exports.put(".", surface);
|
|
58
|
+
out.put("exports", exports);
|
|
59
|
+
Map<String, Object> ambiguousNames = new LinkedHashMap<>();
|
|
60
|
+
if (!ambiguous.isEmpty()) {
|
|
61
|
+
ambiguousNames.put(".", ambiguous);
|
|
62
|
+
}
|
|
63
|
+
out.put("ambiguousNames", ambiguousNames);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static void addType(Parser.ParsedFile pf, Parser.TypeInfo t, String parentFqcn,
|
|
68
|
+
List<Map<String, Object>> nodes,
|
|
69
|
+
Map<String, List<String>> surface, List<String> ambiguous) {
|
|
70
|
+
String fqcn = pf.packageName.isEmpty()
|
|
71
|
+
? t.dottedName
|
|
72
|
+
: pf.packageName + "." + t.dottedName;
|
|
73
|
+
String id = pf.relPath + "::" + t.dottedName;
|
|
74
|
+
if (containsNode(nodes, id)) {
|
|
75
|
+
addEntry(surface, ambiguous, fqcn, List.of(id));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
String childFqcn = fqcn;
|
|
79
|
+
for (Parser.MethodInfo m : t.methods) {
|
|
80
|
+
if (!m.isPublic || m.isConstructor) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
List<String> ids = methodIds(nodes, pf, t, m.name);
|
|
84
|
+
if (!ids.isEmpty()) {
|
|
85
|
+
addEntry(surface, ambiguous, childFqcn + "." + m.name, ids);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (Parser.TypeInfo nested : t.nested) {
|
|
90
|
+
if (nested.isPublic) {
|
|
91
|
+
addType(pf, nested, childFqcn, nodes, surface, ambiguous);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private static void addEntry(Map<String, List<String>> surface, List<String> ambiguous,
|
|
97
|
+
String key, List<String> ids) {
|
|
98
|
+
List<String> sorted = new ArrayList<>(ids);
|
|
99
|
+
java.util.Collections.sort(sorted);
|
|
100
|
+
List<String> existing = surface.get(key);
|
|
101
|
+
if (existing == null) {
|
|
102
|
+
surface.put(key, sorted);
|
|
103
|
+
} else if (!existing.equals(sorted)) {
|
|
104
|
+
ambiguous.add(key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private static boolean containsNode(List<Map<String, Object>> nodes, String id) {
|
|
109
|
+
for (Map<String, Object> n : nodes) {
|
|
110
|
+
if (id.equals(n.get("id"))) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static List<String> methodIds(List<Map<String, Object>> nodes, Parser.ParsedFile pf,
|
|
118
|
+
Parser.TypeInfo t, String methodName) {
|
|
119
|
+
List<String> ids = new ArrayList<>();
|
|
120
|
+
for (Map<String, Object> n : nodes) {
|
|
121
|
+
String id = (String) n.get("id");
|
|
122
|
+
if (id == null || !id.startsWith(pf.relPath + "::" + t.dottedName + ".")) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!pf.relPath.equals(n.get("filePath"))) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
@SuppressWarnings("unchecked")
|
|
129
|
+
Map<String, Object> metadata = (Map<String, Object>) n.get("metadata");
|
|
130
|
+
if (metadata == null || !t.dottedName.equals(metadata.get("parentClass"))) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (Boolean.TRUE.equals(metadata.get("isConstructor"))) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!Boolean.TRUE.equals(metadata.get("isPublic"))) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
String nodeName = (String) n.get("name");
|
|
140
|
+
if (nodeName != null && (nodeName.equals(methodName)
|
|
141
|
+
|| nodeName.endsWith("." + methodName)
|
|
142
|
+
|| nodeName.endsWith("#" + methodName))) {
|
|
143
|
+
ids.add(id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return ids;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -127,6 +127,10 @@ public final class Extractor {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// Overload disambiguation — same-name methods in one file get
|
|
131
|
+
// signature-derived id suffixes; methodCallFacts keys are remapped.
|
|
132
|
+
Overloads.disambiguate(nodes, methodCallFacts);
|
|
133
|
+
|
|
130
134
|
// ── 3. shared lookup (built ONCE, consumed by every detector) ───
|
|
131
135
|
LookupMaps lookup = LookupMaps.build(parsedFiles, nodes);
|
|
132
136
|
lookup.methodCallFacts.putAll(methodCallFacts);
|
|
@@ -168,6 +172,10 @@ public final class Extractor {
|
|
|
168
172
|
result.put("nodes", nodes);
|
|
169
173
|
result.put("edges", edgesOut);
|
|
170
174
|
result.put("routes", routeResult.routes);
|
|
175
|
+
Map<String, Object> exportsMap = ExportsMapBuilder.build(parsedFiles, nodes);
|
|
176
|
+
if (exportsMap != null) {
|
|
177
|
+
result.put("exports", exportsMap);
|
|
178
|
+
}
|
|
171
179
|
Map<String, Object> stats = new LinkedHashMap<>();
|
|
172
180
|
stats.put("totalFiles", totalFiles);
|
|
173
181
|
stats.put("totalNodes", nodes.size());
|
|
@@ -240,6 +248,7 @@ public final class Extractor {
|
|
|
240
248
|
metadata.put("isConstructor", m.isConstructor);
|
|
241
249
|
metadata.put("isStatic", m.isStatic);
|
|
242
250
|
metadata.put("isAbstract", m.isAbstract);
|
|
251
|
+
metadata.put("isPublic", m.isPublic);
|
|
243
252
|
if (!m.annotations.isEmpty()) {
|
|
244
253
|
metadata.put("annotations", m.annotations);
|
|
245
254
|
}
|
|
@@ -255,8 +264,33 @@ public final class Extractor {
|
|
|
255
264
|
if (!m.calls.isEmpty()) {
|
|
256
265
|
List<String> callStrings = m.calls.stream().map(c -> c.name).distinct().sorted().toList();
|
|
257
266
|
metadata.put("calls", callStrings);
|
|
267
|
+
metadata.put("callSites", callSitesOf(m.calls));
|
|
258
268
|
}
|
|
259
269
|
return Contract.codeNode(pf.relPath, t.dottedName + "." + m.name, "METHOD",
|
|
260
270
|
m.startLine, m.endLine, m.rawCode, metadata, LANGUAGE);
|
|
261
271
|
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Structured call-site records ({name, argCount, argTypes, hasSpread?})
|
|
275
|
+
* mirroring metadata.callSites in the TypeScript engine. One record per
|
|
276
|
+
* (name, argCount, argTypes) triple; first-seen order.
|
|
277
|
+
*/
|
|
278
|
+
private static List<Map<String, Object>> callSitesOf(List<Parser.CallInfo> calls) {
|
|
279
|
+
LinkedHashMap<String, Map<String, Object>> out = new LinkedHashMap<>();
|
|
280
|
+
for (Parser.CallInfo ci : calls) {
|
|
281
|
+
String key = ci.name + "/" + ci.argCount + "/" + String.join(",", ci.argTypes);
|
|
282
|
+
if (out.containsKey(key)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
Map<String, Object> cs = new LinkedHashMap<>();
|
|
286
|
+
cs.put("name", ci.name);
|
|
287
|
+
cs.put("argCount", ci.argCount);
|
|
288
|
+
cs.put("argTypes", new ArrayList<>(ci.argTypes));
|
|
289
|
+
if (ci.hasSpread) {
|
|
290
|
+
cs.put("hasSpread", true);
|
|
291
|
+
}
|
|
292
|
+
out.put(key, cs);
|
|
293
|
+
}
|
|
294
|
+
return new ArrayList<>(out.values());
|
|
295
|
+
}
|
|
262
296
|
}
|
|
@@ -28,6 +28,8 @@ public final class LookupMaps {
|
|
|
28
28
|
public final Map<String, String> typeMap = new HashMap<>();
|
|
29
29
|
public final Map<String, String> typeDottedMap = new HashMap<>();
|
|
30
30
|
public final List<String> methodNodes = new ArrayList<>();
|
|
31
|
+
/** classNodeId.methodName → METHOD node ids (overload group, line order) */
|
|
32
|
+
public final Map<String, List<String>> methodOverloads = new LinkedHashMap<>();
|
|
31
33
|
/** method node id → parse-time call facts (CallInfo) */
|
|
32
34
|
public final Map<String, List<Parser.CallInfo>> methodCallFacts = new LinkedHashMap<>();
|
|
33
35
|
/** test file relPath → test method names (metadata.testCases) */
|
|
@@ -50,6 +52,16 @@ public final class LookupMaps {
|
|
|
50
52
|
}
|
|
51
53
|
if ("METHOD".equals(type)) {
|
|
52
54
|
lm.methodNodes.add(id);
|
|
55
|
+
// Overload index — keyed by the UNSUFFIXED base id
|
|
56
|
+
// (rel::Type.method); suffixed overload ids group under it.
|
|
57
|
+
String filePath = (String) node.get("filePath");
|
|
58
|
+
int dot = name.lastIndexOf('.');
|
|
59
|
+
if (dot > 0) {
|
|
60
|
+
// key = unsuffixed base id: rel::Type.method (node "name"
|
|
61
|
+
// never carries a suffix; only "id" does)
|
|
62
|
+
String key = filePath + "::" + name;
|
|
63
|
+
lm.methodOverloads.computeIfAbsent(key, k -> new ArrayList<>()).add(id);
|
|
64
|
+
}
|
|
53
65
|
} else if (isTypeNode(type)) {
|
|
54
66
|
String filePath = (String) node.get("filePath");
|
|
55
67
|
lm.nodesByFile.computeIfAbsent(filePath, k -> new HashMap<>())
|