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

374 lines
12 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/c"
)
// qCAll is a single tree-sitter query alternating over every pattern
// the C extractor needs. One tree walk per file replaces the 8
// `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.
const qCAll = `
[
(function_definition) @func.def
(declaration
declarator: (function_declarator
declarator: (identifier) @proto.name)) @proto.def
(declaration
declarator: (pointer_declarator
declarator: (function_declarator
declarator: (identifier) @proto.name))) @proto.def
(declaration
declarator: (pointer_declarator
declarator: (pointer_declarator
declarator: (function_declarator
declarator: (identifier) @proto.name)))) @proto.def
(struct_specifier
name: (type_identifier) @struct.name) @struct.def
(enum_specifier
name: (type_identifier) @enum.name) @enum.def
(type_definition
declarator: (type_identifier) @typedef.name) @typedef.def
(preproc_include
path: (_) @include.path) @include.def
(preproc_def
name: (identifier) @macro.name) @macro.def
(preproc_function_def
name: (identifier) @macrofn.name) @macrofn.def
(call_expression
function: (identifier) @call.name) @call.expr
(declaration
declarator: (init_declarator
declarator: (identifier) @var.name)) @var.def
]
`
// CExtractor extracts C source files into graph nodes and edges.
type CExtractor struct {
lang *sitter.Language
qAll *parser.PreparedQuery
}
func NewCExtractor() *CExtractor {
lang := c.GetLanguage()
return &CExtractor{
lang: lang,
qAll: parser.MustPreparedQuery(qCAll, lang),
}
}
func (e *CExtractor) Language() string { return "c" }
// Extensions includes ".def" — the conventional extension for a generated C
// fragment #include'd into a translation unit (an X-macro / command table like
// redis's src/commands.def). No other extractor claims ".def"; a non-C ".def"
// (a Windows linker module-definition file) degrades to tree-sitter ERROR nodes
// and emits nothing meaningful. ".inc" is contested (assembly / PHP / Pascal),
// so a clearly-C ".inc" fragment is routed to this extractor by content sniffing
// (sniffAmbiguous) instead of an extension claim.
func (e *CExtractor) Extensions() []string { return []string{".c", ".h", ".def"} }
// --- Deferred match buffers ----------------------------------------
type cDeferredCall struct {
name string
line int
}
type cDeferredVar struct {
name string
line int
endLine int
isConst bool
}
// cDeclIsConst reports whether a C declaration carries a leading `const` type
// qualifier — a file-scope `const int MAX = 10;` is a genuine named constant,
// not a mutable global. The capturing query only matches plain-identifier
// declarators, so pointer-to-const (`const char *p`, where the pointer itself
// stays mutable) never reaches here; a direct-child qualifier scan is exact.
func cDeclIsConst(decl *sitter.Node, src []byte) bool {
if decl == nil {
return false
}
for i, _nc := 0, int(decl.ChildCount()); i < _nc; i++ {
ch := decl.Child(i)
if ch != nil && ch.Type() == "type_qualifier" && ch.Content(src) == "const" {
return true
}
}
return false
}
func (e *CExtractor) 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: "c",
}
fileID := fileNode.ID
result.Nodes = append(result.Nodes, fileNode)
seen := make(map[string]bool)
var calls []cDeferredCall
var vars []cDeferredVar
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)
case m.Captures["proto.def"] != nil:
e.emitPrototype(m, filePath, fileID, result, seen)
case m.Captures["struct.def"] != nil:
e.emitKindType(m, "struct", filePath, fileID, result, seen)
case m.Captures["enum.def"] != nil:
e.emitKindType(m, "enum", filePath, fileID, result, seen)
case m.Captures["typedef.def"] != nil:
e.emitKindType(m, "typedef", filePath, fileID, result, seen)
case m.Captures["include.def"] != nil:
e.emitInclude(m, filePath, fileID, result)
case m.Captures["macro.def"] != nil:
emitCMacro(m.Captures["macro.def"].Node, false, filePath, fileID, "c", src, result, seen)
case m.Captures["macrofn.def"] != nil:
emitCMacro(m.Captures["macrofn.def"].Node, true, filePath, fileID, "c", src, result, seen)
case m.Captures["call.expr"] != nil:
expr := m.Captures["call.expr"]
calls = append(calls, cDeferredCall{
name: m.Captures["call.name"].Text,
line: expr.StartLine + 1,
})
case m.Captures["var.def"] != nil:
def := m.Captures["var.def"]
vars = append(vars, cDeferredVar{
name: m.Captures["var.name"].Text,
line: def.StartLine + 1,
endLine: def.EndLine + 1,
isConst: cDeclIsConst(def.Node, src),
})
}
})
// Globals and call edges both need funcRanges; build once.
funcRanges := buildFuncRanges(result)
for _, v := range vars {
// Skip locals inside function bodies.
if findEnclosingFunc(funcRanges, v.line) != "" {
continue
}
id := filePath + "::" + v.name
if seen[id] {
continue
}
seen[id] = true
kind := graph.KindVariable
if v.isConst {
// A file-scope const is a named constant — joins the value-ref
// impact surface alongside #define macros, not the mutable-global set.
kind = graph.KindConstant
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: kind, Name: v.name,
FilePath: filePath, StartLine: v.line, EndLine: v.endLine,
Language: "c",
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: v.line,
})
}
for _, c := range calls {
callerID := findEnclosingFunc(funcRanges, c.line)
if callerID == "" {
continue
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID, To: "unresolved::" + c.name,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
})
}
captureValueRefCandidates(result, root, filePath, src)
captureFnValueCandidates(result, root, filePath, src)
captureCFnPointerDispatch(result, root, filePath, src)
captureCFnAddressRefs(result, root, filePath, fileID, src)
return result, nil
}
// --- Per-match emit helpers -----------------------------------------
func (e *CExtractor) emitFunction(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
def := m.Captures["func.def"]
// The name lives at the bottom of the declarator chain. A plain
// `int f()` nests function_declarator directly, but a pointer-return
// `robj *f()` / `robj **f()` wraps it in one or two pointer_declarators
// (and a fn-pointer-returning signature adds a parenthesized_declarator).
// cDeclName peels every wrapper down to the identifier, so the extractor
// no longer drops pointer-return definitions on the floor.
name := cDeclName(def.Node.ChildByFieldName("declarator"), src)
if name == "" {
return
}
id := filePath + "::" + name
if seen[id] {
return
}
seen[id] = true
meta := map[string]any{
"signature": strings.TrimSpace(extractCSignature(def.Node, src)),
}
// File-local-static linkage detection. A C function declared
// `static` has translation-unit scope; the scope-aware resolver
// uses this stamp to prefer a same-file static candidate over a
// global candidate of the same name. Mirrors `resolver.MetaScopeStatic`.
if isCStaticFunction(def.Node, src) {
meta["scope_static"] = true
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindFunction, Name: name,
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
Language: "c", Meta: meta,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
}
func (e *CExtractor) emitPrototype(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult, seen map[string]bool) {
name := m.Captures["proto.name"].Text
def := m.Captures["proto.def"]
id := filePath + "::" + name
if seen[id] {
return
}
seen[id] = true
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindFunction, Name: name,
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
Language: "c", Meta: map[string]any{
"signature": strings.TrimSuffix(strings.TrimSpace(def.Text), ";"),
"prototype": true,
},
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
}
// emitKindType collapses struct/enum/typedef emission — all produce a
// KindType node with the same shape. The prefix selects the
// capture-name pair.
func (e *CExtractor) emitKindType(m parser.QueryResult, prefix, filePath, fileID string, result *parser.ExtractionResult, seen map[string]bool) {
name := m.Captures[prefix+".name"].Text
def := m.Captures[prefix+".def"]
id := filePath + "::" + name
if seen[id] {
return
}
seen[id] = true
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: "c", Meta: map[string]any{"type_flavor": prefix},
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
}
func (e *CExtractor) emitInclude(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult) {
pathCap := m.Captures["include.path"]
// `"foo.h"` is a local (quoted) include — resolvable relative to the
// including file; `<stdio.h>` is a system include. Recording which kind it
// is lets the resolver bind local includes to real file nodes and leave
// system headers external (codegraph treats both identically).
raw := strings.TrimSpace(pathCap.Text)
kind := "system"
if strings.HasPrefix(raw, `"`) {
kind = "quoted"
}
includePath := strings.Trim(strings.Trim(raw, `"`), "<>")
result.Edges = append(result.Edges, &graph.Edge{
From: fileID,
To: "unresolved::import::" + includePath,
Kind: graph.EdgeImports,
FilePath: filePath,
Line: pathCap.StartLine + 1,
Meta: map[string]any{"include_kind": kind},
})
}
// --- Helpers --------------------------------------------------------
// extractCSignature extracts a function signature from its definition node.
// It takes the text up to (but not including) the compound_statement body.
func extractCSignature(node *sitter.Node, src []byte) string {
fullText := node.Content(src)
// Find the opening brace of the function body and trim there.
idx := strings.Index(fullText, "{")
if idx > 0 {
return strings.TrimSpace(fullText[:idx])
}
return fullText
}
// isCStaticFunction reports whether the function_definition node was
// declared with C's `static` storage-class specifier (file-local
// linkage). Scans the declaration prefix before the parameter list —
// that's where `static` legally appears. Tolerates leading attributes
// and inline / extern keywords that may bracket `static`.
func isCStaticFunction(node *sitter.Node, src []byte) bool {
if node == nil {
return false
}
full := node.Content(src)
paren := strings.IndexByte(full, '(')
if paren < 0 {
return false
}
prefix := full[:paren]
for _, word := range strings.Fields(prefix) {
if word == "static" {
return true
}
}
return false
}