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
268 lines
9.1 KiB
Go
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 ""
|
|
}
|