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

268 lines
9.1 KiB
Go

package languages
import (
"regexp"
"strings"
"sync"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
cgrammar "github.com/zzet/gortex/internal/parser/tsitter/c"
cppgrammar "github.com/zzet/gortex/internal/parser/tsitter/cpp"
)
// cMacroCallRe matches a call-like invocation `name(` inside a macro's
// replacement list. The captured identifier is the (possibly hidden)
// callee — write_log in `#define LOG(m) write_log(m)`.
var cMacroCallRe = regexp.MustCompile(`([A-Za-z_]\w*)\s*\(`)
// cKeywordsInMacroBody are C/C++ keywords that can syntactically precede
// a `(` in a replacement list but are never call targets.
var cKeywordsInMacroBody = map[string]bool{
"if": true, "for": true, "while": true, "switch": true, "return": true,
"sizeof": true, "alignof": true, "_Alignof": true, "defined": true,
"do": true, "else": true, "case": true, "static_cast": true,
"reinterpret_cast": true, "const_cast": true, "dynamic_cast": true,
"typeof": true, "decltype": true, "catch": true,
}
// emitCMacro emits a KindMacro node for a preproc_def / preproc_function_def
// node and, for function-like macros, the EdgeCalls its replacement list
// hides. defNode is the whole preproc_(function_)def node; isFunc selects
// the function-like shape (parameters + call recovery). lang is "c" or "cpp".
//
// The replacement list is a raw preproc_arg token (tree-sitter does not
// parse it as part of the enclosing file), so call recovery sub-parses
// the body with the C/C++ grammar and walks it for call_expression
// callees — plain `f()`, member `(o)->run()`, and qualified `ns::f()` —
// excluding the macro's own parameters and C/C++ keywords. A malformed
// body falls back to a regex scan. A call site like `SQ(2)` parses as an
// ordinary call_expression and already resolves against the macro by
// name, so caller -> macro -> body-call forms a two-hop path through the
// expansion.
func emitCMacro(defNode *sitter.Node, isFunc bool, filePath, fileID, lang string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
if defNode == nil {
return
}
var name, replacement string
var params []string
for i, _nc := 0, int(defNode.ChildCount()); i < _nc; i++ {
c := defNode.Child(i)
if c == nil {
continue
}
switch c.Type() {
case "identifier":
if name == "" {
name = c.Content(src)
}
case "preproc_params":
for j, _nc := 0, int(c.NamedChildCount()); j < _nc; j++ {
p := c.NamedChild(j)
if p != nil && p.Type() == "identifier" {
params = append(params, p.Content(src))
}
}
case "preproc_arg":
replacement = strings.TrimSpace(c.Content(src))
}
}
if name == "" {
return
}
id := filePath + "::" + name
if seen[id] {
return
}
seen[id] = true
line := int(defNode.StartPoint().Row) + 1
macroKind := "object"
if isFunc {
macroKind = "function"
}
meta := map[string]any{"macro_kind": macroKind}
if len(params) > 0 {
meta["params"] = params
}
if replacement != "" {
r := replacement
if len(r) > macroBodyMaxLen {
r = r[:macroBodyMaxLen]
}
meta["replacement"] = r
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindMacro, Name: name,
FilePath: filePath, StartLine: line, EndLine: int(defNode.EndPoint().Row) + 1,
Language: lang, Meta: meta,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: line,
})
// Recover macro-hidden calls from the replacement list.
if replacement == "" {
return
}
paramSet := make(map[string]bool, len(params))
for _, p := range params {
paramSet[p] = true
}
callSeen := make(map[string]bool)
for _, callee := range recoverMacroCallees(replacement, lang) {
if paramSet[callee] || cKeywordsInMacroBody[callee] || callSeen[callee] {
continue
}
callSeen[callee] = true
result.Edges = append(result.Edges, &graph.Edge{
From: id, To: "unresolved::" + callee,
Kind: graph.EdgeCalls, FilePath: filePath, Line: line,
Origin: graph.OriginASTInferred,
})
}
}
// macroBodyMaxLen bounds how much of a macro replacement list is both
// stored on the node and fed to the sub-parser. Macro bodies are almost
// always short; a body longer than this is pathological (e.g. a
// generated table) and is truncated for storage and scanned with the
// linear regex fallback rather than the tree-sitter parser, keeping
// recovery cheap and bounded.
const macroBodyMaxLen = 4096
// macroWrapPrefix / macroWrapSuffix wrap a replacement list in a minimal
// function body so an expression / statement fragment parses as a
// well-formed C/C++ translation unit. The leading newline before the
// closing `;}` keeps a trailing `//` line comment in the body from
// swallowing the terminator. Recovered names are read from the wrapped
// source, so the wrapper text never leaks into a callee name and no byte
// offset translation is needed.
const (
macroWrapPrefix = "void __gx_macro_probe(void){\n"
macroWrapSuffix = "\n;}"
)
// cMacroLang / cppMacroLang lazily build (once) the grammars used to
// sub-parse a macro body. GetLanguage allocates a fresh wrapper per call,
// so caching avoids an allocation on every function-like macro.
var (
cMacroLang = sync.OnceValue(cgrammar.GetLanguage)
cppMacroLang = sync.OnceValue(cppgrammar.GetLanguage)
)
// recoverMacroCallees returns the callee names hidden in a macro
// replacement list, in source order. It prefers a real tree-sitter
// sub-parse of the body (which recovers member calls `(o)->run()` and
// qualified calls `ns::f()` the regex cannot see) and falls back to the
// regex scan whenever the body cannot be parsed cleanly — a parse
// failure or any ERROR/MISSING node — so a malformed body never recovers
// fewer calls than the historical regex behaviour. Pathologically long
// bodies skip the parser and use the linear regex directly.
func recoverMacroCallees(replacement, lang string) []string {
if len(replacement) > macroBodyMaxLen {
return regexMacroCallees(replacement)
}
if names, ok := subparseMacroCallees(replacement, lang); ok {
return names
}
return regexMacroCallees(replacement)
}
// regexMacroCallees is the historical scan: every `name(` invocation in
// the body, in order, before parameter / keyword filtering (applied by
// the caller).
func regexMacroCallees(replacement string) []string {
matches := cMacroCallRe.FindAllStringSubmatch(replacement, -1)
out := make([]string, 0, len(matches))
for _, m := range matches {
out = append(out, m[1])
}
return out
}
// subparseMacroCallees parses the macro body with the C or C++ grammar
// (selected by the macro's file language) and walks the tree for
// call_expression callees. It returns (names, true) only on a clean
// parse; a parse error, an ERROR/MISSING node anywhere in the tree, or a
// panic from the parser yields (nil, false) so the caller falls back to
// the regex. parser.ParseFile owns parser-pool safety: it Closes a
// parser whose parse errored rather than recycling it, so a malformed
// body cannot poison a pooled parser.
func subparseMacroCallees(replacement, lang string) (names []string, ok bool) {
defer func() {
if r := recover(); r != nil {
names, ok = nil, false
}
}()
grammar := cMacroLang()
if lang == "cpp" {
grammar = cppMacroLang()
}
wrapped := []byte(macroWrapPrefix + replacement + macroWrapSuffix)
tree, err := parser.ParseFile(wrapped, grammar)
if err != nil {
return nil, false
}
defer tree.Close()
root := tree.RootNode()
if root == nil || root.HasError() {
return nil, false
}
walkMacroCalls(root, wrapped, &names)
return names, true
}
// walkMacroCalls appends the callee name of every call_expression in the
// subtree, in source order, descending into arguments so nested calls
// (`f(g())`) are all recovered.
func walkMacroCalls(n *sitter.Node, src []byte, out *[]string) {
if n == nil {
return
}
if n.Type() == "call_expression" {
if name := macroCalleeName(n, src); name != "" {
*out = append(*out, name)
}
}
for c := range n.NamedChildren() {
walkMacroCalls(c, src, out)
}
}
// macroCalleeName recovers the callee name from a call_expression's
// `function` child across the three shapes a macro body can hide it in:
// a plain identifier (`f()`), a field access (`(o)->run()` / `o.run()`),
// or a qualified name (`ns::f()`).
func macroCalleeName(call *sitter.Node, src []byte) string {
fn := call.ChildByFieldName("function")
if fn == nil {
return ""
}
switch fn.Type() {
case "identifier":
return fn.Content(src)
case "field_expression":
if field := fn.ChildByFieldName("field"); field != nil {
return field.Content(src)
}
case "qualified_identifier":
return rightmostQualifiedName(fn, src)
}
return ""
}
// rightmostQualifiedName returns the final segment of a (possibly nested)
// qualified_identifier — `deep` for `A::B::deep`, `f` for `ns::f`.
func rightmostQualifiedName(q *sitter.Node, src []byte) string {
cur := q
for cur != nil && cur.Type() == "qualified_identifier" {
cur = cur.ChildByFieldName("name")
}
if cur != nil && (cur.Type() == "identifier" || cur.Type() == "field_identifier") {
return cur.Content(src)
}
return ""
}