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

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 ""
}