项目文件夹

文件
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

298 行
9.0 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package indexer
import (
"github.com/zzet/gortex/internal/contracts"
"github.com/zzet/gortex/internal/graph"
)
// extractDIContracts walks the graph for DI-tagged EdgeProvides and
// EdgeConsumes edges (emitted by the TypeScript extractor for @Module
// providers and @Inject consumers) and materialises them as Contract
// records in reg. The contract ID shape `di::<token>` is the same on
// both sides so the standard matcher reports orphans (tokens provided
// but not consumed, or consumed but never provided).
//
// This runs as a post-pass after per-file contract extraction because
// DI edges already live in the graph at that point — no source re-parse
// required. Safe to call repeatedly; AddAll de-duplicates by contract
// ID + symbol ID.
//
// In multi-repo mode the walk is scoped to this repo's nodes' out-edges
// instead of the global edge slice. The previous full walk produced
// per-repo work proportional to the entire shared graph, which became
// the dominant per-repo cost at large repo counts (and incorrectly
// re-attributed contracts from other repos to this one via
// AddAllScoped's RepoPrefix overwrite).
func (idx *Indexer) extractDIContracts(reg *contracts.Registry) {
if reg == nil {
return
}
// Spring @Bean linkage runs first and produces new EdgeCalls edges
// that the later Contract-emission pass needs to consider. Ordering
// also keeps bean extraction independent of the contract-side —
// a repo that only uses Spring still gets usable bean links even
// if no @Inject / useClass contracts exist anywhere.
idx.linkSpringBeans()
var discovered []contracts.Contract
if idx.repoPrefix != "" {
// Multi-repo: walk only this repo's outgoing edges via a
// single backend query. The previous GetRepoNodes ×
// GetOutEdges nested walk was O(repo_nodes) per-node round-
// trips on disk backends — at ~68k repo nodes that meant
// 68k backend queries per pass on a disk backend.
for _, e := range idx.graph.GetRepoEdges(idx.repoPrefix) {
c, ok := diContractFromEdge(e)
if !ok {
continue
}
discovered = append(discovered, c)
}
} else {
// Single-repo: every edge belongs to this repo.
for _, e := range idx.graph.AllEdges() {
c, ok := diContractFromEdge(e)
if !ok {
continue
}
discovered = append(discovered, c)
}
}
if len(discovered) == 0 {
return
}
reg.AddAllScoped(discovered, idx.repoPrefix, idx.workspaceID, idx.projectID)
}
// linkSpringBeans emits EdgeCalls from every class that has an
// incoming-method node whose signature mentions a @Bean return type
// back to the bean method. Uses method signatures because the Java
// extractor already stores them on constructor nodes — no second
// parse pass needed. Kept tight by requiring an exact type-name
// token match inside the signature string.
//
// In multi-repo mode the walks are scoped to this repo's nodes so the
// per-repo cost stays proportional to the repo's own size, not the
// shared workspace graph.
func (idx *Indexer) linkSpringBeans() {
type beanRef struct {
methodID string
typeName string
filePath string
line int
}
var beans []beanRef
collectBean := func(e *graph.Edge) {
if e.Kind != graph.EdgeProvides || e.Meta == nil {
return
}
if b, _ := e.Meta["binding"].(string); b != "bean" {
return
}
rt, _ := e.Meta["provides_for"].(string)
if rt == "" {
return
}
beans = append(beans, beanRef{methodID: e.To, typeName: rt, filePath: e.FilePath, line: e.Line})
}
if idx.repoPrefix != "" {
// Single backend query instead of one GetOutEdges per
// repo node — see extractDIContracts above for the round-
// trip math.
for _, e := range idx.graph.GetRepoEdges(idx.repoPrefix) {
collectBean(e)
}
} else {
for _, e := range idx.graph.AllEdges() {
collectBean(e)
}
}
if len(beans) == 0 {
return
}
// Java method-node candidates considered for bean injection. Scoping
// by repo here avoids a per-repo O(global_nodes) walk for every
// bean — the dominant cost on workspaces that mix one Java repo
// with hundreds of non-Java siblings.
var candidates []*graph.Node
if idx.repoPrefix != "" {
for _, n := range idx.graph.GetRepoNodes(idx.repoPrefix) {
if n.Kind == graph.KindMethod && n.Language == "java" {
candidates = append(candidates, n)
}
}
} else {
for _, n := range idx.graph.AllNodes() {
if n.Kind == graph.KindMethod && n.Language == "java" {
candidates = append(candidates, n)
}
}
}
// For each bean, walk Java constructors and @Bean factory methods
// whose params_src (captured at extraction time) mentions the return
// type. Dedupe by (consumer_class, bean_method) so an overloaded
// constructor or factory method only links once.
linked := make(map[string]struct{})
for _, b := range beans {
for _, n := range candidates {
if n.ID == b.methodID {
continue
}
params, _ := n.Meta["params_src"].(string)
if params == "" {
continue
}
if !signatureReferencesType(params, b.typeName) {
continue
}
cls := enclosingClassID(n)
if cls == "" || cls == b.methodID {
continue
}
key := cls + "->" + b.methodID
if _, dup := linked[key]; dup {
continue
}
linked[key] = struct{}{}
idx.graph.AddEdge(&graph.Edge{
From: cls,
To: b.methodID,
Kind: graph.EdgeCalls,
FilePath: b.filePath,
Line: b.line,
Meta: map[string]any{
"via": "spring.Bean",
"bean_of": b.typeName,
},
})
}
}
}
// signatureReferencesType returns true when sig contains typeName as a
// whole identifier (e.g. "Clock", "UserService"). Conservative match —
// substring-but-word-boundary to avoid `Clock` matching `ClockFactory`.
func signatureReferencesType(sig, typeName string) bool {
i := 0
for i < len(sig) {
j := indexOf(sig[i:], typeName)
if j < 0 {
return false
}
start := i + j
end := start + len(typeName)
leftOK := start == 0 || !isJavaIdentChar(sig[start-1])
rightOK := end == len(sig) || !isJavaIdentChar(sig[end])
if leftOK && rightOK {
return true
}
i = end
}
return false
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
func isJavaIdentChar(b byte) bool {
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '_' || b == '$'
}
// enclosingClassID derives the class-level node ID from a method node
// using its Meta["receiver"] (what the Java extractor stores). Returns
// "" if we can't derive one (free functions, static methods on pkg
// objects, etc. — not relevant for Spring anyway).
func enclosingClassID(n *graph.Node) string {
recv, _ := n.Meta["receiver"].(string)
if recv == "" {
return ""
}
return n.FilePath + "::" + recv
}
// diContractFromEdge maps one EdgeProvides / EdgeConsumes edge to a
// Contract when its Meta identifies it as a DI binding. Returns
// (Contract, false) for non-DI edges (HTTP/gRPC contracts already use
// these same edge kinds, so we must not treat every Provides edge as
// a DI record).
func diContractFromEdge(e *graph.Edge) (contracts.Contract, bool) {
var zero contracts.Contract
if e == nil || e.Meta == nil {
return zero, false
}
var token string
var role contracts.Role
var meta map[string]any
switch e.Kind {
case graph.EdgeProvides:
// Providers carry binding: "useClass" / "useValue" / "useFactory"
// / "useExisting" / "bean". useClass and Spring's bean both
// name their abstract via provides_for; the token forms use
// the token name itself.
binding, _ := e.Meta["binding"].(string)
switch binding {
case "useClass", "bean":
if s, _ := e.Meta["provides_for"].(string); s != "" {
token = s
}
case "useValue", "useFactory", "useExisting":
if s, _ := e.Meta["di_token"].(string); s != "" {
token = s
}
default:
return zero, false
}
role = contracts.RoleProvider
meta = map[string]any{"binding": binding}
if target := e.To; target != "" {
// For useClass / bean, record the concrete target so the
// orphan list in the contracts tool links straight to
// either the concrete class (useClass) or the factory
// method (bean). Token-form providers point at the token
// directly, no extra info needed.
if binding == "useClass" || binding == "bean" {
meta[binding] = target
}
}
case graph.EdgeConsumes:
if v, _ := e.Meta["via"].(string); v != "@Inject" {
return zero, false
}
token, _ = e.Meta["di_token"].(string)
role = contracts.RoleConsumer
meta = map[string]any{"via": "@Inject"}
default:
return zero, false
}
if token == "" {
return zero, false
}
return contracts.Contract{
ID: "di::" + token,
Type: contracts.ContractDI,
Role: role,
SymbolID: e.From,
FilePath: e.FilePath,
Line: e.Line,
Meta: meta,
// Confidence mirrors the edge's originating extractor — these
// are static `@Module` / `@Inject` decorators, high-confidence
// by construction. Lower values would belong to future
// inferred DI (e.g. if we ever infer bindings from tsconfig
// paths).
Confidence: 0.9,
}, true
}