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
329 lines
13 KiB
Go
329 lines
13 KiB
Go
package languages
|
|
|
|
import (
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// cFnAddressMaxPerFile fuses the per-file candidate count. A generated parser
|
|
// (a tree-sitter lexer, a bytecode dispatch table) presents tens of thousands
|
|
// of distinct value-position identifiers; unbounded, each becomes a
|
|
// speculative placeholder edge and the set explodes. A hand-written dispatch
|
|
// table sits far below this cap, so the fuse trips only on machine-generated
|
|
// input. Candidates are taken in walk order, so the cut is deterministic.
|
|
const cFnAddressMaxPerFile = 1024
|
|
|
|
// C cross-file function-address references.
|
|
//
|
|
// A function used as a *value* rather than called — a command-table macro
|
|
// argument (`{MAKE_CMD("get", ..., getCommand, ...)}`), a function-pointer
|
|
// comparison (`c->cmd->proc != execCommand`), a function-pointer assignment, an
|
|
// aggregate initializer element, or `&fn` — is a genuine reference the static
|
|
// call graph misses. C has a flat extern namespace, so the referenced function
|
|
// almost always lives in another translation unit; captureFnValueCandidates
|
|
// pre-filters to same-file functions and therefore drops these entirely.
|
|
//
|
|
// This pass captures the bare identifier in those value positions when the name
|
|
// is NOT declared in the current file (so it can only bind cross-module) and is
|
|
// not a parameter / local. It emits the same fn-value candidate the resolver
|
|
// gate already understands, marked ungated so ResolveFnValueCallbacks binds it
|
|
// to a uniquely-named, non-file-local function anywhere in the repo. Unlike the
|
|
// shared pass it attributes a file-scope reference (a command table lives
|
|
// outside any function) to the file node, so a table entry becomes a usage.
|
|
//
|
|
// Flood control matters most on generated C (a tree-sitter lexer, a bytecode
|
|
// dispatch table), where naive capture explodes. Bounds keep the pass to the
|
|
// small set of free identifiers a gate lookup can turn into a real cross-TU
|
|
// function edge:
|
|
//
|
|
// - value-position-only, and never a name the file declares (functions go to
|
|
// the gated pass; variables / constants / types resolve locally) or a
|
|
// parameter / local / uninitialised declaration / in-file enum member;
|
|
// - one candidate per (enclosing symbol or file, name): a function address
|
|
// binds by name repo-wide, so the same free identifier on N lines of a
|
|
// generated ==/!= lexer is one reference, not N;
|
|
// - a per-file fuse (cFnAddressMaxPerFile) bounds a pathological generated
|
|
// file; a hand-written dispatch table sits far below it.
|
|
func captureCFnAddressRefs(result *parser.ExtractionResult, root *sitter.Node, filePath, fileID string, src []byte) {
|
|
if root == nil || result == nil {
|
|
return
|
|
}
|
|
sameFileFunc := map[string]bool{}
|
|
localDecl := map[string]bool{}
|
|
for _, n := range result.Nodes {
|
|
if n == nil || n.FilePath != filePath {
|
|
continue
|
|
}
|
|
switch n.Kind {
|
|
case graph.KindFunction, graph.KindMethod:
|
|
sameFileFunc[n.Name] = true
|
|
case graph.KindVariable, graph.KindConstant, graph.KindType:
|
|
localDecl[n.Name] = true
|
|
}
|
|
}
|
|
shadowed := cCollectLocalNames(root, src)
|
|
|
|
funcRanges := buildFuncRanges(result)
|
|
seen := map[string]bool{}
|
|
var cands []FnValueCandidate
|
|
walkNodes(root, func(n *sitter.Node) {
|
|
if len(cands) >= cFnAddressMaxPerFile {
|
|
return // per-file fuse: bound a pathological generated file
|
|
}
|
|
if n.Type() != "identifier" {
|
|
return
|
|
}
|
|
form, ok := cFnAddressPosition(n)
|
|
if !ok {
|
|
return
|
|
}
|
|
name := n.Content(src)
|
|
if name == "" || isCFnAddressNonTarget(name) || isCValueRefNoise(name) {
|
|
return
|
|
}
|
|
// A name the file itself declares (function / variable / constant /
|
|
// type) or that is a parameter / local can never be a cross-TU
|
|
// function address — resolve it locally or not at all.
|
|
if sameFileFunc[name] || localDecl[name] || shadowed[name] {
|
|
return
|
|
}
|
|
// Defensive callee guard: positions above exclude the callee, but a
|
|
// macro-call argument that is itself a call (`f(g())`) must not treat
|
|
// the inner callee as a value.
|
|
if byteAfterIdentStartsCall(src, int(n.EndByte())) {
|
|
return
|
|
}
|
|
line := int(n.StartPoint().Row) + 1
|
|
from := findEnclosingFunc(funcRanges, line)
|
|
if from == "" {
|
|
from = fileID // file-scope reference (command / dispatch table)
|
|
}
|
|
// One candidate per (enclosing symbol or file, name): a function
|
|
// address binds by name repo-wide, so the same free identifier on N
|
|
// lines of a generated ==/!= lexer collapses to a single reference.
|
|
// First occurrence by walk order wins; mirrors the shared
|
|
// captureFnValueCandidates key shape.
|
|
key := from + "\x00" + name
|
|
if seen[key] {
|
|
return
|
|
}
|
|
seen[key] = true
|
|
cands = append(cands, FnValueCandidate{
|
|
FromID: from, Name: name, FilePath: filePath, Line: line,
|
|
Form: form, Lang: "c", Ungated: true,
|
|
})
|
|
})
|
|
EmitFnValueCandidates(result, cands)
|
|
}
|
|
|
|
// cFnAddressPosition reports whether an identifier node sits in a C value
|
|
// position that can hold a function address, and the fn_ref_form to tag ("" for
|
|
// a plain value, "address_of" for `&fn`). Positions cover the generated-table
|
|
// shape — a macro / call argument (`MAKE_CMD(..., getCommand, ...)`), an
|
|
// aggregate initializer element (`{ ..., getCommand }`), a designated
|
|
// initializer value (`{ .proc = getCommand }`) — and the in-function
|
|
// function-pointer idioms: a `==` / `!=` comparison (`c->cmd->proc !=
|
|
// execCommand`), an assignment or declaration-initializer right-hand side
|
|
// (`c->proc = execCommand`, `cmdProc p = execCommand`), a return operand, and
|
|
// `&fn`.
|
|
func cFnAddressPosition(n *sitter.Node) (string, bool) {
|
|
p := n.Parent()
|
|
if p == nil {
|
|
return "", false
|
|
}
|
|
switch p.Type() {
|
|
case "argument_list", "initializer_list", "return_statement":
|
|
return "", true
|
|
case "initializer_pair":
|
|
if isFieldChild(p, "value", n) {
|
|
return "", true
|
|
}
|
|
case "assignment_expression":
|
|
if isFieldChild(p, "right", n) {
|
|
return "", true
|
|
}
|
|
case "init_declarator":
|
|
if isFieldChild(p, "value", n) {
|
|
return "", true
|
|
}
|
|
case "binary_expression":
|
|
// A function pointer is compared for identity, never ordered — only
|
|
// `==` / `!=` operands are candidate function addresses.
|
|
if op := p.ChildByFieldName("operator"); op != nil {
|
|
if t := op.Type(); t == "==" || t == "!=" {
|
|
return "", true
|
|
}
|
|
}
|
|
case "pointer_expression":
|
|
// `&fn` address-of. tree-sitter-c models it as a pointer_expression
|
|
// with a '&' operator; a '*' operator is a dereference, not a value.
|
|
if op := p.ChildByFieldName("operator"); op != nil && op.Type() == "&" {
|
|
return "address_of", true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// isFieldChild reports whether n is exactly the `field`-named child of p (by
|
|
// byte span), so an identifier is recognised only in the intended slot.
|
|
func isFieldChild(p *sitter.Node, field string, n *sitter.Node) bool {
|
|
c := p.ChildByFieldName(field)
|
|
return c != nil && c.StartByte() == n.StartByte() && c.EndByte() == n.EndByte()
|
|
}
|
|
|
|
// cCollectLocalNames gathers the file's parameter, local, uninitialised-
|
|
// declaration, and in-file enum-member names — every declared name that can
|
|
// never denote a cross-TU function address and so must be dropped from the
|
|
// candidate set. It spans four declaration shapes:
|
|
//
|
|
// - parameters and initialised locals (`f(int x)`, `Handler h = g`) — the
|
|
// dominant flood source, since a call passes locals / params by value into
|
|
// the very positions this pass scans;
|
|
// - uninitialised declarations (`int32_t lookahead;`) — a generated lexer
|
|
// compares such a local on ==/!= across tens of thousands of lines, and the
|
|
// identifier is only ever that local, never a function address;
|
|
// - in-file enum members (`enum { sym_a, sym_b }`) — a generated parser
|
|
// declares its token / symbol enum in-file, then floods designated-
|
|
// initializer tables with the members, none of which is a function.
|
|
//
|
|
// A same-file function prototype is deliberately NOT collected: a declaration
|
|
// whose declarator is (or wraps) a function_declarator names a function, left
|
|
// bindable for the resolver gate, not shadowed here.
|
|
func cCollectLocalNames(root *sitter.Node, src []byte) map[string]bool {
|
|
out := map[string]bool{}
|
|
walkNodes(root, func(n *sitter.Node) {
|
|
switch n.Type() {
|
|
case "parameter_declaration":
|
|
if d := n.ChildByFieldName("declarator"); d != nil {
|
|
if name := cDeclName(d, src); name != "" {
|
|
out[name] = true
|
|
}
|
|
}
|
|
case "init_declarator":
|
|
if name := cDeclName(n.ChildByFieldName("declarator"), src); name != "" {
|
|
out[name] = true
|
|
}
|
|
case "declaration":
|
|
// An uninitialised declaration's declarators sit directly under it
|
|
// (an initialised one is wrapped in init_declarator, handled above).
|
|
// A declaration may carry several (`int a, *b, c[3];`), so every
|
|
// declarator-field child is unwrapped to its variable name.
|
|
for i := 0; i < int(n.ChildCount()); i++ {
|
|
if n.FieldNameForChild(i) != "declarator" {
|
|
continue
|
|
}
|
|
if name := cUninitDeclName(n.Child(i), src); name != "" {
|
|
out[name] = true
|
|
}
|
|
}
|
|
case "enumerator":
|
|
if d := n.ChildByFieldName("name"); d != nil {
|
|
if name := d.Content(src); name != "" {
|
|
out[name] = true
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return out
|
|
}
|
|
|
|
// cUninitDeclName unwraps the pointer / array / parenthesized wrappers of an
|
|
// uninitialised declaration's declarator to its variable name. It returns "" for
|
|
// a function_declarator — a prototype names a function, which must stay a
|
|
// bindable candidate rather than a shadowing local — and for any other
|
|
// unexpected shape. Unlike cDeclName it never descends through a
|
|
// function_declarator, so `int *foo(int);` (a pointer-returning prototype)
|
|
// contributes no name.
|
|
func cUninitDeclName(decl *sitter.Node, src []byte) string {
|
|
for decl != nil {
|
|
switch decl.Type() {
|
|
case "identifier", "field_identifier":
|
|
return decl.Content(src)
|
|
case "pointer_declarator", "array_declarator":
|
|
decl = decl.ChildByFieldName("declarator")
|
|
case "parenthesized_declarator":
|
|
if decl.NamedChildCount() == 0 {
|
|
return ""
|
|
}
|
|
decl = decl.NamedChild(0)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// isCFnAddressNonTarget reports whether a name is a C literal / keyword /
|
|
// builtin constant that can never be a function address, so the capture skips it
|
|
// before the candidate is emitted (the resolver gate would drop it anyway).
|
|
func isCFnAddressNonTarget(name string) bool {
|
|
switch name {
|
|
case "NULL", "sizeof", "offsetof", "va_arg", "va_start", "va_end",
|
|
"true", "false", "nil", "Nil":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isCValueRefNoise reports whether a captured value-position identifier is
|
|
// grep-level noise that must never become a cross-file function reference. A
|
|
// generated command-table row interleaves the handler with flag macros
|
|
// (CMD_WRITE), enum constants (OBJ_STRING), short arity counts, and keywords
|
|
// that only reach identifier position through ERROR recovery on a malformed
|
|
// fragment. Three cheap shape rules drop them before they flood the resolver
|
|
// gate, keeping the pass to the small set of free identifiers that can turn
|
|
// into a real cross-TU function edge:
|
|
//
|
|
// - shorter than 4 characters — too short to be a distinct handler name and
|
|
// the dominant collision source under the gate's repo-wide unique-name bind;
|
|
// - ALL_CAPS — the macro / enum-constant convention; a handler name is mixed-
|
|
// or lower-case, never all-uppercase;
|
|
// - a C keyword — never a value, only reachable in this position via a
|
|
// recovered parse.
|
|
//
|
|
// A real handler (getCommand, strlenCommand, pingCommand) is mixed-case and
|
|
// well over four characters, so none of the rules touch it.
|
|
func isCValueRefNoise(name string) bool {
|
|
if len(name) < 4 {
|
|
return true
|
|
}
|
|
if isAllCapsToken(name) {
|
|
return true
|
|
}
|
|
return cValueRefKeyword[name]
|
|
}
|
|
|
|
// isAllCapsToken reports whether name follows the ALL_CAPS macro / constant
|
|
// convention: it contains at least one letter and no lowercase letter. A
|
|
// mixed-case handler (getCommand) has a lowercase letter and is never treated
|
|
// as a macro; a digits-and-underscores-only token (never a valid identifier)
|
|
// is excluded by the letter requirement.
|
|
func isAllCapsToken(name string) bool {
|
|
hasLetter := false
|
|
for i := 0; i < len(name); i++ {
|
|
c := name[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z':
|
|
return false
|
|
case c >= 'A' && c <= 'Z':
|
|
hasLetter = true
|
|
}
|
|
}
|
|
return hasLetter
|
|
}
|
|
|
|
// cValueRefKeyword is the set of C keywords long enough to survive the
|
|
// length rule that could reach an identifier value position only through ERROR
|
|
// recovery on a malformed generated fragment.
|
|
var cValueRefKeyword = map[string]bool{
|
|
"auto": true, "break": true, "case": true, "char": true, "const": true,
|
|
"continue": true, "default": true, "double": true, "else": true,
|
|
"enum": true, "extern": true, "float": true, "goto": true, "long": true,
|
|
"register": true, "restrict": true, "return": true, "short": true,
|
|
"signed": true, "sizeof": true, "static": true, "struct": true,
|
|
"switch": true, "typedef": true, "union": true, "unsigned": true,
|
|
"void": true, "volatile": true, "while": true, "inline": true,
|
|
"_Bool": true, "_Complex": true,
|
|
}
|