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
489 lines
23 KiB
Go
489 lines
23 KiB
Go
package resolver
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// Framework-dispatch synthesizer engine.
|
|
//
|
|
// Direct AST/LSP resolution lands the calls a compiler can see. A large
|
|
// class of real call edges, though, is wired by a *framework* at runtime
|
|
// and is invisible to static resolution: a gRPC client stub dispatched
|
|
// to its server handler, a Temporal workflow proxy to its activity, an
|
|
// event published on one side of an in-process channel and handled on
|
|
// the other, a JS bridge method routed to its native implementation.
|
|
//
|
|
// FrameworkSynthesizer is the plugin contract for a pass that
|
|
// materialises one such family of edges. Every synthesizer is a
|
|
// full-recompute, idempotent pass: it derives each edge it owns from
|
|
// durable graph state (placeholder edges plus their Meta markers, shared
|
|
// topic nodes, registration call edges) so a reindex of any endpoint
|
|
// re-lands or un-lands the edge without leaving a stale one behind —
|
|
// graph.AddEdge dedupes by edge key and graph.EvictFile drops a node's
|
|
// edges in both directions. Every edge a synthesizer lands is stamped
|
|
// with provenance (StampSynthesized) so its origin is auditable and the
|
|
// `analyze kind=synthesizers` roll-up can attribute it.
|
|
//
|
|
// The engine is the single orchestration point: the indexers call
|
|
// RunFrameworkSynthesizers at every settle point (full index, watcher
|
|
// reindex, incremental reindex) in place of invoking each pass directly,
|
|
// so adding a synthesizer (a native-bridge resolver, an event-channel
|
|
// pass) is one line in defaultFrameworkSynthesizers rather than an edit
|
|
// at six call sites.
|
|
type FrameworkSynthesizer interface {
|
|
// Name is the stable provenance tag stamped on every edge the
|
|
// synthesizer lands (lower-kebab, e.g. "grpc-stub", "event-channel").
|
|
Name() string
|
|
// Synthesize runs the pass over g and returns the number of edges the
|
|
// synthesizer owns (landed on a real target) after this run.
|
|
Synthesize(g graph.Store) int
|
|
}
|
|
|
|
// Edge.Meta keys stamped by StampSynthesized.
|
|
const (
|
|
// MetaSynthesizedBy names the synthesizer that produced an edge.
|
|
MetaSynthesizedBy = "synthesized_by"
|
|
// MetaProvenance records that an edge is a heuristic materialisation
|
|
// rather than a compiler-verified fact.
|
|
MetaProvenance = "provenance"
|
|
// ProvenanceHeuristic is the MetaProvenance value the string- and
|
|
// name-keyed framework synthesizers stamp — these edges are
|
|
// framework-dispatch inferences correlated by a literal (an event
|
|
// name, a dispatch string, a registry key) with no type evidence.
|
|
ProvenanceHeuristic = "heuristic"
|
|
// ProvenanceFramework is the MetaProvenance value the typed,
|
|
// decorator-, base-list- or type-keyed synthesizers stamp — the
|
|
// framework's own contract (a decorator, a generic base, a typed
|
|
// listener parameter) names the target, so the edge carries more
|
|
// confidence than a string-correlated guess. analyze kind=synthesizers
|
|
// reports the two tiers separately from the same MetaProvenance read.
|
|
ProvenanceFramework = "framework"
|
|
)
|
|
|
|
// Confidence tiers the framework synthesizers stamp on a landed edge.
|
|
// Typed/decorator/base-list/type-keyed passes (RTK Query, Celery, Spring,
|
|
// MediatR, Sidekiq, Laravel, GoFrame) use ConfidenceTyped; the string-
|
|
// and name-keyed passes (Vuex, Redux-thunk, object-registry, fn-pointer,
|
|
// Django) use ConfidenceHeuristic.
|
|
const (
|
|
// ConfidenceTyped is the confidence for a type-/decorator-/base-list-
|
|
// keyed dispatch edge — the framework contract names the target.
|
|
ConfidenceTyped = 0.85
|
|
// ConfidenceHeuristic is the confidence for a string-/name-keyed
|
|
// dispatch edge — correlated by a literal, not by a type.
|
|
ConfidenceHeuristic = 0.6
|
|
)
|
|
|
|
// Stable per-synthesizer provenance names. Used both as the registry
|
|
// label (for the report grouping) and as the value stamped on each
|
|
// landed edge, so the two never drift.
|
|
const (
|
|
SynthGRPCStub = "grpc-stub"
|
|
SynthTemporalStub = "temporal-stub"
|
|
SynthEventChannel = "event-channel"
|
|
SynthSwiftObjC = "swift-objc-bridge"
|
|
SynthReactNative = "react-native-bridge"
|
|
SynthReactNativePair = "react-native-native-pair"
|
|
SynthObserverChannel = "observer-channel"
|
|
SynthClosureCollection = "closure-collection"
|
|
SynthReactSetState = "react-setstate"
|
|
SynthFlutterSetState = "flutter-setstate"
|
|
SynthKMPExpectActual = "kmp-expect-actual"
|
|
SynthExpoModules = "expo-modules-bridge"
|
|
SynthFabric = "fabric-codegen"
|
|
SynthMyBatis = "mybatis"
|
|
SynthRustScope = "rust-scope"
|
|
SynthFactoryChain = "factory-chain"
|
|
SynthSQLCallsite = "sql-callsite"
|
|
SynthStoreFactory = "store-factory"
|
|
SynthReduxThunk = "redux-thunk"
|
|
SynthNgRxEffect = "ngrx-effect"
|
|
SynthObjectRegistry = "object-registry"
|
|
SynthRTKQuery = "rtk-query"
|
|
SynthVuexDispatch = "vuex-dispatch"
|
|
SynthCelery = "celery-dispatch"
|
|
SynthSpringEvent = "spring-event"
|
|
SynthMediatR = "mediatr-dispatch"
|
|
SynthCSharpIfaceDispatch = "csharp-iface-dispatch"
|
|
SynthSidekiq = "sidekiq-dispatch"
|
|
SynthLaravelEvent = "laravel-event"
|
|
SynthFnPointerDispatch = "fn-pointer-dispatch"
|
|
SynthMacroExpansion = "macro-expansion"
|
|
SynthGoFrameRoute = "goframe-route"
|
|
SynthDjangoDescriptor = "django-descriptor"
|
|
SynthExpressResolve = "express-resolve"
|
|
SynthReactResolve = "react-resolve"
|
|
SynthFastAPIResolve = "fastapi-resolve"
|
|
SynthRailsResolve = "rails-resolve"
|
|
SynthSwiftUIResolve = "swiftui-resolve"
|
|
SynthUIKitResolve = "uikit-resolve"
|
|
SynthVaporResolve = "vapor-resolve"
|
|
SynthGinMiddleware = "gin-middleware"
|
|
SynthSvelteKitLoad = "sveltekit-load"
|
|
SynthSpeculative = "speculative-dispatch"
|
|
SynthFnValue = SynthFnValueCallback
|
|
SynthPascalFormName = SynthPascalForm
|
|
SynthValueRefName = SynthValueRef
|
|
)
|
|
|
|
// StampSynthesized marks an edge as the product of a framework
|
|
// synthesizer: which synthesizer produced it (name) and that it is a
|
|
// heuristic materialisation. Safe on an edge with a nil Meta map.
|
|
func StampSynthesized(e *graph.Edge, name string) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
if e.Meta == nil {
|
|
e.Meta = map[string]any{}
|
|
}
|
|
e.Meta[MetaSynthesizedBy] = name
|
|
if _, ok := e.Meta[MetaProvenance]; !ok {
|
|
e.Meta[MetaProvenance] = ProvenanceHeuristic
|
|
}
|
|
}
|
|
|
|
// StampSynthesizedTyped marks an edge as the product of a typed-tier
|
|
// framework synthesizer: like StampSynthesized, but records
|
|
// ProvenanceFramework instead of ProvenanceHeuristic so the
|
|
// type-/decorator-/base-list-keyed passes (RTK Query, Celery, Spring,
|
|
// MediatR, Sidekiq, Laravel, GoFrame) separate from the string-keyed
|
|
// ones in analyze kind=synthesizers. Safe on an edge with a nil Meta map.
|
|
func StampSynthesizedTyped(e *graph.Edge, name string) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
if e.Meta == nil {
|
|
e.Meta = map[string]any{}
|
|
}
|
|
e.Meta[MetaProvenance] = ProvenanceFramework
|
|
StampSynthesized(e, name)
|
|
}
|
|
|
|
// UnstampSynthesized clears the provenance markers an edge picked up from
|
|
// a synthesizer. Called when a pass re-orphans an edge (its target
|
|
// disappeared) so the edge reads as a plain placeholder again.
|
|
func UnstampSynthesized(e *graph.Edge) {
|
|
if e == nil || e.Meta == nil {
|
|
return
|
|
}
|
|
delete(e.Meta, MetaSynthesizedBy)
|
|
delete(e.Meta, MetaProvenance)
|
|
}
|
|
|
|
// synthFunc adapts a plain pass function into a FrameworkSynthesizer so
|
|
// the existing passes (ResolveGRPCStubCalls, …) register without a
|
|
// wrapper type each.
|
|
//
|
|
// scopedFn is optional: when set, the end-of-batch driver calls it with the
|
|
// changed-repo prefix set so the synthesizer can restrict its CANDIDATE scan to
|
|
// those repos (resolution stays whole-graph). A synthesizer without a scopedFn
|
|
// always runs whole-graph — correct, just not narrowed — so scoping any one
|
|
// pass is opt-in and additive.
|
|
type synthFunc struct {
|
|
name string
|
|
fn func(graph.Store) int
|
|
scopedFn func(graph.Store, map[string]bool) int
|
|
}
|
|
|
|
func (s synthFunc) Name() string { return s.name }
|
|
func (s synthFunc) Synthesize(g graph.Store) int { return s.fn(g) }
|
|
|
|
// synthesizeScoped runs the scoped variant when one is registered and a scope
|
|
// is armed; otherwise it falls back to the whole-graph pass. A nil scope always
|
|
// means whole-graph, so the fresh-index path is byte-identical to before.
|
|
func (s synthFunc) synthesizeScoped(g graph.Store, scope map[string]bool) int {
|
|
if scope != nil && s.scopedFn != nil {
|
|
return s.scopedFn(g, scope)
|
|
}
|
|
return s.fn(g)
|
|
}
|
|
|
|
// defaultFrameworkSynthesizers returns the registered framework
|
|
// synthesizers in run order. Order is load-bearing: every synthesizer
|
|
// here runs after InferImplements/InferOverrides (some depend on the
|
|
// EdgeImplements edges they produce) and before DetectCrossRepoEdges (so
|
|
// a cross-repo synthesized call gets its parallel cross_repo_calls edge).
|
|
// Native-bridge resolvers append to this slice.
|
|
func defaultFrameworkSynthesizers() []FrameworkSynthesizer {
|
|
return []FrameworkSynthesizer{
|
|
synthFunc{name: SynthGRPCStub, fn: ResolveGRPCStubCalls},
|
|
synthFunc{name: SynthTemporalStub, fn: ResolveTemporalCalls},
|
|
synthFunc{name: SynthEventChannel, fn: ResolveEventChannelCalls},
|
|
synthFunc{name: SynthSwiftObjC, fn: ResolveSwiftObjCBridge},
|
|
synthFunc{name: SynthReactNative, fn: ResolveReactNativeBridge},
|
|
synthFunc{name: SynthReactNativePair, fn: ResolveReactNativeNativePairing},
|
|
synthFunc{name: SynthObserverChannel, fn: ResolveObserverChannelCalls},
|
|
synthFunc{name: SynthClosureCollection, fn: ResolveClosureCollectionCalls},
|
|
synthFunc{name: SynthReactSetState, fn: ResolveReactSetStateCalls},
|
|
synthFunc{name: SynthFlutterSetState, fn: ResolveFlutterSetStateCalls},
|
|
synthFunc{name: SynthKMPExpectActual, fn: ResolveKMPExpectActual},
|
|
synthFunc{name: SynthExpoModules, fn: ResolveExpoModuleBridge},
|
|
synthFunc{name: SynthFabric, fn: ResolveFabricComponents},
|
|
synthFunc{name: SynthMyBatis, fn: ResolveMyBatisCalls},
|
|
synthFunc{name: SynthSQLCallsite, fn: ResolveSQLCallsites},
|
|
// Store-factory (Zustand/Redux/Pinia/MobX) indirect action calls —
|
|
// binds getState()-chain and destructured calls to the action node.
|
|
synthFunc{name: SynthStoreFactory, fn: ResolveStoreFactoryCalls},
|
|
// Redux Toolkit createAsyncThunk dispatch chains: a thunk →
|
|
// each action/thunk it dispatches from its payload-creator body.
|
|
// After store-factory so its action nodes are indexed for the
|
|
// thunk → reducer cross-link.
|
|
synthFunc{name: SynthReduxThunk, fn: ResolveReduxThunkCalls},
|
|
// NgRx effects: a createEffect(() => actions$.pipe(ofType(X))) effect ->
|
|
// the action X it reacts to. After the store/thunk passes so action
|
|
// creator nodes are indexed.
|
|
synthFunc{name: SynthNgRxEffect, fn: ResolveNgRxEffects},
|
|
// Object-literal command/handler registry dispatch →
|
|
// `new registry[key]().execute()`. Runs before the speculative
|
|
// pass so a claimed dispatch site suppresses the hidden best-guess.
|
|
synthFunc{name: SynthObjectRegistry, fn: ResolveObjectRegistryCalls},
|
|
// RTK Query generated-hook → createApi endpoint, and component →
|
|
// generated hook. Typed tier: the hook naming is RTK-contractual.
|
|
synthFunc{name: SynthRTKQuery, fn: ResolveRTKQueryCalls},
|
|
// Vuex string-keyed dispatch/commit → action/mutation, with
|
|
// module-namespace disambiguation.
|
|
synthFunc{name: SynthVuexDispatch, fn: ResolveVuexDispatchCalls},
|
|
// Celery task dispatch: `task.delay()` / `send_task("name")` →
|
|
// the decorator-gated task function. Typed tier.
|
|
synthFunc{name: SynthCelery, fn: ResolveCeleryCalls},
|
|
// Spring application events: publishEvent(new X()) → every
|
|
// @EventListener / ApplicationListener<X>, type-keyed fan-out.
|
|
synthFunc{name: SynthSpringEvent, fn: ResolveSpringEventCalls},
|
|
// MediatR CQRS dispatch: Send(new X()) → the IRequestHandler<X>
|
|
// Handle, Publish(new X()) → every INotificationHandler<X>.
|
|
synthFunc{name: SynthMediatR, fn: ResolveMediatRCalls},
|
|
// C# member-level interface dispatch: a call bound to an interface
|
|
// member fans out to the same-named member on each in-repo
|
|
// implementation, at the ast_inferred tier so it rides in the default
|
|
// find_usages / get_callers result. After the implements-producing
|
|
// passes so the impl fan-out is complete.
|
|
synthFunc{name: SynthCSharpIfaceDispatch, fn: ResolveCSharpInterfaceDispatch},
|
|
// Sidekiq job dispatch: Worker.perform_async(...) → the worker's
|
|
// perform, namespace-aware. Include-gated, typed tier.
|
|
synthFunc{name: SynthSidekiq, fn: ResolveSidekiqCalls},
|
|
// Laravel events: event(new X()) / X::dispatch() → every listener
|
|
// handle(X), from the Listeners convention and the $listen map.
|
|
synthFunc{name: SynthLaravelEvent, fn: ResolveLaravelEventCalls},
|
|
// C/C++ function-pointer dispatch: a fn registered into a struct's
|
|
// fn-pointer field → the indirect recv->field() call, keyed by
|
|
// (struct type, field) with a field-copy fixpoint.
|
|
synthFunc{name: SynthFnPointerDispatch, fn: ResolveFnPointerDispatch},
|
|
// C/C++ function-like macro expansion: a macro invocation
|
|
// `CALL_M(o)` → each call hidden in the macro's replacement list,
|
|
// attributed to the use-site line so a forward call walk shows the
|
|
// call where the macro is invoked, not at its `#define`.
|
|
synthFunc{name: SynthMacroExpansion, fn: ResolveMacroExpansionCalls},
|
|
// Gin middleware-chain dispatcher → registered handlers. Bridges the
|
|
// `c.handlers[idx](c)` indirection so ServeHTTP→handler reachability
|
|
// flows; repo-scoped, gated on a dispatcher existing.
|
|
synthFunc{name: SynthGinMiddleware, fn: ResolveGinMiddlewareCalls},
|
|
// Express named-handler resolution: middleware idents and
|
|
// XController.method args bound by directory convention.
|
|
synthFunc{name: SynthExpressResolve, fn: ResolveExpressHandlers},
|
|
// React custom-hook / context resolution: a `useAuth()` call binds to
|
|
// its /hooks/ definition; a `*Context`/`*Provider` reference binds to
|
|
// /context/ or /providers/, with the suffix-strip fallback.
|
|
synthFunc{name: SynthReactResolve, fn: ResolveReactHooksContext},
|
|
// FastAPI dependency / router fallback: a residual `Depends(get_db)`
|
|
// binds to a /dependencies/ provider, an `include_router(api_router)`
|
|
// to a /routers/ definition — only when reference resolution left the
|
|
// target unresolved.
|
|
synthFunc{name: SynthFastAPIResolve, fn: ResolveFastAPIDeps},
|
|
// Rails receiver-constant resolution: a `UserService.perform` /
|
|
// `User.find` / `ApplicationHelper.fmt` call binds to the directory-
|
|
// located service / model / helper definition named by its receiver.
|
|
synthFunc{name: SynthRailsResolve, fn: ResolveRailsRefs},
|
|
// SwiftUI directory-convention fallback: a residual `*View` /
|
|
// `*ViewModel` / `*Store` / `*Manager` / PascalCase-model reference
|
|
// binds to its /Views/ /ViewModels/ /Stores/ /Models/ definition.
|
|
synthFunc{name: SynthSwiftUIResolve, fn: ResolveSwiftUIRefs},
|
|
// UIKit directory-convention fallback: a residual `*ViewController` /
|
|
// `*Cell` / `*Delegate` / `*DataSource` reference binds to its
|
|
// /ViewControllers/ /Cells/ /Delegates/ definition.
|
|
synthFunc{name: SynthUIKitResolve, fn: ResolveUIKitRefs},
|
|
// Vapor directory-convention fallback: a residual `*Controller` /
|
|
// `*Middleware` reference binds to its /Controllers/ /Middleware/
|
|
// definition. After UIKit so `*ViewController` binds there first.
|
|
synthFunc{name: SynthVaporResolve, fn: ResolveVaporRefs},
|
|
// GoFrame reflective route → controller method, joined by the
|
|
// method's request-struct type rather than its name.
|
|
synthFunc{name: SynthGoFrameRoute, fn: ResolveGoFrameRoutes},
|
|
// SvelteKit +page ↔ +page.server load pairing: a route's page component
|
|
// reaches its server data loader so a trace flows page→load. Repo-scoped.
|
|
synthFunc{name: SynthSvelteKitLoad, fn: ResolveSvelteKitLoad},
|
|
// Rust impl-block / self-receiver / module-path resolution
|
|
// completion. Runs in the same settle window so residual
|
|
// unresolved Rust calls land before external-call synthesis
|
|
// classifies the rest as external.
|
|
synthFunc{name: SynthRustScope, fn: ResolveRustScopeCalls},
|
|
// After rust-scope and the implements/extends-producing passes so the
|
|
// cross-file factory-chain walk + conformance hop see settled edges.
|
|
synthFunc{name: SynthFactoryChain, fn: ResolveFactoryChains},
|
|
// Function-as-value callback registration — binds each captured
|
|
// value-position function identifier to its same-file definition and
|
|
// drops unbound candidates. The per-language capture feeds it via
|
|
// placeholder edges; the pass is inert until those land.
|
|
synthFunc{name: SynthFnValue, fn: ResolveFnValueCallbacks, scopedFn: ResolveFnValueCallbacksScoped},
|
|
// Pascal unit ↔ form (.pas/.dfm) pairing by same-dir basename.
|
|
synthFunc{name: SynthPascalFormName, fn: ResolvePascalForms},
|
|
// Same-file distinctive value references → EdgeReads to the constant,
|
|
// so a config constant's blast radius reaches every reader.
|
|
synthFunc{name: SynthValueRefName, fn: ResolveValueRefs, scopedFn: ResolveValueRefsScoped},
|
|
}
|
|
}
|
|
|
|
// SynthCount is the per-synthesizer result row in a FrameworkSynthReport.
|
|
type SynthCount struct {
|
|
Name string `json:"name"`
|
|
Edges int `json:"edges"`
|
|
// Millis is how long this synthesizer's Synthesize call took. Named
|
|
// passes that land 0 edges are not free — many scan a shared edge/node
|
|
// kind across the whole graph before concluding there is nothing to
|
|
// bind — so this rides on every row, not just the ones with edges.
|
|
Millis int64 `json:"ms,omitempty"`
|
|
}
|
|
|
|
// FrameworkSynthReport is the aggregate result of one
|
|
// RunFrameworkSynthesizers invocation.
|
|
type FrameworkSynthReport struct {
|
|
Total int `json:"total"`
|
|
Per []SynthCount `json:"per_synthesizer"`
|
|
// Gated counts synthesized reference/import edges dropped by the
|
|
// cross-language-family gate (coincidental PascalCase collisions across
|
|
// two known, different families; bridge synthesizers are exempt).
|
|
Gated int `json:"gated_cross_family,omitempty"`
|
|
// ReceiverGated counts C# member-call edges demoted to the speculative
|
|
// tier because they attach to a same-named member of a type unrelated to
|
|
// the edge's receiver_type.
|
|
ReceiverGated int `json:"receiver_type_gated,omitempty"`
|
|
// GateMillis/ClaimMillis/DemoteMillis time the three tail passes that
|
|
// run once (not per-synthesizer) after the main loop, so a slow one
|
|
// doesn't hide behind the loop's aggregate elapsed.
|
|
GateMillis int64 `json:"gate_ms,omitempty"`
|
|
ClaimMillis int64 `json:"claim_ms,omitempty"`
|
|
DemoteMillis int64 `json:"demote_ms,omitempty"`
|
|
}
|
|
|
|
// scopedSynthesizer is the optional capability a FrameworkSynthesizer exposes
|
|
// when it can restrict its candidate scan to a changed-repo prefix set. The
|
|
// driver consults it only when a scope is armed; a synthesizer that does not
|
|
// implement it runs whole-graph, which is always correct.
|
|
type scopedSynthesizer interface {
|
|
synthesizeScoped(g graph.Store, scope map[string]bool) int
|
|
}
|
|
|
|
// RunFrameworkSynthesizers runs every registered framework synthesizer
|
|
// over g, in registration order, and returns the per-synthesizer and
|
|
// total landed-edge counts. A nil graph is a no-op.
|
|
func RunFrameworkSynthesizers(g graph.Store) FrameworkSynthReport {
|
|
return RunFrameworkSynthesizersScoped(g, nil)
|
|
}
|
|
|
|
// RunFrameworkSynthesizersScoped is RunFrameworkSynthesizers with an armed
|
|
// changed-repo scope: each synthesizer that implements scopedSynthesizer
|
|
// narrows its candidate scan to those repos, the rest run whole-graph. A nil
|
|
// scope runs every pass whole-graph, so the fresh-index / single-repo path is
|
|
// byte-identical to the pre-scoping behaviour. The claiming-resolver, family-
|
|
// gate and receiver-gate tail passes always run whole-graph — they reconcile
|
|
// the settled cross-repo call graph, not a per-repo candidate set.
|
|
func RunFrameworkSynthesizersScoped(g graph.Store, scope map[string]bool) FrameworkSynthReport {
|
|
rep := FrameworkSynthReport{}
|
|
if g == nil {
|
|
return rep
|
|
}
|
|
for _, s := range defaultFrameworkSynthesizers() {
|
|
start := time.Now()
|
|
var n int
|
|
if ss, ok := s.(scopedSynthesizer); ok {
|
|
n = ss.synthesizeScoped(g, scope)
|
|
} else {
|
|
n = s.Synthesize(g)
|
|
}
|
|
rep.Per = append(rep.Per, SynthCount{Name: s.Name(), Edges: n, Millis: time.Since(start).Milliseconds()})
|
|
rep.Total += n
|
|
}
|
|
// Drop coincidental cross-language-family reference/import results before
|
|
// the claiming resolvers run, so a gated edge cannot be mistaken for a
|
|
// resolved placeholder downstream. Bridge synthesizers are exempt.
|
|
gateStart := time.Now()
|
|
rep.Gated = applyFrameworkFamilyGate(g)
|
|
rep.GateMillis = time.Since(gateStart).Milliseconds()
|
|
// Claiming resolvers run last — after every framework synthesizer has
|
|
// had its chance to consume a pre-stamped placeholder, but before
|
|
// external-call synthesis classifies the residual unresolved refs as
|
|
// external. Reported in registration order for determinism.
|
|
claimStart := time.Now()
|
|
claimed := RunClaimingResolvers(g)
|
|
rep.ClaimMillis = time.Since(claimStart).Milliseconds()
|
|
for _, r := range defaultClaimingResolvers() {
|
|
n := claimed[r.Name()]
|
|
rep.Per = append(rep.Per, SynthCount{Name: r.Name(), Edges: n})
|
|
rep.Total += n
|
|
}
|
|
// Receiver-type gate runs last: it corrects (demotes) already-bound C#
|
|
// member calls, so it must see the settled call graph.
|
|
demoteStart := time.Now()
|
|
rep.ReceiverGated = demoteCSharpMisattributedMemberCalls(g)
|
|
rep.DemoteMillis = time.Since(demoteStart).Milliseconds()
|
|
return rep
|
|
}
|
|
|
|
// ClaimingResolver retroactively claims a residual unresolved reference —
|
|
// one naming no declared symbol — that the extractor could not pre-tag, and
|
|
// rewrites it to a framework-known target. This is the generic
|
|
// claimsReference hook: a resolver offers a cheap name-vocabulary pre-filter
|
|
// (Claims) and, when it wins, rebinds the edge (Resolve). It runs before
|
|
// external-call synthesis would otherwise discard the reference as external.
|
|
type ClaimingResolver interface {
|
|
// Name is the stable provenance label stamped on the rebound edge.
|
|
Name() string
|
|
// Claims reports whether this resolver wants the unresolved edge — a
|
|
// cheap pre-filter on the reference's vocabulary, no graph work.
|
|
Claims(e *graph.Edge) bool
|
|
// Resolve rebinds e.To to a concrete target, returning true on a hit.
|
|
Resolve(g graph.Store, e *graph.Edge) bool
|
|
}
|
|
|
|
// defaultClaimingResolvers returns the registered claiming resolvers, in
|
|
// offer order.
|
|
func defaultClaimingResolvers() []ClaimingResolver {
|
|
return []ClaimingResolver{
|
|
DjangoDescriptorResolver{},
|
|
}
|
|
}
|
|
|
|
// RunClaimingResolvers offers every residual unresolved EdgeCalls /
|
|
// EdgeReferences to the claiming resolvers; the first whose Claims pre-filter
|
|
// passes and whose Resolve lands a target wins. Returns the per-resolver
|
|
// count of claimed edges. Unresolved edges are collected before resolving so
|
|
// a resolver's ReindexEdges does not mutate a live iteration.
|
|
func RunClaimingResolvers(g graph.Store) map[string]int {
|
|
out := map[string]int{}
|
|
if g == nil {
|
|
return out
|
|
}
|
|
resolvers := defaultClaimingResolvers()
|
|
if len(resolvers) == 0 {
|
|
return out
|
|
}
|
|
var pending []*graph.Edge
|
|
for _, kind := range []graph.EdgeKind{graph.EdgeCalls, graph.EdgeReferences} {
|
|
for e := range g.EdgesByKind(kind) {
|
|
if e != nil && e.To != "" && graph.IsUnresolvedTarget(e.To) {
|
|
pending = append(pending, e)
|
|
}
|
|
}
|
|
}
|
|
for _, e := range pending {
|
|
for _, r := range resolvers {
|
|
if r.Claims(e) && r.Resolve(g, e) {
|
|
out[r.Name()]++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|