devlensio 1.0.4 → 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.
Files changed (60) hide show
  1. package/dist/graph/edges/callEdges.js +97 -5
  2. package/dist/graph/edges/callEdges.test.js +154 -0
  3. package/dist/parser/extractors/classes.js +23 -4
  4. package/dist/parser/extractors/functions.d.ts +10 -3
  5. package/dist/parser/extractors/functions.js +135 -65
  6. package/dist/parser/extractors/hooks.js +5 -29
  7. package/dist/parser/extractors/objectMethods.js +3 -4
  8. package/dist/parser/index.js +11 -6
  9. package/dist/parser/overloads.d.ts +2 -0
  10. package/dist/parser/overloads.js +96 -0
  11. package/dist/parser/overloads.test.d.ts +1 -0
  12. package/dist/parser/overloads.test.js +175 -0
  13. package/dist/parser/typeUtils.d.ts +2 -0
  14. package/dist/parser/typeUtils.js +2 -0
  15. package/dist/types.d.ts +6 -0
  16. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  17. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  18. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  19. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  20. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  21. package/extractors/go/calls.go +134 -2
  22. package/extractors/go/contract.go +1 -0
  23. package/extractors/go/exports.go +107 -0
  24. package/extractors/go/exports_test.go +64 -0
  25. package/extractors/go/extractor.go +1 -0
  26. package/extractors/go/lookup.go +63 -7
  27. package/extractors/go/nodes.go +28 -0
  28. package/extractors/go/parser.go +51 -8
  29. package/extractors/java/devlens_java_extractor.jar +0 -0
  30. package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
  31. package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
  32. package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
  33. package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
  34. package/extractors/java/src/devlens/extractor/Parser.java +39 -0
  35. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
  36. package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
  37. package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
  38. package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
  39. package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
  40. package/extractors/python/devlens_extractors_python/extractor.py +7 -1
  41. package/extractors/python/devlens_extractors_python/lookup.py +6 -0
  42. package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
  43. package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
  44. package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -0
  45. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  46. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  47. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  48. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  49. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  50. package/extractors/rust/src/calls.rs +27 -16
  51. package/extractors/rust/src/contract.rs +2 -0
  52. package/extractors/rust/src/exports.rs +387 -0
  53. package/extractors/rust/src/exports_test.rs +105 -0
  54. package/extractors/rust/src/extractor.rs +2 -0
  55. package/extractors/rust/src/lookup.rs +60 -0
  56. package/extractors/rust/src/main.rs +4 -0
  57. package/extractors/rust/src/nodes.rs +19 -0
  58. package/extractors/rust/src/parser.rs +66 -1
  59. package/extractors/rust/src/routes.rs +2 -1
  60. package/package.json +1 -1
