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
1436 lines
49 KiB
Go
1436 lines
49 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
"github.com/zzet/gortex/internal/parser/tsitter/rust"
|
|
)
|
|
|
|
// qRustAll is a single tree-sitter query alternating over every pattern
|
|
// the Rust extractor needs. One tree walk per file replaces the 14
|
|
// `parser.RunQuery` calls the previous design made (each of which
|
|
// recompiled its query and ran an independent cursor over the whole
|
|
// tree). Capture names are disjoint across patterns so the dispatch in
|
|
// Extract can branch on which name is set. Impl/trait membership is
|
|
// resolved via a parent walk on the captured node rather than nested
|
|
// queries — same behaviour, one cursor pass.
|
|
const qRustAll = `
|
|
[
|
|
(function_item
|
|
name: (identifier) @func.name) @func.def
|
|
|
|
(function_signature_item
|
|
name: (identifier) @sig.name) @sig.def
|
|
|
|
(struct_item
|
|
name: (type_identifier) @struct.name) @struct.def
|
|
|
|
(enum_item
|
|
name: (type_identifier) @enum.name) @enum.def
|
|
|
|
(trait_item
|
|
name: (type_identifier) @trait.name) @trait.def
|
|
|
|
(enum_variant
|
|
name: (identifier) @variant.name) @variant.def
|
|
|
|
(field_declaration
|
|
name: (field_identifier) @field.name) @field.def
|
|
|
|
(const_item
|
|
name: (identifier) @const.name) @const.def
|
|
|
|
(static_item
|
|
name: (identifier) @static.name) @static.def
|
|
|
|
(use_declaration
|
|
argument: (_) @use.path) @use.def
|
|
|
|
(let_declaration
|
|
pattern: (identifier) @lvar.name
|
|
type: (_)? @lvar.type
|
|
value: (_)? @lvar.value) @lvar.def
|
|
|
|
(call_expression
|
|
function: (identifier) @call.name) @call.expr
|
|
|
|
(call_expression
|
|
function: (scoped_identifier
|
|
name: (identifier) @callp.name)) @callp.expr
|
|
|
|
(call_expression
|
|
function: (field_expression
|
|
value: (_) @callm.receiver
|
|
field: (field_identifier) @callm.method)) @callm.expr
|
|
]
|
|
`
|
|
|
|
// RustExtractor extracts Rust source files into graph nodes and edges.
|
|
type RustExtractor struct {
|
|
lang *sitter.Language
|
|
qAll *parser.PreparedQuery
|
|
}
|
|
|
|
func NewRustExtractor() *RustExtractor {
|
|
lang := rust.GetLanguage()
|
|
return &RustExtractor{
|
|
lang: lang,
|
|
qAll: parser.MustPreparedQuery(qRustAll, lang),
|
|
}
|
|
}
|
|
|
|
func (e *RustExtractor) Language() string { return "rust" }
|
|
func (e *RustExtractor) Extensions() []string { return []string{".rs"} }
|
|
|
|
// --- Deferred match buffers ----------------------------------------
|
|
|
|
type rustDeferredCall struct {
|
|
name string
|
|
receiver string // selector receiver text (empty for plain/path calls)
|
|
path string // full scoped_identifier text for path calls (e.g. "Foo::new", "crate::util::helper"); "" otherwise
|
|
line int
|
|
isSelector bool
|
|
// returnUsage is how the call site consumes the return value
|
|
// (graph.ReturnUsage* label), classified at capture time and
|
|
// stamped as edge Meta on the EdgeCalls emitted for this site.
|
|
returnUsage string
|
|
}
|
|
|
|
// rustDeferredLet buffers a let_declaration for the post-pass type-env
|
|
// build. Tier 0 (explicit type) is processed across all lets first;
|
|
// Tier 1 (`new()` / struct expression on the RHS) only fills in keys
|
|
// the explicit pass did not — same precedence the legacy two-query
|
|
// version produced.
|
|
type rustDeferredLet struct {
|
|
name string
|
|
explicit string // normalized type from explicit annotation, "" if none
|
|
typeRaw string // verbatim explicit-annotation source (pre-canonicalization), "" if none
|
|
typeLine int // 1-based line of the explicit annotation (0 if none)
|
|
value *sitter.Node // RHS expression node, or nil
|
|
}
|
|
|
|
// rustFuncDef buffers a function/method definition node for the post-emit
|
|
// per-function receiver-scope build (paramsByFunc).
|
|
type rustFuncDef struct {
|
|
node *sitter.Node
|
|
owner string // impl/trait type that self binds to; "" for free functions
|
|
line int // 1-based start line, mapped back to the emitted id via funcRanges
|
|
}
|
|
|
|
func (e *RustExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
|
|
tree, err := parser.ParseFile(src, e.lang)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tree.Close()
|
|
|
|
root := tree.RootNode()
|
|
result := &parser.ExtractionResult{}
|
|
|
|
fileNode := &graph.Node{
|
|
ID: filePath, Kind: graph.KindFile, Name: filePath,
|
|
FilePath: filePath, StartLine: 1, EndLine: int(root.EndPoint().Row) + 1,
|
|
Language: "rust",
|
|
}
|
|
fileID := fileNode.ID
|
|
result.Nodes = append(result.Nodes, fileNode)
|
|
|
|
seen := make(map[string]bool)
|
|
annotationSeen := make(map[string]bool)
|
|
traitMethods := make(map[string][]string) // trait name → declared method names
|
|
|
|
var calls []rustDeferredCall
|
|
var lets []rustDeferredLet
|
|
var funcDefs []rustFuncDef
|
|
|
|
parser.EachMatch(e.qAll, root, src, func(m parser.QueryResult) {
|
|
switch {
|
|
|
|
case m.Captures["func.def"] != nil:
|
|
e.emitFunction(m, filePath, fileID, src, result, seen, annotationSeen)
|
|
fd := m.Captures["func.def"]
|
|
owner := rustImplMethodReceiver(fd.Node, src)
|
|
if owner == "" {
|
|
owner = rustTraitMethodOwner(fd.Node, src)
|
|
}
|
|
funcDefs = append(funcDefs, rustFuncDef{node: fd.Node, owner: owner, line: fd.StartLine + 1})
|
|
|
|
case m.Captures["sig.def"] != nil:
|
|
e.recordTraitMethod(m, filePath, fileID, src, result, traitMethods, seen, annotationSeen)
|
|
sd := m.Captures["sig.def"]
|
|
funcDefs = append(funcDefs, rustFuncDef{node: sd.Node, owner: rustTraitMethodOwner(sd.Node, src), line: sd.StartLine + 1})
|
|
|
|
case m.Captures["struct.def"] != nil:
|
|
e.emitStruct(m, filePath, fileID, src, result, seen, annotationSeen)
|
|
|
|
case m.Captures["enum.def"] != nil:
|
|
e.emitEnum(m, filePath, fileID, src, result, seen, annotationSeen)
|
|
|
|
case m.Captures["trait.def"] != nil:
|
|
e.emitTrait(m, filePath, fileID, src, result, seen, annotationSeen, traitMethods)
|
|
|
|
case m.Captures["variant.def"] != nil:
|
|
e.emitVariant(m, filePath, src, result)
|
|
|
|
case m.Captures["field.def"] != nil:
|
|
e.emitField(m, filePath, src, result)
|
|
|
|
case m.Captures["const.def"] != nil:
|
|
e.emitNamed(m, "const", filePath, fileID, result, seen)
|
|
|
|
case m.Captures["static.def"] != nil:
|
|
e.emitNamed(m, "static", filePath, fileID, result, seen)
|
|
|
|
case m.Captures["use.def"] != nil:
|
|
e.emitUse(m, filePath, fileID, src, result)
|
|
|
|
case m.Captures["lvar.def"] != nil:
|
|
d := rustDeferredLet{
|
|
name: m.Captures["lvar.name"].Text,
|
|
}
|
|
if t, ok := m.Captures["lvar.type"]; ok {
|
|
d.explicit = normalizeRustTypeName(t.Text)
|
|
// Keep the verbatim annotation source + its line so the
|
|
// post-pass can emit a cross-file EdgeTypedAs from the
|
|
// enclosing function to the named type, mirroring the
|
|
// param/return function-shape emission. normalizeRustTypeName
|
|
// is lossy (drops wrappers the canonicalizer keeps), so we
|
|
// re-canonicalize from the raw text downstream.
|
|
d.typeRaw = t.Text
|
|
d.typeLine = t.StartLine + 1
|
|
}
|
|
if v, ok := m.Captures["lvar.value"]; ok && v.Node != nil {
|
|
d.value = v.Node
|
|
}
|
|
lets = append(lets, d)
|
|
|
|
case m.Captures["callm.expr"] != nil:
|
|
expr := m.Captures["callm.expr"]
|
|
calls = append(calls, rustDeferredCall{
|
|
name: m.Captures["callm.method"].Text,
|
|
receiver: m.Captures["callm.receiver"].Text,
|
|
line: expr.StartLine + 1,
|
|
isSelector: true,
|
|
returnUsage: classifyReturnUsage(expr.Node, src, rustReturnUsageSpec),
|
|
})
|
|
|
|
case m.Captures["callp.expr"] != nil:
|
|
expr := m.Captures["callp.expr"]
|
|
c := rustDeferredCall{
|
|
name: m.Captures["callp.name"].Text,
|
|
line: expr.StartLine + 1,
|
|
returnUsage: classifyReturnUsage(expr.Node, src, rustReturnUsageSpec),
|
|
}
|
|
// The query only captures the scoped_identifier's trailing
|
|
// segment, so the qualifier (Foo / Self / crate / super /
|
|
// module path) would otherwise be lost. Read the full
|
|
// `function` child text so the Rust scope resolver can bind
|
|
// `Type::method`, `Self::method`, and `crate::/super::/self::`
|
|
// module paths.
|
|
if expr.Node != nil {
|
|
if fn := expr.Node.ChildByFieldName("function"); fn != nil {
|
|
c.path = fn.Content(src)
|
|
}
|
|
}
|
|
calls = append(calls, c)
|
|
|
|
case m.Captures["call.expr"] != nil:
|
|
expr := m.Captures["call.expr"]
|
|
calls = append(calls, rustDeferredCall{
|
|
name: m.Captures["call.name"].Text,
|
|
line: expr.StartLine + 1,
|
|
returnUsage: classifyReturnUsage(expr.Node, src, rustReturnUsageSpec),
|
|
})
|
|
}
|
|
})
|
|
|
|
// Stamp trait method names onto trait nodes' Meta["methods"]. Some
|
|
// trait nodes were emitted before their function_signature_item
|
|
// children fired through the alternation, so we backfill at the end.
|
|
for _, n := range result.Nodes {
|
|
if n.Kind != graph.KindInterface {
|
|
continue
|
|
}
|
|
if methods, ok := traitMethods[n.Name]; ok {
|
|
if n.Meta == nil {
|
|
n.Meta = make(map[string]any)
|
|
}
|
|
n.Meta["methods"] = methods
|
|
}
|
|
}
|
|
|
|
// Build type environment in the legacy precedence:
|
|
// Tier 0 — explicit `let x: Type = ...` annotations (overwrite)
|
|
// Tier 1 — RHS inference (`StructExpr {...}`, `Type::new(...)`),
|
|
// only when Tier 0 didn't supply a type.
|
|
tenv := make(typeEnv)
|
|
for _, l := range lets {
|
|
if l.explicit != "" {
|
|
tenv[l.name] = l.explicit
|
|
}
|
|
}
|
|
for _, l := range lets {
|
|
if _, exists := tenv[l.name]; exists {
|
|
continue
|
|
}
|
|
if l.value == nil {
|
|
continue
|
|
}
|
|
if inferred := inferTypeFromRustExpr(l.value, src); inferred != "" {
|
|
tenv[l.name] = inferred
|
|
}
|
|
}
|
|
|
|
// All function/method nodes have been emitted; map call sites to
|
|
// their enclosing definition.
|
|
funcRanges := buildFuncRanges(result)
|
|
|
|
// Per-function receiver scope: map each function/method's parameters and
|
|
// self receiver to their types so a selector call on a parameter receiver
|
|
// (args.foo()) resolves — mirroring Go's paramsByFunc. Keyed by the
|
|
// enclosing function id via funcRanges.
|
|
paramsByFunc := make(map[string]map[string]string)
|
|
for _, fd := range funcDefs {
|
|
id := findEnclosingFunc(funcRanges, fd.line)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
if pm := rustFuncParamTypes(fd.node, fd.owner, src); len(pm) > 0 {
|
|
paramsByFunc[id] = pm
|
|
}
|
|
}
|
|
|
|
// Emit a cross-file type-usage edge for every `let x: Type = ...`
|
|
// binding annotation. Without this a type referenced only in a local
|
|
// binding (never in a param/return) is invisible to find_usages
|
|
// absent a language server. Attributed to the enclosing function
|
|
// (file node fallback), canonicalized to the bare named type, with
|
|
// primitives skipped — same shape as the param/return emission.
|
|
for _, l := range lets {
|
|
if l.typeRaw == "" {
|
|
continue
|
|
}
|
|
ownerID := findEnclosingFunc(funcRanges, l.typeLine)
|
|
if ownerID == "" {
|
|
ownerID = fileID
|
|
}
|
|
emitRustTypeUseEdges(ownerID, l.typeRaw, filePath, l.typeLine, result)
|
|
}
|
|
|
|
for _, c := range calls {
|
|
callerID := findEnclosingFunc(funcRanges, c.line)
|
|
if callerID == "" {
|
|
// A call outside any function body — e.g. inside a const/static
|
|
// initialiser (`const ALIASES = [alias(...), alias(...)]`).
|
|
// Attribute it to the file node instead of dropping the site, so
|
|
// find_usages still surfaces the call. Mirrors the let-binding
|
|
// type-use pass's file-node fallback.
|
|
callerID = fileID
|
|
}
|
|
if c.isSelector {
|
|
edge := &graph.Edge{
|
|
From: callerID, To: "unresolved::*." + c.name,
|
|
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
|
|
}
|
|
if recvType, ok := lookupRustRecvType(paramsByFunc, tenv, callerID, c.receiver); ok {
|
|
edge.Meta = map[string]any{"receiver_type": recvType}
|
|
} else if strings.Contains(c.receiver, ".") || strings.Contains(c.receiver, "(") {
|
|
stampFactoryChainReceiver(edge, c.receiver, resolveChainType(c.receiver, tenv, result))
|
|
}
|
|
// Record a self/Self receiver so the Rust scope resolver can
|
|
// bind a `self.method()` selector to the enclosing impl type
|
|
// without an explicit `receiver_type` hint. Only the
|
|
// self/Self receivers are stamped — other receivers carry no
|
|
// extra meta (their binding flows through receiver_type).
|
|
if c.receiver == "self" || c.receiver == "Self" {
|
|
if edge.Meta == nil {
|
|
edge.Meta = map[string]any{}
|
|
}
|
|
edge.Meta["rust_recv"] = c.receiver
|
|
} else if _, hasType := edge.Meta["receiver_type"]; !hasType &&
|
|
strings.HasPrefix(c.receiver, "self.") && !strings.ContainsAny(c.receiver, "()[]") {
|
|
// A field-access receiver rooted at self (self.config.line_term
|
|
// .method()). Its type can't be read from a single param/local,
|
|
// but the scope resolver can walk the struct field types from the
|
|
// enclosing impl type. Record the chain so it can.
|
|
if edge.Meta == nil {
|
|
edge.Meta = map[string]any{}
|
|
}
|
|
edge.Meta["rust_recv_expr"] = c.receiver
|
|
}
|
|
stampReturnUsage(edge, c.returnUsage)
|
|
result.Edges = append(result.Edges, edge)
|
|
continue
|
|
}
|
|
edge := &graph.Edge{
|
|
From: callerID, To: "unresolved::" + c.name,
|
|
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
|
|
}
|
|
// Preserve the full scoped path (e.g. "Foo::new",
|
|
// "crate::util::helper") for the Rust scope resolver — the
|
|
// extractor's call target keeps only the trailing segment.
|
|
if c.path != "" && strings.Contains(c.path, "::") {
|
|
edge.Meta = map[string]any{"rust_path": c.path}
|
|
}
|
|
stampReturnUsage(edge, c.returnUsage)
|
|
result.Edges = append(result.Edges, edge)
|
|
}
|
|
|
|
// Cross-language interop sentinel: when any function in this
|
|
// file carries the `#[wasm_bindgen]` attribute, stamp the file
|
|
// node so audit / porting queries can filter by it. The check
|
|
// runs as a post-pass against the already-emitted annotation
|
|
// edges, avoiding the need to thread fileNode through every
|
|
// emit helper that calls emitRustAnnotationEdges.
|
|
const wasmAnnotationID = "annotation::rust::wasm_bindgen"
|
|
for _, e := range result.Edges {
|
|
if e.Kind == graph.EdgeAnnotated && e.To == wasmAnnotationID {
|
|
if fileNode.Meta == nil {
|
|
fileNode.Meta = map[string]any{}
|
|
}
|
|
fileNode.Meta["uses_wasm_bindgen"] = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Emit the reference-form edges (construction, trait impls / bounds,
|
|
// casts, path / static access, derive attributes) the declaration-only
|
|
// type-use pass above does not cover. Owner attribution reuses the same
|
|
// funcRanges line map (file-node fallback).
|
|
emitRustReferenceForms(root, funcRanges, fileID, filePath, src, result)
|
|
|
|
captureValueRefCandidates(result, root, filePath, src)
|
|
captureFnValueCandidates(result, root, filePath, src)
|
|
return result, nil
|
|
}
|
|
|
|
// --- Per-match emit helpers -----------------------------------------
|
|
|
|
// emitFunction handles every function_item in the file, classifying it
|
|
// by its enclosing container:
|
|
// - direct child of an impl_item's body → KindMethod with receiver
|
|
// - direct child of a trait_item's body → KindFunction (default impl,
|
|
// legacy parity — the old extractor's class-method query did not
|
|
// match trait bodies, so these landed in the free-function pass)
|
|
// - anything else → free function
|
|
func (e *RustExtractor) emitFunction(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool) {
|
|
name := m.Captures["func.name"].Text
|
|
def := m.Captures["func.def"]
|
|
startLine1 := def.StartLine + 1
|
|
|
|
doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash)
|
|
visibility := rustVisibility(def.Node, src)
|
|
typeParams := rustTypeParams(def.Node, src)
|
|
complexity, cognitive, loopDepth := 0, 0, 0
|
|
if def.Node != nil {
|
|
if body := def.Node.ChildByFieldName("body"); body != nil {
|
|
complexity, cognitive, loopDepth = BodyComplexityMetrics(body, "rust")
|
|
}
|
|
}
|
|
|
|
implType := rustImplMethodReceiver(def.Node, src)
|
|
if implType != "" {
|
|
id, ok := disambiguateID(seen, filePath+"::"+implType+"."+name, startLine1)
|
|
if !ok {
|
|
return
|
|
}
|
|
meta := map[string]any{
|
|
"receiver": implType,
|
|
"signature": "fn " + name + "(...)",
|
|
"visibility": visibility,
|
|
}
|
|
if doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
if rt := extractRustReturnType(def.Node, src); rt != "" {
|
|
meta["return_type"] = rt
|
|
}
|
|
if len(typeParams) > 0 {
|
|
meta["type_params"] = typeParams
|
|
}
|
|
ApplyComplexityMeta(meta, complexity, cognitive, loopDepth)
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: name,
|
|
FilePath: filePath, StartLine: startLine1, EndLine: def.EndLine + 1,
|
|
Language: "rust", Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine1,
|
|
})
|
|
typeID := filePath + "::" + implType
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: id, To: typeID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: startLine1,
|
|
})
|
|
// `impl Trait for Type` methods override the trait declaration's
|
|
// method. Emit a method-level EdgeOverrides so find_implementations
|
|
// pairs them and find_usages can fan dispatch sites out through the
|
|
// trait — resolved (possibly cross-file) by the Rust scope pass. This
|
|
// covers impls on external types (io::Error) that inferOverrides
|
|
// cannot, since inferOverrides needs both endpoints to be local type
|
|
// nodes and an external type has none.
|
|
if traitBase := rustImplTraitName(def.Node, src); traitBase != "" {
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: id, To: "unresolved::" + traitBase + "." + name,
|
|
Kind: graph.EdgeOverrides, FilePath: filePath, Line: startLine1,
|
|
})
|
|
}
|
|
emitRustAnnotationEdges(rustCollectAttributes(def.Node), id, filePath, src, result, annotationSeen)
|
|
emitRustThrowsEdges(def.Node, src, id, filePath, startLine1, result)
|
|
emitRustFunctionShape(id, def.Node, src, filePath, startLine1, result)
|
|
return
|
|
}
|
|
|
|
// A fn directly inside a trait body is a default-method declaration:
|
|
// emit it as <file>::<Trait>.<method> (KindMethod) like the impl case
|
|
// rather than a bare free function, so trait-dispatch resolution and
|
|
// find_implementations pairing see a consistent method node.
|
|
if traitName := rustTraitMethodOwner(def.Node, src); traitName != "" {
|
|
e.emitTraitMethodNode(traitName, name, def, filePath, fileID, src, result, seen, annotationSeen)
|
|
return
|
|
}
|
|
|
|
// Free function. Mirror the legacy rsQFunction pass — emit as KindFunction.
|
|
id, ok := disambiguateID(seen, filePath+"::"+name, startLine1)
|
|
if !ok {
|
|
return
|
|
}
|
|
meta := map[string]any{
|
|
"signature": "fn " + name + "(...)",
|
|
"visibility": visibility,
|
|
}
|
|
if doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
if len(typeParams) > 0 {
|
|
meta["type_params"] = typeParams
|
|
}
|
|
ApplyComplexityMeta(meta, complexity, cognitive, loopDepth)
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindFunction, Name: name,
|
|
FilePath: filePath, StartLine: startLine1, EndLine: def.EndLine + 1,
|
|
Language: "rust", Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine1,
|
|
})
|
|
emitRustAnnotationEdges(rustCollectAttributes(def.Node), id, filePath, src, result, annotationSeen)
|
|
emitRustThrowsEdges(def.Node, src, id, filePath, startLine1, result)
|
|
emitRustFunctionShape(id, def.Node, src, filePath, startLine1, result)
|
|
}
|
|
|
|
// rustTypeParams reads the `type_parameters` child of a Rust item
|
|
// (function_item, struct_item, enum_item, trait_item, impl_item) and
|
|
// returns each declared type parameter as {name, bound} where bound
|
|
// is optional. Multi-trait bounds (`T: Send + Sync`) are joined with
|
|
// " + ".
|
|
func rustTypeParams(item *sitter.Node, src []byte) []map[string]string {
|
|
if item == nil {
|
|
return nil
|
|
}
|
|
tps := item.ChildByFieldName("type_parameters")
|
|
if tps == nil {
|
|
// Some grammar versions don't expose the field name; fall
|
|
// back to a child-type scan.
|
|
for i, _nc := 0, int(item.ChildCount()); i < _nc; i++ {
|
|
c := item.Child(i)
|
|
if c != nil && c.Type() == "type_parameters" {
|
|
tps = c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if tps == nil {
|
|
return nil
|
|
}
|
|
var out []map[string]string
|
|
for i, _nc := 0, int(tps.NamedChildCount()); i < _nc; i++ {
|
|
tp := tps.NamedChild(i)
|
|
if tp == nil {
|
|
continue
|
|
}
|
|
// Lifetimes (`'a`) have their own node type; skip.
|
|
if tp.Type() == "lifetime" {
|
|
continue
|
|
}
|
|
// Plain identifier — `<T>`.
|
|
if tp.Type() == "type_identifier" {
|
|
out = append(out, map[string]string{"name": tp.Content(src)})
|
|
continue
|
|
}
|
|
// Constrained — `<T: Clone>` or `<T = u8>`. Older grammar
|
|
// versions emit these as `constrained_type_parameter`;
|
|
// newer ones flatten them under a `type_parameter` node.
|
|
entry := map[string]string{}
|
|
for j, _nc := 0, int(tp.ChildCount()); j < _nc; j++ {
|
|
c := tp.Child(j)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
switch c.Type() {
|
|
case "type_identifier":
|
|
if entry["name"] == "" {
|
|
entry["name"] = c.Content(src)
|
|
}
|
|
case "trait_bounds":
|
|
txt := strings.TrimSpace(c.Content(src))
|
|
txt = strings.TrimPrefix(txt, ":")
|
|
entry["bound"] = strings.TrimSpace(txt)
|
|
}
|
|
}
|
|
// As a final fallback for grammars that store the constraint
|
|
// as raw text alongside the identifier, try to parse the full
|
|
// child content if we got a name but no bound.
|
|
if entry["name"] != "" && entry["bound"] == "" {
|
|
text := strings.TrimSpace(tp.Content(src))
|
|
if i := strings.Index(text, ":"); i >= 0 {
|
|
entry["bound"] = strings.TrimSpace(text[i+1:])
|
|
}
|
|
}
|
|
if entry["name"] != "" {
|
|
out = append(out, entry)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// emitRustThrowsEdges inspects a function's return type for a Result
|
|
// wrapper and emits an EdgeThrows to the error type when found. Idiom:
|
|
//
|
|
// fn parse(s: &str) -> Result<i32, ParseError> {…} → throws ParseError
|
|
// fn open(p: &Path) -> Result<File, std::io::Error> {…} → throws Error
|
|
// fn no_error() -> i32 {…} → no edge
|
|
//
|
|
// `Origin: ASTInferred` because we're pattern-matching the return
|
|
// type text, not type-checking it.
|
|
func emitRustThrowsEdges(funcNode *sitter.Node, src []byte, fromID, filePath string, line int, result *parser.ExtractionResult) {
|
|
if funcNode == nil {
|
|
return
|
|
}
|
|
rt := rustRawReturnType(funcNode, src)
|
|
if rt == "" {
|
|
return
|
|
}
|
|
errType := rustErrorTypeFromResult(rt)
|
|
if errType == "" {
|
|
return
|
|
}
|
|
target := "unresolved::" + errType
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fromID,
|
|
To: target,
|
|
Kind: graph.EdgeThrows,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Origin: graph.OriginASTInferred,
|
|
})
|
|
}
|
|
|
|
// rustRawReturnType returns the verbatim return-type text of a Rust
|
|
// function_item, including generic parameters. Unlike
|
|
// extractRustReturnType, it does not normalize the type — preserves
|
|
// Result<T, E> shape so the throws extractor can read both arguments.
|
|
func rustRawReturnType(node *sitter.Node, src []byte) string {
|
|
if node == nil {
|
|
return ""
|
|
}
|
|
pastArrow := false
|
|
for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
text := string(src[child.StartByte():child.EndByte()])
|
|
if text == "->" {
|
|
pastArrow = true
|
|
continue
|
|
}
|
|
if pastArrow {
|
|
if child.Type() == "block" {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(text)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// rustErrorTypeFromResult parses a Rust return-type string and returns
|
|
// the trailing identifier of the error parameter when the type is a
|
|
// Result. Handles:
|
|
//
|
|
// Result<T, E> → E
|
|
// Result<T, std::io::E> → E (trailing ident)
|
|
// Result<T, Box<dyn E>> → E
|
|
// anyhow::Result<T> → "" (single-arg form: error type elided)
|
|
//
|
|
// Returns "" for non-Result returns or when the error type can't be
|
|
// extracted unambiguously.
|
|
func rustErrorTypeFromResult(rt string) string {
|
|
rt = strings.TrimSpace(rt)
|
|
// Strip leading qualifier like `std::result::` to land on `Result`.
|
|
if i := strings.LastIndex(rt, "::"); i >= 0 {
|
|
head := rt[:i]
|
|
// Only strip qualifier when what follows starts with Result.
|
|
if strings.HasPrefix(rt[i+2:], "Result<") {
|
|
_ = head
|
|
rt = rt[i+2:]
|
|
}
|
|
}
|
|
if !strings.HasPrefix(rt, "Result<") {
|
|
return ""
|
|
}
|
|
inner := strings.TrimPrefix(rt, "Result<")
|
|
inner = strings.TrimSuffix(inner, ">")
|
|
// Split on the top-level comma — depth tracking for nested
|
|
// generics like Result<Vec<T>, MyError>.
|
|
depth := 0
|
|
parts := []string{}
|
|
start := 0
|
|
for i, r := range inner {
|
|
switch r {
|
|
case '<':
|
|
depth++
|
|
case '>':
|
|
depth--
|
|
case ',':
|
|
if depth == 0 {
|
|
parts = append(parts, inner[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
}
|
|
parts = append(parts, inner[start:])
|
|
if len(parts) < 2 {
|
|
return ""
|
|
}
|
|
errPart := strings.TrimSpace(parts[1])
|
|
// Box<dyn ErrType> / Box<dyn ErrType + Send> → ErrType
|
|
for _, prefix := range []string{"Box<dyn ", "Box<", "Arc<dyn ", "Arc<"} {
|
|
if strings.HasPrefix(errPart, prefix) {
|
|
errPart = strings.TrimPrefix(errPart, prefix)
|
|
errPart = strings.TrimSuffix(errPart, ">")
|
|
break
|
|
}
|
|
}
|
|
if i := strings.Index(errPart, "+"); i >= 0 {
|
|
errPart = errPart[:i]
|
|
}
|
|
errPart = strings.TrimSpace(errPart)
|
|
// Strip qualifier like std::io::Error.
|
|
if i := strings.LastIndex(errPart, "::"); i >= 0 {
|
|
errPart = errPart[i+2:]
|
|
}
|
|
// Strip generic instantiation like Error<Foo>.
|
|
if i := strings.Index(errPart, "<"); i >= 0 {
|
|
errPart = errPart[:i]
|
|
}
|
|
errPart = strings.TrimSpace(errPart)
|
|
if errPart == "" || errPart == "_" {
|
|
return ""
|
|
}
|
|
return errPart
|
|
}
|
|
|
|
// rustCollectAttributes walks the previous siblings of an item node
|
|
// and returns each `attribute_item` (#[...]) attached to it. Walks
|
|
// stop on the first non-attribute sibling. Outer attributes are the
|
|
// only form we handle — inner attributes (`#![foo]`) don't apply to a
|
|
// specific item.
|
|
func rustCollectAttributes(item *sitter.Node) []*sitter.Node {
|
|
if item == nil {
|
|
return nil
|
|
}
|
|
var out []*sitter.Node
|
|
for sib := item.PrevSibling(); sib != nil; sib = sib.PrevSibling() {
|
|
if sib.Type() != "attribute_item" {
|
|
break
|
|
}
|
|
out = append(out, sib)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// emitRustAnnotationEdges turns a slice of attribute_item nodes into
|
|
// EdgeAnnotated edges. `#[derive(Trait1, Trait2)]` is expanded into
|
|
// one edge per trait — that's the form that lets agents query "find
|
|
// every type that derives Debug" with one hop.
|
|
func emitRustAnnotationEdges(attrs []*sitter.Node, fromID, filePath string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
|
|
for _, attr := range attrs {
|
|
var attrNode *sitter.Node
|
|
for i, _nc := 0, int(attr.NamedChildCount()); i < _nc; i++ {
|
|
c := attr.NamedChild(i)
|
|
if c != nil && c.Type() == "attribute" {
|
|
attrNode = c
|
|
break
|
|
}
|
|
}
|
|
if attrNode == nil {
|
|
continue
|
|
}
|
|
name, args := rustAttributeNameAndArgs(attrNode, src)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
line := int(attr.StartPoint().Row) + 1
|
|
if name == "derive" && args != "" {
|
|
for _, t := range strings.Split(args, ",") {
|
|
traitName := strings.TrimSpace(t)
|
|
if traitName != "" {
|
|
EmitAnnotationEdge(fromID, "rust", traitName, "", filePath, line, result, seen)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
EmitAnnotationEdge(fromID, "rust", name, args, filePath, line, result, seen)
|
|
}
|
|
}
|
|
|
|
// rustAttributeNameAndArgs reads an `attribute` AST node (the body of
|
|
// an attribute_item) and returns the attribute path's text plus any
|
|
// args inside the token_tree.
|
|
func rustAttributeNameAndArgs(attr *sitter.Node, src []byte) (string, string) {
|
|
if attr == nil {
|
|
return "", ""
|
|
}
|
|
var name, args string
|
|
for i, _nc := 0, int(attr.ChildCount()); i < _nc; i++ {
|
|
c := attr.Child(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
switch c.Type() {
|
|
case "identifier", "scoped_identifier", "path":
|
|
if name == "" {
|
|
name = c.Content(src)
|
|
}
|
|
case "token_tree":
|
|
txt := c.Content(src)
|
|
if len(txt) >= 2 && txt[0] == '(' && txt[len(txt)-1] == ')' {
|
|
txt = txt[1 : len(txt)-1]
|
|
}
|
|
args = txt
|
|
}
|
|
}
|
|
return name, args
|
|
}
|
|
|
|
// rustVisibility inspects an item node for a visibility_modifier child
|
|
// and returns the canonical visibility string. Default for items
|
|
// without a modifier is "private" (Rust default).
|
|
func rustVisibility(item *sitter.Node, src []byte) string {
|
|
if item == nil {
|
|
return VisibilityPrivate
|
|
}
|
|
for i, _nc := 0, int(item.ChildCount()); i < _nc; i++ {
|
|
c := item.Child(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if c.Type() != "visibility_modifier" {
|
|
continue
|
|
}
|
|
text := strings.TrimSpace(c.Content(src))
|
|
switch {
|
|
case text == "pub":
|
|
return VisibilityPublic
|
|
case strings.HasPrefix(text, "pub(crate"):
|
|
return VisibilityInternal
|
|
case strings.HasPrefix(text, "pub(super"), strings.HasPrefix(text, "pub(in"):
|
|
return VisibilityInternal
|
|
case strings.HasPrefix(text, "pub("):
|
|
return VisibilityPublic
|
|
}
|
|
}
|
|
return VisibilityPrivate
|
|
}
|
|
|
|
func (e *RustExtractor) recordTraitMethod(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, traitMethods map[string][]string, seen, annotationSeen map[string]bool) {
|
|
def := m.Captures["sig.def"]
|
|
traitNode := findEnclosingRustContainer(def.Node, "trait_item")
|
|
if traitNode == nil {
|
|
return
|
|
}
|
|
traitName := rustDeclName(traitNode, src)
|
|
if traitName == "" {
|
|
return
|
|
}
|
|
name := m.Captures["sig.name"].Text
|
|
traitMethods[traitName] = append(traitMethods[traitName], name)
|
|
// A dyn-dispatch call site binds to the trait *declaration*, so the
|
|
// signature needs its own <file>::<Trait>.<method> node for find_usages
|
|
// to answer and for inferOverrides to pair impl methods against it.
|
|
e.emitTraitMethodNode(traitName, name, def, filePath, fileID, src, result, seen, annotationSeen)
|
|
}
|
|
|
|
func (e *RustExtractor) emitStruct(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool) {
|
|
name := m.Captures["struct.name"].Text
|
|
def := m.Captures["struct.def"]
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
meta := map[string]any{"visibility": rustVisibility(def.Node, src)}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
meta["type_flavor"] = "struct"
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindType, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "rust",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
emitRustAnnotationEdges(rustCollectAttributes(def.Node), id, filePath, src, result, annotationSeen)
|
|
emitRustGenericParamNodes(id, def.Node, src, filePath, def.StartLine+1, result)
|
|
}
|
|
|
|
func (e *RustExtractor) emitEnum(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool) {
|
|
name := m.Captures["enum.name"].Text
|
|
def := m.Captures["enum.def"]
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
meta := map[string]any{
|
|
"kind": "enum",
|
|
"type_flavor": "enum",
|
|
"visibility": rustVisibility(def.Node, src),
|
|
}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindType, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "rust",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
emitRustAnnotationEdges(rustCollectAttributes(def.Node), id, filePath, src, result, annotationSeen)
|
|
emitRustGenericParamNodes(id, def.Node, src, filePath, def.StartLine+1, result)
|
|
}
|
|
|
|
func (e *RustExtractor) emitTrait(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool, traitMethods map[string][]string) {
|
|
name := m.Captures["trait.name"].Text
|
|
def := m.Captures["trait.def"]
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
meta := map[string]any{"visibility": rustVisibility(def.Node, src)}
|
|
if methods, ok := traitMethods[name]; ok {
|
|
meta["methods"] = methods
|
|
}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
meta["type_flavor"] = "trait"
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindInterface, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "rust", Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
emitRustAnnotationEdges(rustCollectAttributes(def.Node), id, filePath, src, result, annotationSeen)
|
|
emitRustGenericParamNodes(id, def.Node, src, filePath, def.StartLine+1, result)
|
|
}
|
|
|
|
func (e *RustExtractor) emitVariant(m parser.QueryResult, filePath string, src []byte, result *parser.ExtractionResult) {
|
|
def := m.Captures["variant.def"]
|
|
enumNode := findEnclosingRustContainer(def.Node, "enum_item")
|
|
if enumNode == nil {
|
|
return
|
|
}
|
|
enumName := rustDeclName(enumNode, src)
|
|
if enumName == "" {
|
|
return
|
|
}
|
|
variantName := m.Captures["variant.name"].Text
|
|
enumID := filePath + "::" + enumName
|
|
variantID := enumID + "." + variantName
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: variantID, Kind: graph.KindVariable, Name: variantName,
|
|
FilePath: filePath,
|
|
StartLine: def.StartLine + 1,
|
|
EndLine: def.EndLine + 1,
|
|
Language: "rust",
|
|
Meta: map[string]any{"receiver": enumName, "kind": "enum_variant"},
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: variantID, To: enumID, Kind: graph.EdgeMemberOf,
|
|
FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
}
|
|
|
|
func (e *RustExtractor) emitField(m parser.QueryResult, filePath string, src []byte, result *parser.ExtractionResult) {
|
|
def := m.Captures["field.def"]
|
|
// Legacy only emitted struct fields. tree-sitter-rust also produces
|
|
// field_declaration nodes inside union_item; skip those.
|
|
structNode := findEnclosingRustContainer(def.Node, "struct_item")
|
|
if structNode == nil {
|
|
return
|
|
}
|
|
structName := rustDeclName(structNode, src)
|
|
if structName == "" {
|
|
return
|
|
}
|
|
fieldName := m.Captures["field.name"].Text
|
|
structID := filePath + "::" + structName
|
|
fieldID := structID + "." + fieldName
|
|
meta := map[string]any{
|
|
"receiver": structName,
|
|
"visibility": rustVisibility(def.Node, src),
|
|
}
|
|
if t := def.Node.ChildByFieldName("type"); t != nil {
|
|
meta["field_type"] = strings.TrimSpace(t.Content(src))
|
|
}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: fieldID, Kind: graph.KindField, Name: fieldName,
|
|
FilePath: filePath,
|
|
StartLine: def.StartLine + 1,
|
|
EndLine: def.EndLine + 1,
|
|
Language: "rust",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fieldID, To: structID, Kind: graph.EdgeMemberOf,
|
|
FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
}
|
|
|
|
// emitNamed handles const_item / static_item — they share the same
|
|
// node shape so we collapse them into one helper that takes the
|
|
// capture-name prefix.
|
|
func (e *RustExtractor) emitNamed(m parser.QueryResult, kind, filePath, fileID string, result *parser.ExtractionResult, seen map[string]bool) {
|
|
nameCap := m.Captures[kind+".name"]
|
|
def := m.Captures[kind+".def"]
|
|
if nameCap == nil || def == nil {
|
|
return
|
|
}
|
|
name := nameCap.Text
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindVariable, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "rust",
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
}
|
|
|
|
func (e *RustExtractor) emitUse(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult) {
|
|
path := m.Captures["use.path"]
|
|
usePath := strings.ReplaceAll(path.Text, "::", "/")
|
|
edge := &graph.Edge{
|
|
From: fileID, To: "unresolved::import::" + usePath,
|
|
Kind: graph.EdgeImports, FilePath: filePath, Line: path.StartLine + 1,
|
|
}
|
|
// A `pub use` (or `pub(crate) use`) declaration is a re-export: the
|
|
// names it brings into scope become part of this module's public
|
|
// surface. Tag the import edge so the re-export chain follower can
|
|
// tell a transparent forward from a private `use`. A plain `use`
|
|
// carries no tag and never forwards a name to a downstream consumer.
|
|
if def := m.Captures["use.def"]; def != nil {
|
|
if v := rustVisibility(def.Node, src); v == VisibilityPublic || v == VisibilityInternal {
|
|
edge.Meta = map[string]any{"reexport": true}
|
|
}
|
|
}
|
|
result.Edges = append(result.Edges, edge)
|
|
}
|
|
|
|
// --- Helpers --------------------------------------------------------
|
|
|
|
// findEnclosingRustContainer walks the parent chain of n looking for
|
|
// the nearest ancestor whose Type() matches t. Returns nil if none.
|
|
func findEnclosingRustContainer(n *sitter.Node, t string) *sitter.Node {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
for p := n.Parent(); p != nil; p = p.Parent() {
|
|
if p.Type() == t {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// rustImplMethodReceiver returns the receiver type name when fn is a
|
|
// direct member of an impl_item's declaration_list, mirroring the
|
|
// legacy rsQImplMethod pattern. Returns "" for trait default impls,
|
|
// nested fns, and free functions — the legacy code only treated the
|
|
// direct-child case as a method.
|
|
func rustImplMethodReceiver(fn *sitter.Node, src []byte) string {
|
|
if fn == nil {
|
|
return ""
|
|
}
|
|
parent := fn.Parent()
|
|
if parent == nil || parent.Type() != "declaration_list" {
|
|
return ""
|
|
}
|
|
grand := parent.Parent()
|
|
if grand == nil || grand.Type() != "impl_item" {
|
|
return ""
|
|
}
|
|
typeNode := grand.ChildByFieldName("type")
|
|
if typeNode == nil {
|
|
return ""
|
|
}
|
|
return typeNode.Content(src)
|
|
}
|
|
|
|
// rustTraitMethodOwner returns the trait name when fn is a direct member
|
|
// of a trait_item's declaration_list — a trait-method declaration (a
|
|
// signature or a default-body method). Mirrors rustImplMethodReceiver but
|
|
// for trait bodies; returns "" for impl bodies, nested fns and free fns.
|
|
func rustTraitMethodOwner(fn *sitter.Node, src []byte) string {
|
|
if fn == nil {
|
|
return ""
|
|
}
|
|
parent := fn.Parent()
|
|
if parent == nil || parent.Type() != "declaration_list" {
|
|
return ""
|
|
}
|
|
grand := parent.Parent()
|
|
if grand == nil || grand.Type() != "trait_item" {
|
|
return ""
|
|
}
|
|
nameNode := grand.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return ""
|
|
}
|
|
return nameNode.Content(src)
|
|
}
|
|
|
|
// rustImplTraitName returns the base name of the trait a fn's enclosing
|
|
// impl block implements (`impl Trait for Type`), or "" when fn is not an
|
|
// impl method or the impl is inherent (`impl Type`). Unwraps scoped and
|
|
// generic trait paths (fmt::Display -> Display, Iterator<Item=u8> ->
|
|
// Iterator).
|
|
func rustImplTraitName(fn *sitter.Node, src []byte) string {
|
|
if fn == nil {
|
|
return ""
|
|
}
|
|
parent := fn.Parent()
|
|
if parent == nil || parent.Type() != "declaration_list" {
|
|
return ""
|
|
}
|
|
impl := parent.Parent()
|
|
if impl == nil || impl.Type() != "impl_item" {
|
|
return ""
|
|
}
|
|
tr := impl.ChildByFieldName("trait")
|
|
if tr == nil {
|
|
return ""
|
|
}
|
|
return rustTraitPathBaseName(tr.Content(src))
|
|
}
|
|
|
|
// rustTraitPathBaseName reduces a trait reference (possibly scoped and/or
|
|
// generic) to its base type name: `fmt::Display` -> `Display`,
|
|
// `Iterator<Item = u8>` -> `Iterator`.
|
|
func rustTraitPathBaseName(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
if i := strings.Index(s, "<"); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
if i := strings.LastIndex(s, "::"); i >= 0 {
|
|
s = s[i+2:]
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
// emitTraitMethodNode emits a KindMethod node for a trait-declaration
|
|
// method — a signature (function_signature_item, no body) or a default
|
|
// method (function_item with a body). Named <file>::<Trait>.<method> so
|
|
// the id scheme matches impl methods; EdgeMemberOf points at the trait's
|
|
// interface node so inferOverrides pairs each concrete impl method against
|
|
// its declaration. Meta["trait_decl"]="true" marks the node as a
|
|
// declaration rather than a concrete impl.
|
|
func (e *RustExtractor) emitTraitMethodNode(traitName, name string, def *parser.CapturedNode, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool) {
|
|
if def == nil || def.Node == nil {
|
|
return
|
|
}
|
|
startLine1 := def.StartLine + 1
|
|
id, ok := disambiguateID(seen, filePath+"::"+traitName+"."+name, startLine1)
|
|
if !ok {
|
|
return
|
|
}
|
|
complexity, cognitive, loopDepth := 0, 0, 0
|
|
if body := def.Node.ChildByFieldName("body"); body != nil {
|
|
complexity, cognitive, loopDepth = BodyComplexityMetrics(body, "rust")
|
|
}
|
|
meta := map[string]any{
|
|
"receiver": traitName,
|
|
"trait_decl": "true",
|
|
"signature": "fn " + name + "(...)",
|
|
"visibility": rustVisibility(def.Node, src),
|
|
}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
if rt := extractRustReturnType(def.Node, src); rt != "" {
|
|
meta["return_type"] = rt
|
|
}
|
|
if tps := rustTypeParams(def.Node, src); len(tps) > 0 {
|
|
meta["type_params"] = tps
|
|
}
|
|
ApplyComplexityMeta(meta, complexity, cognitive, loopDepth)
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: name,
|
|
FilePath: filePath, StartLine: startLine1, EndLine: def.EndLine + 1,
|
|
Language: "rust", Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine1,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: id, To: filePath + "::" + traitName, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: startLine1,
|
|
})
|
|
emitRustAnnotationEdges(rustCollectAttributes(def.Node), id, filePath, src, result, annotationSeen)
|
|
emitRustThrowsEdges(def.Node, src, id, filePath, startLine1, result)
|
|
emitRustFunctionShape(id, def.Node, src, filePath, startLine1, result)
|
|
}
|
|
|
|
// rustDeclName returns the source text of the `name` field on a Rust
|
|
// declaration node (struct_item, enum_item, trait_item, etc.).
|
|
func rustDeclName(decl *sitter.Node, src []byte) string {
|
|
if decl == nil {
|
|
return ""
|
|
}
|
|
nameNode := decl.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return ""
|
|
}
|
|
return nameNode.Content(src)
|
|
}
|
|
|
|
// extractRustReturnType walks a function_item node to find the return type after `->`.
|
|
func extractRustReturnType(node *sitter.Node, src []byte) string {
|
|
if node == nil {
|
|
return ""
|
|
}
|
|
// In tree-sitter-rust, function_item has children: fn, name, parameters, ->, type, block.
|
|
// Look for a type child after "->".
|
|
pastArrow := false
|
|
for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ {
|
|
child := node.Child(i)
|
|
text := string(src[child.StartByte():child.EndByte()])
|
|
if text == "->" {
|
|
pastArrow = true
|
|
continue
|
|
}
|
|
if pastArrow {
|
|
if child.Type() == "block" {
|
|
return ""
|
|
}
|
|
// This should be the return type node.
|
|
rawType := string(src[child.StartByte():child.EndByte()])
|
|
if rt := normalizeRustTypeName(rawType); rt != "" {
|
|
return rt
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// normalizeRustTypeName strips references, generics, and module paths from a Rust type.
|
|
func normalizeRustTypeName(t string) string {
|
|
t = strings.TrimSpace(t)
|
|
// Remove reference prefixes.
|
|
t = strings.TrimPrefix(t, "&mut ")
|
|
t = strings.TrimPrefix(t, "&")
|
|
// Remove generics.
|
|
if idx := strings.Index(t, "<"); idx > 0 {
|
|
t = t[:idx]
|
|
}
|
|
// Take last segment of module path.
|
|
if idx := strings.LastIndex(t, "::"); idx >= 0 {
|
|
t = t[idx+2:]
|
|
}
|
|
// Skip primitives.
|
|
switch t {
|
|
case "i8", "i16", "i32", "i64", "i128", "isize",
|
|
"u8", "u16", "u32", "u64", "u128", "usize",
|
|
"f32", "f64", "bool", "char", "str", "String",
|
|
"Self", "self":
|
|
return ""
|
|
}
|
|
if t == "" || (t[0] >= 'a' && t[0] <= 'z') {
|
|
return ""
|
|
}
|
|
return t
|
|
}
|
|
|
|
// rustFuncParamTypes returns a map from parameter name to its normalized,
|
|
// non-primitive type for a function/method node. A self parameter binds to
|
|
// ownerType (the enclosing impl/trait type) when known. Mirrors Go's
|
|
// paramsByFunc so a selector call on a parameter or self receiver resolves.
|
|
func rustFuncParamTypes(fn *sitter.Node, ownerType string, src []byte) map[string]string {
|
|
if fn == nil {
|
|
return nil
|
|
}
|
|
params := fn.ChildByFieldName("parameters")
|
|
if params == nil {
|
|
return nil
|
|
}
|
|
out := map[string]string{}
|
|
for i, n := 0, int(params.NamedChildCount()); i < n; i++ {
|
|
p := params.NamedChild(i)
|
|
if p == nil {
|
|
continue
|
|
}
|
|
switch p.Type() {
|
|
case "self_parameter":
|
|
if ownerType != "" {
|
|
out["self"] = ownerType
|
|
}
|
|
case "parameter":
|
|
pat := p.ChildByFieldName("pattern")
|
|
ty := p.ChildByFieldName("type")
|
|
if pat == nil || ty == nil {
|
|
continue
|
|
}
|
|
if t := normalizeRustTypeName(ty.Content(src)); t != "" {
|
|
out[pat.Content(src)] = t
|
|
}
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
// lookupRustRecvType resolves a selector-call receiver name to a type,
|
|
// checking the caller's own parameter/self scope before the file-wide let
|
|
// environment so a parameter shadows a same-named binding elsewhere.
|
|
func lookupRustRecvType(paramsByFunc map[string]map[string]string, tenv typeEnv, callerID, name string) (string, bool) {
|
|
if callerID != "" {
|
|
if scope, ok := paramsByFunc[callerID]; ok {
|
|
if t, ok := scope[name]; ok {
|
|
return t, true
|
|
}
|
|
}
|
|
}
|
|
t, ok := tenv[name]
|
|
return t, ok
|
|
}
|
|
|
|
// rustConcreteTypeName reports whether name can seed the local type
|
|
// environment from an associated-constructor qualifier as a resolvable,
|
|
// concrete type. It must be UpperCamelCase and name a real type, so a single
|
|
// uppercase letter (a generic parameter such as T/E/K, which owns no methods
|
|
// in the graph) and the `Self` placeholder (which names no node) are both
|
|
// rejected. A struct literal cannot name a generic parameter and so is guarded
|
|
// more loosely above; only here — the qualifier of a `T::ctor()` call — may a
|
|
// bare single letter really be a generic parameter, so the single-letter form
|
|
// is refused to keep an inferred receiver from ever pointing at a non-type.
|
|
func rustConcreteTypeName(name string) bool {
|
|
if len(name) < 2 || name == "Self" {
|
|
return false
|
|
}
|
|
return name[0] >= 'A' && name[0] <= 'Z'
|
|
}
|
|
|
|
// rustSelfReturningCtor reports whether an associated-function name is one of
|
|
// the conventional constructors that yields a value of the type it hangs off.
|
|
// Binding a let's inferred type to that qualifier is safe because idiomatic
|
|
// Rust reserves these names for constructors that return the owning type itself,
|
|
// unwrapped: `T::from`/`T::default` implement the From / Default traits
|
|
// (`fn(..) -> Self`), and `new`/`with_capacity`/`with_config`/`empty` are the
|
|
// near-universal inherent-constructor convention, each returning `Self`.
|
|
//
|
|
// Only names whose result IS the qualifier type belong here. Constructors that
|
|
// return a *wrapper* around Self are deliberately excluded: `from_str` and
|
|
// `open` yield a `Result<Self>` and `builder` a separate builder struct, so the
|
|
// binding's real type is that wrapper — not the qualifier. The downstream
|
|
// owner-match cannot recover from admitting them: it refuses to bind to a method
|
|
// the qualifier type does NOT own, but a wrapper-returning constructor whose
|
|
// qualifier happens to own a same-named method (a getter shadowing a builder's
|
|
// setter, say) would bind that call to the wrong owner rather than fall through
|
|
// to unresolved. Confining the list to genuinely Self-returning names is what
|
|
// keeps every seeded receiver pointing at the value actually in hand.
|
|
func rustSelfReturningCtor(method string) bool {
|
|
switch method {
|
|
case "new", "default", "from", "with_capacity", "with_config", "empty":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// inferTypeFromRustExpr inspects a tree-sitter expression node to infer
|
|
// the type of a let declaration's RHS.
|
|
func inferTypeFromRustExpr(node *sitter.Node, src []byte) string {
|
|
switch node.Type() {
|
|
case "struct_expression":
|
|
// Config { port: 8080 } — first named child is the type name. The
|
|
// literal constructs a value of exactly that type, so a selector call
|
|
// on the binding resolves against it. A struct literal can never name a
|
|
// generic parameter (Rust forbids `T { .. }`), so even a single-letter
|
|
// UpperCamelCase name is a concrete type here and is trusted — the
|
|
// single-letter rejection only guards the associated-call arm below,
|
|
// where the qualifier may instead be a generic parameter.
|
|
if node.NamedChildCount() > 0 {
|
|
name := node.NamedChild(0).Content(src)
|
|
// Strip module path.
|
|
if idx := strings.LastIndex(name, "::"); idx >= 0 {
|
|
name = name[idx+2:]
|
|
}
|
|
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
|
|
return name
|
|
}
|
|
}
|
|
|
|
case "call_expression":
|
|
// Type::<ctor>(..) — a scoped associated call whose qualifier names the
|
|
// constructed type. Only the conventional Self-returning constructors are
|
|
// trusted, and the qualifier must be a bare concrete type name, so the
|
|
// seeded binding can only point at the value the constructor produces —
|
|
// never at a lowercase module function, `Self`, or a generic parameter.
|
|
if node.NamedChildCount() > 0 {
|
|
funcNode := node.NamedChild(0)
|
|
if funcNode.Type() == "scoped_identifier" {
|
|
funcText := funcNode.Content(src)
|
|
// e.g. "Config::from" or "module::Config::with_capacity".
|
|
if idx := strings.LastIndex(funcText, "::"); idx > 0 {
|
|
method := funcText[idx+2:]
|
|
if rustSelfReturningCtor(method) {
|
|
typePart := funcText[:idx]
|
|
// Take last path segment as the type name.
|
|
if j := strings.LastIndex(typePart, "::"); j >= 0 {
|
|
typePart = typePart[j+2:]
|
|
}
|
|
if rustConcreteTypeName(typePart) {
|
|
return typePart
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|