-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathentity_linker.go
More file actions
96 lines (86 loc) · 2.95 KB
/
Copy pathentity_linker.go
File metadata and controls
96 lines (86 loc) · 2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package linker
import (
"fmt"
"sort"
"strings"
"github.com/randomcodespace/codeiq/internal/model"
)
// repoSuffixes is the ordered list of suffixes matched on REPOSITORY labels.
// First match wins, so the order matters: `Repository` before `Repo` so that
// `UserRepository` strips → `User` (not `UserRepository` minus `Repo` →
// `UserRepository`).
var repoSuffixes = []string{"Repository", "Repo", "Dao", "DAO"}
// EntityLinker emits QUERIES edges from REPOSITORY nodes to the ENTITY nodes
// they manage, matched by naming convention (e.g. `UserRepository` →
// `User`, `OrderDao` → `Order`).
//
// Mirrors src/main/java/io/github/randomcodespace/iq/analyzer/linker/EntityLinker.java
// (lines 33-98).
type EntityLinker struct{}
// NewEntityLinker returns a stateless linker.
func NewEntityLinker() *EntityLinker { return &EntityLinker{} }
// Link iterates repositories and matches them to entities by simple-name
// (case-insensitive) after stripping the longest recognised suffix. Skips
// repositories that already have an outbound QUERIES edge to the candidate
// entity to avoid duplicates with what detectors emitted.
func (l *EntityLinker) Link(nodes []*model.CodeNode, edges []*model.CodeEdge) Result {
var entities, repositories []*model.CodeNode
for _, n := range nodes {
switch n.Kind {
case model.NodeEntity:
entities = append(entities, n)
case model.NodeRepository:
repositories = append(repositories, n)
}
}
if len(entities) == 0 || len(repositories) == 0 {
return Result{}
}
entityByName := make(map[string]*model.CodeNode)
for _, e := range entities {
entityByName[strings.ToLower(e.Label)] = e
if e.FQN != "" {
simple := e.FQN
if idx := strings.LastIndex(simple, "."); idx >= 0 {
simple = simple[idx+1:]
}
entityByName[strings.ToLower(simple)] = e
}
}
existing := map[string]struct{}{}
for _, e := range edges {
if e.Kind == model.EdgeQueries {
existing[e.SourceID+"->"+e.TargetID] = struct{}{}
}
}
// Iterate repositories in ID order for determinism (Java side relies on
// the GraphBuilder snapshot already being sorted; we don't, so sort here).
sort.Slice(repositories, func(i, j int) bool { return repositories[i].ID < repositories[j].ID })
var newEdges []*model.CodeEdge
for _, repo := range repositories {
for _, suf := range repoSuffixes {
if !strings.HasSuffix(repo.Label, suf) {
continue
}
base := strings.ToLower(repo.Label[:len(repo.Label)-len(suf)])
ent, ok := entityByName[base]
if !ok {
break // first matching suffix wins, even if entity missing
}
key := repo.ID + "->" + ent.ID
if _, dup := existing[key]; dup {
break
}
newEdges = append(newEdges, &model.CodeEdge{
ID: fmt.Sprintf("entity-link:%s->%s", repo.ID, ent.ID),
Kind: model.EdgeQueries,
SourceID: repo.ID,
TargetID: ent.ID,
Source: SrcEntityLinker,
Properties: map[string]any{"inferred": true},
})
break
}
}
return Result{Edges: newEdges}
}