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
580 lines
19 KiB
Go
580 lines
19 KiB
Go
// Package dataflow implements the CPG-lite dataflow primitives
|
||
// surfaced by the flow_between and taint_paths MCP tools. It walks
|
||
// EdgeValueFlow / EdgeArgOf / EdgeReturnsTo edges over the live
|
||
// graph to answer two questions agents care about:
|
||
//
|
||
// 1. flow_between(source_id, sink_id, max_depth) — list every
|
||
// ranked path that connects two specific symbols. Used for
|
||
// refactor-safety questions ("if I change f's return type,
|
||
// every site that ultimately consumes that value") and bug
|
||
// investigation ("trace where this wrong value originated").
|
||
//
|
||
// 2. taint_paths(source_pattern, sink_pattern) — pattern-driven
|
||
// resolution of source / sink sets, then a flow_between for
|
||
// each candidate pair. Used for security-style queries
|
||
// ("every flow from os.Getenv to db.Query") and architectural
|
||
// audits ("every flow from request.Body to a logger").
|
||
//
|
||
// The traversal forms the smallest useful subset of Joern's CPG
|
||
// reachability primitives that the segment can ship inside an MCP
|
||
// surface — intra-procedural data dependence, captured at
|
||
// extraction time as EdgeValueFlow, plus inter-procedural binding
|
||
// at every call site, captured as EdgeArgOf / EdgeReturnsTo.
|
||
//
|
||
// Direction. flow_between always walks forward — out-edges of the
|
||
// current frontier — because every dataflow edge points in the
|
||
// direction of value movement: a value_flow goes source→consumer,
|
||
// an arg_of goes argument→callee param, and a returns_to goes
|
||
// callee→assignment. BFS over OutEdges therefore traces "where
|
||
// does this value go". Reverse flow ("where did this value come
|
||
// from") is a future addition; today the user can swap source
|
||
// and sink to walk the same edges from the other end.
|
||
package dataflow
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
|
||
"github.com/zzet/gortex/internal/graph"
|
||
)
|
||
|
||
// DefaultMaxDepth bounds how far BFS will look — each hop on a
|
||
// dataflow edge counts. Eight is empirically wide enough to cover
|
||
// most real handlers and security-relevant flows while keeping
|
||
// pathological "fully connected" graphs from blowing the response
|
||
// budget.
|
||
const DefaultMaxDepth = 8
|
||
|
||
// DefaultMaxPaths bounds how many distinct paths flow_between
|
||
// will return for a single (source, sink) pair. The handler ranks
|
||
// refinement-confirmed paths ahead of disproved (pruned) ones, then
|
||
// by length, then by edge-confidence, so the user gets the most
|
||
// plausible paths first.
|
||
const DefaultMaxPaths = 10
|
||
|
||
// EdgeStep is one hop along a flow path. It carries the edge kind,
|
||
// origin tier, and coarse tier label so the caller can distinguish a
|
||
// strong intra-procedural chain from a heuristic inter-procedural
|
||
// binding without recomputing the origin → tier mapping. Refined is
|
||
// stamped by the CFG-backed reaching-definitions refinement when the
|
||
// hop's endpoints are bindings of the same function:
|
||
// confirmed_intraprocedural (a def→use chain verifies the hop) or
|
||
// pruned (the source's definition is killed before the target on
|
||
// every path). Empty when the hop is out of refinement scope.
|
||
type EdgeStep struct {
|
||
From string `json:"from"`
|
||
To string `json:"to"`
|
||
Kind string `json:"kind"`
|
||
Origin string `json:"origin,omitempty"`
|
||
Tier string `json:"tier,omitempty"`
|
||
Refined string `json:"refined,omitempty"`
|
||
}
|
||
|
||
// Path is an ordered sequence of edge hops from a source node to
|
||
// a sink node. The IDs slice is the source-first node sequence and
|
||
// is always one longer than Edges. Confidence is a normalised
|
||
// 0-1 score derived from edge origin tiers — higher means a
|
||
// stronger end-to-end binding.
|
||
type Path struct {
|
||
IDs []string `json:"ids"`
|
||
Edges []EdgeStep `json:"edges"`
|
||
Confidence float64 `json:"confidence"`
|
||
}
|
||
|
||
// Length returns the number of hops in the path.
|
||
func (p Path) Length() int { return len(p.Edges) }
|
||
|
||
// Engine is the dataflow query backend. It holds a reference to
|
||
// the graph and exposes the two MCP-ready primitives. Concurrency-
|
||
// safe by virtue of relying only on graph.Store's read methods —
|
||
// unless a Refiner is attached (refiners cache per-function CFGs and
|
||
// are meant for a single query).
|
||
type Engine struct {
|
||
g graph.Store
|
||
refiner *Refiner
|
||
}
|
||
|
||
// New returns an engine backed by the given graph.
|
||
func New(g graph.Store) *Engine { return &Engine{g: g} }
|
||
|
||
// WithRefiner attaches a CFG-backed reaching-definitions refiner:
|
||
// FlowBetween / TaintPaths results get per-hop
|
||
// confirmed_intraprocedural / pruned markers where both hop
|
||
// endpoints are bindings of the same function. Returns the engine
|
||
// for chaining.
|
||
func (e *Engine) WithRefiner(r *Refiner) *Engine {
|
||
e.refiner = r
|
||
return e
|
||
}
|
||
|
||
// IsDataflowKind returns true for the three edge kinds the BFS
|
||
// traverses.
|
||
func IsDataflowKind(k graph.EdgeKind) bool {
|
||
switch k {
|
||
case graph.EdgeValueFlow, graph.EdgeArgOf, graph.EdgeReturnsTo:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// FlowBetween returns up to maxPaths shortest paths from sourceID
|
||
// to sinkID, walking forward over dataflow edges. Returns nil when
|
||
// no path exists within maxDepth hops.
|
||
//
|
||
// maxDepth and maxPaths are clamped to safe defaults when zero.
|
||
func (e *Engine) FlowBetween(sourceID, sinkID string, maxDepth, maxPaths int) []Path {
|
||
return e.FlowBetweenWithTier(sourceID, sinkID, maxDepth, maxPaths, "")
|
||
}
|
||
|
||
// FlowBetweenWithTier is FlowBetween with an additional provenance
|
||
// filter: edges whose backfilled Origin tier ranks below minTier are
|
||
// skipped during traversal, pruning entire branches that cannot
|
||
// produce a fully-resolved path. Empty minTier disables the filter
|
||
// (identical to FlowBetween). The per-step Tier label is stamped on
|
||
// every retained EdgeStep so callers do not need to recompute it.
|
||
func (e *Engine) FlowBetweenWithTier(sourceID, sinkID string, maxDepth, maxPaths int, minTier string) []Path {
|
||
if e == nil || e.g == nil || sourceID == "" || sinkID == "" {
|
||
return nil
|
||
}
|
||
if sourceID == sinkID {
|
||
return []Path{{IDs: []string{sourceID}, Confidence: 1}}
|
||
}
|
||
if maxDepth <= 0 {
|
||
maxDepth = DefaultMaxDepth
|
||
}
|
||
if maxPaths <= 0 {
|
||
maxPaths = DefaultMaxPaths
|
||
}
|
||
|
||
// DFS with depth bound. We track the current path's node set
|
||
// to prevent cycles. Memoisation by (node, depth) would help
|
||
// pathological graphs but the maxDepth cap is already
|
||
// effective on real codebases — the typical taint walk hits
|
||
// 4-6 hops before either landing on the sink or running out
|
||
// of frontier.
|
||
var paths []Path
|
||
visited := make(map[string]bool, 32)
|
||
stack := []string{sourceID}
|
||
steps := []EdgeStep{}
|
||
visited[sourceID] = true
|
||
|
||
var dfs func(nodeID string, depth int)
|
||
dfs = func(nodeID string, depth int) {
|
||
if len(paths) >= maxPaths {
|
||
return
|
||
}
|
||
if depth >= maxDepth {
|
||
return
|
||
}
|
||
out := e.g.GetOutEdges(nodeID)
|
||
for _, ed := range out {
|
||
if !IsDataflowKind(ed.Kind) {
|
||
continue
|
||
}
|
||
if visited[ed.To] {
|
||
continue
|
||
}
|
||
origin := edgeOrigin(ed)
|
||
if minTier != "" && !graph.MeetsMinTier(origin, minTier) {
|
||
continue
|
||
}
|
||
step := EdgeStep{
|
||
From: ed.From,
|
||
To: ed.To,
|
||
Kind: string(ed.Kind),
|
||
Origin: origin,
|
||
Tier: graph.ResolvedBy(origin),
|
||
}
|
||
if ed.To == sinkID {
|
||
ids := append([]string(nil), stack...)
|
||
ids = append(ids, ed.To)
|
||
edgeCopy := append([]EdgeStep(nil), steps...)
|
||
edgeCopy = append(edgeCopy, step)
|
||
paths = append(paths, Path{
|
||
IDs: ids,
|
||
Edges: edgeCopy,
|
||
Confidence: confidenceFromEdges(edgeCopy),
|
||
})
|
||
if len(paths) >= maxPaths {
|
||
return
|
||
}
|
||
continue
|
||
}
|
||
visited[ed.To] = true
|
||
stack = append(stack, ed.To)
|
||
steps = append(steps, step)
|
||
dfs(ed.To, depth+1)
|
||
stack = stack[:len(stack)-1]
|
||
steps = steps[:len(steps)-1]
|
||
delete(visited, ed.To)
|
||
}
|
||
}
|
||
dfs(sourceID, 0)
|
||
|
||
// CFG-backed refinement: judge same-function value_flow hops
|
||
// with reaching-definition chains before ranking, so pruned
|
||
// paths sink below confirmed ones.
|
||
if e.refiner != nil {
|
||
e.refiner.refinePaths(paths)
|
||
}
|
||
|
||
rankPaths(paths)
|
||
if len(paths) > maxPaths {
|
||
paths = paths[:maxPaths]
|
||
}
|
||
return paths
|
||
}
|
||
|
||
// edgeOrigin returns the stamped Origin on an edge, falling back to
|
||
// DefaultOriginFor when the field is empty so back-compat graphs
|
||
// (produced before Origin was a first-class field) still classify
|
||
// cleanly for filtering and tier surfacing.
|
||
func edgeOrigin(e *graph.Edge) string {
|
||
if e.Origin != "" {
|
||
return e.Origin
|
||
}
|
||
src, _ := e.Meta["semantic_source"].(string)
|
||
return graph.DefaultOriginFor(e.Kind, e.Confidence, src)
|
||
}
|
||
|
||
// rankPaths sorts in-place so the most plausible explanation comes
|
||
// first. A path the reaching-definitions refinement disproved (any
|
||
// pruned hop) always sinks below every non-pruned path, regardless of
|
||
// length — confidence demotion alone can't move a pruned hop past a
|
||
// shorter confirmed one when length is the primary key. Among paths
|
||
// of the same pruned status, shorter and then higher-confidence paths
|
||
// sort first.
|
||
func rankPaths(paths []Path) {
|
||
sort.SliceStable(paths, func(i, j int) bool {
|
||
pi, pj := hasPrunedHop(paths[i]), hasPrunedHop(paths[j])
|
||
if pi != pj {
|
||
return !pi // non-pruned paths rank ahead of pruned ones
|
||
}
|
||
if len(paths[i].Edges) != len(paths[j].Edges) {
|
||
return len(paths[i].Edges) < len(paths[j].Edges)
|
||
}
|
||
return paths[i].Confidence > paths[j].Confidence
|
||
})
|
||
}
|
||
|
||
// hasPrunedHop reports whether any hop on the path was disproved by
|
||
// the CFG-backed refinement.
|
||
func hasPrunedHop(p Path) bool {
|
||
for _, e := range p.Edges {
|
||
if e.Refined == RefinedPruned {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// confidenceFromEdges computes a normalised path confidence from
|
||
// the per-edge origin tiers. Each edge contributes a 0-1 score
|
||
// based on how well-grounded its kind / origin are; the path's
|
||
// score is the geometric mean (product) so a single weak edge
|
||
// drags the whole chain down — matching the agent intuition that
|
||
// a dataflow path is only as strong as its weakest hop.
|
||
func confidenceFromEdges(edges []EdgeStep) float64 {
|
||
if len(edges) == 0 {
|
||
return 1
|
||
}
|
||
prod := 1.0
|
||
for _, e := range edges {
|
||
prod *= confidenceFromOrigin(e.Origin, e.Kind)
|
||
}
|
||
return prod
|
||
}
|
||
|
||
func confidenceFromOrigin(origin, kind string) float64 {
|
||
// Delegates to the shared graph.EdgeTierScore so dataflow and callpath
|
||
// compute path confidence from one provenance→weight mapping.
|
||
return graph.EdgeTierScore(origin, graph.EdgeKind(kind))
|
||
}
|
||
|
||
// TaintPattern resolves a source / sink pattern against the live
|
||
// graph. Patterns are name-substrings (case-insensitive) by
|
||
// default; `exact:Name` switches to exact-name match; `path:dir/`
|
||
// scopes to nodes whose FilePath starts with that prefix combined
|
||
// with any name fragment after `::`.
|
||
type TaintPattern struct {
|
||
Raw string
|
||
NameMatch func(string) bool
|
||
PathMatch func(string) bool
|
||
KindMatch func(graph.NodeKind) bool
|
||
}
|
||
|
||
// ParsePattern compiles a string into a TaintPattern. Supported
|
||
// syntaxes:
|
||
//
|
||
// - "Name" → case-insensitive substring match on node name.
|
||
// - "exact:Name" → exact name match.
|
||
// - "path:dir/" → file path prefix match (any name).
|
||
// - "kind:function" → restrict to a particular node kind.
|
||
// - "name=… kind=…" → multi-clause AND form (space-separated).
|
||
//
|
||
// The clauses combine with AND; an empty pattern matches nothing.
|
||
func ParsePattern(raw string) TaintPattern {
|
||
p := TaintPattern{Raw: raw}
|
||
if strings.TrimSpace(raw) == "" {
|
||
return p
|
||
}
|
||
clauses := strings.Fields(raw)
|
||
for _, c := range clauses {
|
||
switch {
|
||
case strings.HasPrefix(c, "exact:"):
|
||
want := strings.TrimPrefix(c, "exact:")
|
||
p.NameMatch = chainName(p.NameMatch, func(name string) bool {
|
||
return name == want
|
||
})
|
||
case strings.HasPrefix(c, "name:"):
|
||
want := strings.ToLower(strings.TrimPrefix(c, "name:"))
|
||
p.NameMatch = chainName(p.NameMatch, func(name string) bool {
|
||
return strings.Contains(strings.ToLower(name), want)
|
||
})
|
||
case strings.HasPrefix(c, "name="):
|
||
want := strings.ToLower(strings.TrimPrefix(c, "name="))
|
||
p.NameMatch = chainName(p.NameMatch, func(name string) bool {
|
||
return strings.Contains(strings.ToLower(name), want)
|
||
})
|
||
case strings.HasPrefix(c, "path:"):
|
||
want := strings.TrimPrefix(c, "path:")
|
||
p.PathMatch = chainName(p.PathMatch, func(path string) bool {
|
||
return strings.HasPrefix(path, want)
|
||
})
|
||
case strings.HasPrefix(c, "kind:"):
|
||
wantKind := graph.NodeKind(strings.TrimPrefix(c, "kind:"))
|
||
p.KindMatch = chainKind(p.KindMatch, func(k graph.NodeKind) bool {
|
||
return k == wantKind
|
||
})
|
||
default:
|
||
// Bare token — case-insensitive substring on name.
|
||
want := strings.ToLower(c)
|
||
p.NameMatch = chainName(p.NameMatch, func(name string) bool {
|
||
return strings.Contains(strings.ToLower(name), want)
|
||
})
|
||
}
|
||
}
|
||
return p
|
||
}
|
||
|
||
func chainName(prev, next func(string) bool) func(string) bool {
|
||
if prev == nil {
|
||
return next
|
||
}
|
||
return func(s string) bool { return prev(s) && next(s) }
|
||
}
|
||
|
||
func chainKind(prev, next func(graph.NodeKind) bool) func(graph.NodeKind) bool {
|
||
if prev == nil {
|
||
return next
|
||
}
|
||
return func(k graph.NodeKind) bool { return prev(k) && next(k) }
|
||
}
|
||
|
||
// Empty reports whether the pattern matches nothing.
|
||
func (p TaintPattern) Empty() bool {
|
||
return p.Raw == "" || (p.NameMatch == nil && p.PathMatch == nil && p.KindMatch == nil)
|
||
}
|
||
|
||
// matches reports whether n satisfies the compiled clauses. All
|
||
// configured matchers must pass; absent matchers are skipped.
|
||
func (p TaintPattern) matches(n *graph.Node) bool {
|
||
if n == nil {
|
||
return false
|
||
}
|
||
if p.NameMatch != nil && !p.NameMatch(n.Name) {
|
||
return false
|
||
}
|
||
if p.PathMatch != nil && !p.PathMatch(n.FilePath) {
|
||
return false
|
||
}
|
||
if p.KindMatch != nil && !p.KindMatch(n.Kind) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// ResolveCandidates walks the graph and returns up to limit
|
||
// distinct symbol IDs whose nodes match the pattern. Returns the
|
||
// caller-friendly nodes themselves so MCP responses can include
|
||
// names + paths without a second lookup.
|
||
//
|
||
// The seed set is bounded by taintEligibleKinds — the fixed 8-kind
|
||
// allowlist (function/method/param/field/variable/constant/type/
|
||
// interface) that taintEligible enforces. Iterating the per-kind
|
||
// NodesByKind bucket of each lets the backend stream only those
|
||
// kinds instead of materialising the full node table;
|
||
// on a disk backend AllNodes() pulls ~70k rows per request just to land
|
||
// at a handful of taint candidates. Pattern post-filters (name /
|
||
// path / pattern-supplied kind) still run Go-side — they compose
|
||
// AND, can't be projected onto the bucket index efficiently, and
|
||
// the per-bucket population is already small.
|
||
func (e *Engine) ResolveCandidates(p TaintPattern, limit int) []*graph.Node {
|
||
if e == nil || e.g == nil || p.Empty() {
|
||
return nil
|
||
}
|
||
if limit <= 0 {
|
||
limit = 100
|
||
}
|
||
out := make([]*graph.Node, 0, 16)
|
||
for _, k := range taintEligibleKinds {
|
||
if len(out) >= limit {
|
||
break
|
||
}
|
||
for n := range e.g.NodesByKind(k) {
|
||
if n == nil {
|
||
continue
|
||
}
|
||
if !p.matches(n) {
|
||
continue
|
||
}
|
||
out = append(out, n)
|
||
if len(out) >= limit {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
sort.SliceStable(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
||
return out
|
||
}
|
||
|
||
// taintEligibleKinds is the seed-bucket allowlist of node kinds that
|
||
// could plausibly be a dataflow source or sink. Files / imports / pkg
|
||
// markers don't carry value semantics, so excluding them up front
|
||
// keeps the candidate set focused. Kept as a slice (not a set) so
|
||
// callers can iterate the NodesByKind bucket of each kind in a stable
|
||
// order.
|
||
var taintEligibleKinds = []graph.NodeKind{
|
||
graph.KindFunction, graph.KindMethod, graph.KindParam,
|
||
graph.KindField, graph.KindVariable, graph.KindConstant,
|
||
graph.KindType, graph.KindInterface,
|
||
}
|
||
|
||
// TaintFinding is one (source, sink) hit produced by TaintPaths.
|
||
// Paths is non-empty when at least one BFS path connects the two.
|
||
type TaintFinding struct {
|
||
Source *graph.Node `json:"source"`
|
||
Sink *graph.Node `json:"sink"`
|
||
Paths []Path `json:"paths"`
|
||
}
|
||
|
||
// TaintPaths resolves both patterns, then runs flow_between for
|
||
// each (source, sink) pair. Returns up to limit findings, sorted
|
||
// by best path confidence × shortest length.
|
||
//
|
||
// Role-aware expansion. Sources and sinks expand differently
|
||
// because the dataflow edges are directional. A "source" function
|
||
// produces values via its return — flow originates at the
|
||
// function node itself, which has incoming returns_to edges
|
||
// agents will walk forward from. A "sink" function consumes
|
||
// values via its parameters — flow terminates at the param nodes
|
||
// where arg_of lands. So when the sink pattern resolves to a
|
||
// function/method, we automatically include each declared
|
||
// parameter as an additional candidate. This matches the agent
|
||
// intuition that `name:DBQuery` for a sink means "every value
|
||
// that lands in any argument of DBQuery", not the function
|
||
// itself (which has no incoming dataflow).
|
||
func (e *Engine) TaintPaths(sourcePattern, sinkPattern TaintPattern, maxDepth, limit int) []TaintFinding {
|
||
return e.TaintPathsWithTier(sourcePattern, sinkPattern, maxDepth, limit, "")
|
||
}
|
||
|
||
// TaintPathsWithTier is TaintPaths with the same per-edge provenance
|
||
// filter as FlowBetweenWithTier; empty minTier preserves the legacy
|
||
// behavior.
|
||
func (e *Engine) TaintPathsWithTier(sourcePattern, sinkPattern TaintPattern, maxDepth, limit int, minTier string) []TaintFinding {
|
||
if e == nil || e.g == nil {
|
||
return nil
|
||
}
|
||
if maxDepth <= 0 {
|
||
maxDepth = DefaultMaxDepth
|
||
}
|
||
if limit <= 0 {
|
||
limit = 20
|
||
}
|
||
sources := e.ResolveCandidates(sourcePattern, 0)
|
||
rawSinks := e.ResolveCandidates(sinkPattern, 0)
|
||
sinks := e.expandSinkCandidates(rawSinks)
|
||
if len(sources) == 0 || len(sinks) == 0 {
|
||
return nil
|
||
}
|
||
var findings []TaintFinding
|
||
for _, src := range sources {
|
||
for _, sink := range sinks {
|
||
if src.ID == sink.ID {
|
||
continue
|
||
}
|
||
paths := e.FlowBetweenWithTier(src.ID, sink.ID, maxDepth, DefaultMaxPaths, minTier)
|
||
if len(paths) == 0 {
|
||
continue
|
||
}
|
||
findings = append(findings, TaintFinding{
|
||
Source: src,
|
||
Sink: sink,
|
||
Paths: paths,
|
||
})
|
||
}
|
||
}
|
||
sort.SliceStable(findings, func(i, j int) bool {
|
||
bi, bj := bestPath(findings[i].Paths), bestPath(findings[j].Paths)
|
||
if bi.Length() != bj.Length() {
|
||
return bi.Length() < bj.Length()
|
||
}
|
||
return bi.Confidence > bj.Confidence
|
||
})
|
||
if len(findings) > limit {
|
||
findings = findings[:limit]
|
||
}
|
||
return findings
|
||
}
|
||
|
||
// expandSinkCandidates appends every parameter node of every
|
||
// matched function/method, deduplicated by ID. Originals stay in
|
||
// the candidate set — sometimes a flow really does land on the
|
||
// function symbol itself (e.g., when a callee value is passed
|
||
// straight back through another return) and excluding it would
|
||
// hide that case.
|
||
func (e *Engine) expandSinkCandidates(raw []*graph.Node) []*graph.Node {
|
||
if len(raw) == 0 || e == nil || e.g == nil {
|
||
return raw
|
||
}
|
||
seen := make(map[string]struct{}, len(raw)*2)
|
||
out := make([]*graph.Node, 0, len(raw)*2)
|
||
add := func(n *graph.Node) {
|
||
if n == nil {
|
||
return
|
||
}
|
||
if _, ok := seen[n.ID]; ok {
|
||
return
|
||
}
|
||
seen[n.ID] = struct{}{}
|
||
out = append(out, n)
|
||
}
|
||
for _, n := range raw {
|
||
add(n)
|
||
if n.Kind != graph.KindFunction && n.Kind != graph.KindMethod {
|
||
continue
|
||
}
|
||
for _, edge := range e.g.GetInEdges(n.ID) {
|
||
if edge.Kind != graph.EdgeParamOf {
|
||
continue
|
||
}
|
||
pNode := e.g.GetNode(edge.From)
|
||
if pNode == nil || pNode.Kind != graph.KindParam {
|
||
continue
|
||
}
|
||
add(pNode)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// bestPath returns the lowest-cost path in a finding (assumed
|
||
// already rankPaths-sorted). Falls back to a zero-value when the
|
||
// finding has none.
|
||
func bestPath(paths []Path) Path {
|
||
if len(paths) == 0 {
|
||
return Path{}
|
||
}
|
||
return paths[0]
|
||
}
|