@@ -0,0 +1,107 @@
1
+ // exports.go — the flattened public surface of a Go module, shipped in the
2
+ // ExtractorResult for cross-repo linking (docs/EXPORTS_MAP.md).
3
+ //
4
+ // Go-specific concerns:
5
+ // - Export = capitalized identifier (token.IsExported); Go has no
6
+ // package.json "door" — every package is importable, so the map is keyed
7
+ // by package: subpath "." for the module-root package, "./rel/dir" for
8
+ // the rest (mirrors the TS multi-door shape).
9
+ // - The map key is the name an importer writes: plain funcs/structs/
10
+ // interfaces/enums use their bare name; exported methods of exported
11
+ // types use "Type.Method" (that is what consumer code writes).
12
+ // - Go has no overloads → values are always single-element arrays.
13
+ // - Test files are excluded (their symbols are not importable surface).
14
+ // - Same-name duplicates within a package cannot compile, but if the
15
+ // walker ever yields them, all ids are emitted rather than guessing.
16
+ // - Requires go.mod: without a module path the consumer-side join key
17
+ // ([mod]/importPath) is undefined, so no map is emitted.
18
+ package main
19
+
20
+ import (
21
+ "go/token"
22
+ "sort"
23
+ "strings"
24
+ )
25
+
26
+ type ExportsMap struct {
27
+ Exports map[string]map[string][]string `json:"exports"`
28
+ AmbiguousNames map[string][]string `json:"ambiguousNames"`
29
+ }
30
+
31
+ func buildExportsMap(pr *parsedRepo) *ExportsMap {
32
+ if pr.mod == nil || !pr.mod.HasMod || pr.mod.ModulePath == "" {
33
+ return nil
34
+ }
35
+
36
+ exports := map[string]map[string][]string{}
37
+ ambiguous := map[string][]string{}
38
+
39
+ for _, pkg := range pr.packages {
40
+ rel := strings.TrimPrefix(pkg.ImportPath, pr.mod.ModulePath)
41
+ rel = strings.TrimPrefix(rel, "/")
42
+ subpath := "."
43
+ if rel != "" && rel != "." {
44
+ subpath = "./" + rel
45
+ }
46
+
47
+ perPath := map[string][]string{}
48
+ for _, pf := range pkg.Files {
49
+ if pf.IsTest {
50
+ continue
51
+ }
52
+ for _, fn := range pf.Funcs {
53
+ if fn.IsTest {
54
+ continue
55
+ }
56
+ if fn.IsMethod {
57
+ if !token.IsExported(fn.RecvType) || !token.IsExported(fn.Name) {
58
+ continue
59
+ }
60
+ addExport(perPath, fn.RecvType+"."+fn.Name, methodNodeID(pf.RelPath, fn))
61
+ } else {
62
+ if !token.IsExported(fn.Name) {
63
+ continue
64
+ }
65
+ addExport(perPath, fn.Name, funcNodeID(pf.RelPath, fn.Name))
66
+ }
67
+ }
68
+ for _, st := range pf.Structs {
69
+ if !token.IsExported(st.Name) {
70
+ continue
71
+ }
72
+ addExport(perPath, st.Name, structNodeID(pf.RelPath, st.Name))
73
+ }
74
+ for _, it := range pf.Interfaces {
75
+ if !token.IsExported(it.Name) {
76
+ continue
77
+ }
78
+ addExport(perPath, it.Name, interfaceNodeID(pf.RelPath, it.Name))
79
+ }
80
+ for _, en := range pf.numericEnums {
81
+ if !token.IsExported(en.TypeName) {
82
+ continue
83
+ }
84
+ addExport(perPath, en.TypeName, structNodeID(pf.RelPath, en.TypeName))
85
+ }
86
+ }
87
+
88
+ if len(perPath) > 0 {
89
+ exports[subpath] = perPath
90
+ }
91
+ }
92
+
93
+ if len(exports) == 0 {
94
+ return nil
95
+ }
96
+ return &ExportsMap{Exports: exports, AmbiguousNames: ambiguous}
97
+ }
98
+
99
+ func addExport(perPath map[string][]string, name, id string) {
100
+ for _, existing := range perPath[name] {
101
+ if existing == id {
102
+ return
103
+ }
104
+ }
105
+ perPath[name] = append(perPath[name], id)
106
+ sort.Strings(perPath[name])
107
+ }
@@ -0,0 +1,64 @@
1
+ package main
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+ )
7
+
8
+ func TestBuildExportsMapPublicSurface(t *testing.T) {
9
+ repo := parseRepo("tests/fixtures/plainfix", parseGoMod("tests/fixtures/plainfix"), func(string, string) {})
10
+ m := buildExportsMap(repo)
11
+ if m == nil {
12
+ t.Fatal("expected exports map for a module with go.mod")
13
+ }
14
+
15
+ perPath, ok := m.Exports["."]
16
+ if !ok {
17
+ t.Fatalf("expected root package subpath '.', got subpaths %v", keys(m.Exports))
18
+ }
19
+
20
+ expectIds(perPath, t, "Shape", []string{"shapes.go::Shape"})
21
+ expectIds(perPath, t, "Circle", []string{"shapes.go::Circle"})
22
+ expectIds(perPath, t, "Circle.Area", []string{"shapes.go::Circle.Area"})
23
+ expectIds(perPath, t, "Circle.Perimeter", []string{"shapes.go::Circle.Perimeter"})
24
+ expectIds(perPath, t, "Square.Scale", []string{"shapes.go::Square.Scale"})
25
+ expectIds(perPath, t, "Color", []string{"shapes.go::Color"})
26
+
27
+ if len(m.AmbiguousNames) != 0 {
28
+ t.Fatalf("expected no ambiguous names, got %v", m.AmbiguousNames)
29
+ }
30
+ }
31
+
32
+ func TestBuildExportsMapRequiresGoMod(t *testing.T) {
33
+ repo := parseRepo("tests/fixtures/plainfix", &modFile{}, func(string, string) {})
34
+ if m := buildExportsMap(repo); m != nil {
35
+ t.Fatal("expected no exports map without a module path")
36
+ }
37
+ }
38
+
39
+ func expectIds(perPath map[string][]string, t *testing.T, name string, want []string) {
40
+ t.Helper()
41
+ got, ok := perPath[name]
42
+ if !ok {
43
+ t.Fatalf("expected exported name %q in map, got keys %v", name, keysOf(perPath))
44
+ }
45
+ if !reflect.DeepEqual(got, want) {
46
+ t.Fatalf("%s: got %v, want %v", name, got, want)
47
+ }
48
+ }
49
+
50
+ func keys(m map[string]map[string][]string) []string {
51
+ out := []string{}
52
+ for k := range m {
53
+ out = append(out, k)
54
+ }
55
+ return out
56
+ }
57
+
58
+ func keysOf(m map[string][]string) []string {
59
+ out := []string{}
60
+ for k := range m {
61
+ out = append(out, k)
62
+ }
63
+ return out
64
+ }
@@ -109,6 +109,7 @@ func runExtractor(repoPath string, opts *Options) *ExtractorResult {
109
109
  result.Nodes = nodes
110
110
  result.Edges = allEdges
111
111
  result.Routes = routes
112
+ result.Exports = buildExportsMap(parsed)
112
113
  result.Stats = Stats{
113
114
  TotalFiles: len(parsed.files),
114
115
  TotalNodes: len(nodes),
@@ -4,7 +4,10 @@
4
4
 
5
5
  package main
6
6
 
7
- import "strings"
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
- l.MethodNodeByPkgTypeName[fn.PkgPath+"::"+fn.RecvType+"."+fn.Name] = methodNodeID(pf.RelPath, fn)
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
- l.FuncNodeByPkgName[fn.PkgPath+"::"+fn.Name] = funcNodeID(pf.RelPath, fn.Name)
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 { return rel + "::" + name }
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 { return rel + "::" + name }
98
- func interfaceNodeID(rel, name string) string { return rel + "::" + name }
99
- func thirdPartyID(importPath string) string { return "[mod]/" + importPath }
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 {
@@ -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)
@@ -20,12 +20,15 @@ import (
20
20
  // ── fact types ──
21
21
 
22
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
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) {
@@ -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<>())