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
374 lines
12 KiB
Go
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
|
|
}
|