chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,739 @@
|
||||
package languages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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/cpp"
|
||||
)
|
||||
|
||||
// qCppAll 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. Class-method extraction
|
||||
// still walks the class_specifier body inline — C++ methods can have
|
||||
// declarators other than bare identifiers (destructor_name,
|
||||
// field_identifier, qualified_identifier), which the legacy code
|
||||
// handled via extractFuncName and an explicit body walk; keeping that
|
||||
// walk inside the class.def dispatch preserves behaviour while still
|
||||
// collapsing the repeated whole-tree scans into one.
|
||||
const qCppAll = `
|
||||
[
|
||||
(namespace_definition
|
||||
name: (namespace_identifier) @ns.name) @ns.def
|
||||
|
||||
(class_specifier
|
||||
name: (type_identifier) @class.name) @class.def
|
||||
|
||||
(struct_specifier
|
||||
name: (type_identifier) @struct.name) @struct.def
|
||||
|
||||
(enum_specifier
|
||||
name: (type_identifier) @enum.name) @enum.def
|
||||
|
||||
(function_definition
|
||||
declarator: (function_declarator
|
||||
declarator: (identifier) @func.name)) @func.def
|
||||
|
||||
(function_definition
|
||||
declarator: (pointer_declarator
|
||||
declarator: (function_declarator
|
||||
declarator: (identifier) @func.name))) @func.def
|
||||
|
||||
(function_definition
|
||||
declarator: (pointer_declarator
|
||||
declarator: (pointer_declarator
|
||||
declarator: (function_declarator
|
||||
declarator: (identifier) @func.name)))) @func.def
|
||||
|
||||
(function_definition
|
||||
declarator: (reference_declarator
|
||||
(function_declarator
|
||||
declarator: (identifier) @func.name))) @func.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
|
||||
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
field: (field_identifier) @callm.method)) @callm.expr
|
||||
]
|
||||
`
|
||||
|
||||
// CppExtractor extracts C++ source files into graph nodes and edges.
|
||||
type CppExtractor struct {
|
||||
lang *sitter.Language
|
||||
qAll *parser.PreparedQuery
|
||||
}
|
||||
|
||||
func NewCppExtractor() *CppExtractor {
|
||||
lang := cpp.GetLanguage()
|
||||
return &CppExtractor{
|
||||
lang: lang,
|
||||
qAll: parser.MustPreparedQuery(qCppAll, lang),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CppExtractor) Language() string { return "cpp" }
|
||||
func (e *CppExtractor) Extensions() []string { return []string{".cpp", ".cc", ".cxx", ".hpp"} }
|
||||
|
||||
// --- Deferred call buffer ----------------------------------------
|
||||
|
||||
// cppDeferredCall buffers a call site discovered during the
|
||||
// per-match walk so the post-pass can attribute it to the enclosing
|
||||
// function once funcRanges is built. argTypes carries the C++ ADL
|
||||
// hint set populated by extractCppCallArgTypes.
|
||||
type cppDeferredCall struct {
|
||||
name string
|
||||
line int
|
||||
isMember bool
|
||||
receiver string
|
||||
argTypes []string
|
||||
}
|
||||
|
||||
func (e *CppExtractor) 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: "cpp",
|
||||
}
|
||||
fileID := fileNode.ID
|
||||
result.Nodes = append(result.Nodes, fileNode)
|
||||
|
||||
seen := make(map[string]bool)
|
||||
var calls []cppDeferredCall
|
||||
|
||||
parser.EachMatch(e.qAll, root, src, func(m parser.QueryResult) {
|
||||
switch {
|
||||
|
||||
case m.Captures["ns.def"] != nil:
|
||||
e.emitNamespace(m, filePath, fileID, result)
|
||||
|
||||
case m.Captures["class.def"] != nil:
|
||||
e.emitClass(m, filePath, fileID, src, result, seen)
|
||||
|
||||
case m.Captures["struct.def"] != nil:
|
||||
e.emitStruct(m, filePath, fileID, src, result, seen)
|
||||
|
||||
case m.Captures["enum.def"] != nil:
|
||||
e.emitEnum(m, filePath, fileID, result, seen)
|
||||
|
||||
case m.Captures["func.def"] != nil:
|
||||
e.emitFunction(m, filePath, fileID, src, 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, "cpp", src, result, seen)
|
||||
|
||||
case m.Captures["macrofn.def"] != nil:
|
||||
emitCMacro(m.Captures["macrofn.def"].Node, true, filePath, fileID, "cpp", src, result, seen)
|
||||
|
||||
case m.Captures["callm.expr"] != nil:
|
||||
expr := m.Captures["callm.expr"]
|
||||
calls = append(calls, cppDeferredCall{
|
||||
name: m.Captures["callm.method"].Text,
|
||||
line: expr.StartLine + 1,
|
||||
isMember: true,
|
||||
receiver: cppCallReceiverText(expr.Node, src),
|
||||
argTypes: extractCppCallArgTypes(expr.Node, src),
|
||||
})
|
||||
|
||||
case m.Captures["call.expr"] != nil:
|
||||
expr := m.Captures["call.expr"]
|
||||
calls = append(calls, cppDeferredCall{
|
||||
name: m.Captures["call.name"].Text,
|
||||
line: expr.StartLine + 1,
|
||||
argTypes: extractCppCallArgTypes(expr.Node, src),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Resolve call edges against funcRanges.
|
||||
funcRanges := buildFuncRanges(result)
|
||||
|
||||
// Emit type-use edges (EdgeTypedAs) for declaration positions the
|
||||
// per-match pass leaves edge-less: local variable declarations,
|
||||
// function parameters, and return types — each attributed to the
|
||||
// enclosing function/method via funcRanges. Member/field type-uses
|
||||
// are attributed to the owning class/struct node during the body
|
||||
// walk above, so they're already in result.Edges.
|
||||
collectCppTypeUseEdges(root, funcRanges, filePath, src, result)
|
||||
|
||||
// Emit the remaining reference forms a type can appear in beyond a
|
||||
// declaration position: construction (new / stack), base-class
|
||||
// inheritance, casts, and Capitalized scope/static access. These are
|
||||
// EdgeInstantiates (construction) and EdgeReferences with a
|
||||
// ref_context subkind (inherit / cast / static_access).
|
||||
emitCppReferenceForms(root, src, filePath, fileID, funcRanges, result)
|
||||
|
||||
for _, c := range calls {
|
||||
callerID := findEnclosingFunc(funcRanges, c.line)
|
||||
if callerID == "" {
|
||||
continue
|
||||
}
|
||||
edge := &graph.Edge{
|
||||
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
|
||||
From: callerID,
|
||||
}
|
||||
if c.isMember {
|
||||
edge.To = "unresolved::*." + c.name
|
||||
} else {
|
||||
edge.To = "unresolved::" + c.name
|
||||
}
|
||||
if len(c.argTypes) > 0 {
|
||||
edge.Meta = map[string]any{
|
||||
"scope_arg_types": strings.Join(c.argTypes, ","),
|
||||
}
|
||||
}
|
||||
if c.isMember && c.receiver != "" {
|
||||
stampFactoryChainReceiver(edge, c.receiver, resolveChainType(c.receiver, nil, result))
|
||||
}
|
||||
result.Edges = append(result.Edges, edge)
|
||||
}
|
||||
|
||||
captureCFnPointerDispatch(result, root, filePath, src)
|
||||
captureFnValueCandidates(result, root, filePath, src)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Per-match emit helpers -----------------------------------------
|
||||
|
||||
func (e *CppExtractor) emitNamespace(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult) {
|
||||
name := m.Captures["ns.name"].Text
|
||||
def := m.Captures["ns.def"]
|
||||
id := filePath + "::" + name
|
||||
result.Nodes = append(result.Nodes, &graph.Node{
|
||||
ID: id, Kind: graph.KindPackage, Name: name,
|
||||
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
||||
Language: "cpp",
|
||||
})
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
||||
})
|
||||
}
|
||||
|
||||
// emitClass emits the class node and walks its body inline for methods.
|
||||
// The inline body walk replaces legacy extractClassMethods and catches
|
||||
// declarators the outer function_definition query misses
|
||||
// (field_identifier, destructor_name, qualified_identifier).
|
||||
func (e *CppExtractor) emitClass(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
className := m.Captures["class.name"].Text
|
||||
def := m.Captures["class.def"]
|
||||
classID := filePath + "::" + className
|
||||
if seen[classID] {
|
||||
return
|
||||
}
|
||||
seen[classID] = true
|
||||
meta := map[string]any{"type_flavor": "class"}
|
||||
if ns := enclosingCppNamespace(def.Node, src); ns != "" {
|
||||
meta["scope_ns"] = ns
|
||||
}
|
||||
if parent := extractCppParentClass(def.Node, src); parent != "" {
|
||||
meta["scope_parent"] = parent
|
||||
}
|
||||
result.Nodes = append(result.Nodes, &graph.Node{
|
||||
ID: classID, Kind: graph.KindType, Name: className,
|
||||
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
||||
Language: "cpp", Meta: meta,
|
||||
})
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: fileID, To: classID, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
||||
})
|
||||
e.walkClassBody(def.Node, src, filePath, fileID, className, classID, seen, result)
|
||||
}
|
||||
|
||||
func (e *CppExtractor) walkClassBody(classNode *sitter.Node, src []byte, filePath, fileID, className, classID string, seen map[string]bool, result *parser.ExtractionResult) {
|
||||
var body *sitter.Node
|
||||
for i, _nc := 0, int(classNode.NamedChildCount()); i < _nc; i++ {
|
||||
child := classNode.NamedChild(i)
|
||||
if child.Type() == "field_declaration_list" {
|
||||
body = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
typeSeen := make(map[string]bool)
|
||||
for i, _nc := 0, int(body.NamedChildCount()); i < _nc; i++ {
|
||||
child := body.NamedChild(i)
|
||||
switch child.Type() {
|
||||
case "access_specifier":
|
||||
continue
|
||||
case "function_definition":
|
||||
e.addMethodFromNode(child, src, filePath, fileID, className, classID, seen, result)
|
||||
case "field_declaration":
|
||||
// Member/field type-use: a `Foo bar;` member references Foo.
|
||||
// Attribute to the owning class node (the extractor doesn't
|
||||
// materialise a per-field node). Smart-pointer / container
|
||||
// fields unwrap to the inner type via the canonicaliser.
|
||||
line := int(child.StartPoint().Row) + 1
|
||||
if tn := child.ChildByFieldName("type"); tn != nil {
|
||||
emitCppTypeUseEdges(classID, tn.Content(src), filePath, line, result, typeSeen)
|
||||
}
|
||||
case "declaration_list":
|
||||
for j, _nc := 0, int(child.NamedChildCount()); j < _nc; j++ {
|
||||
gc := child.NamedChild(j)
|
||||
if gc.Type() == "function_definition" {
|
||||
e.addMethodFromNode(gc, src, filePath, fileID, className, classID, seen, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CppExtractor) addMethodFromNode(funcNode *sitter.Node, src []byte, filePath, fileID, className, classID string, seen map[string]bool, result *parser.ExtractionResult) {
|
||||
methodName := extractFuncName(funcNode, src)
|
||||
if methodName == "" {
|
||||
return
|
||||
}
|
||||
startLine := int(funcNode.StartPoint().Row) + 1
|
||||
endLine := int(funcNode.EndPoint().Row) + 1
|
||||
|
||||
id := filePath + "::" + className + "." + methodName
|
||||
if seen[id] {
|
||||
id = filePath + "::" + className + "." + methodName + "_L" + fmt.Sprint(startLine)
|
||||
}
|
||||
if seen[id] {
|
||||
return
|
||||
}
|
||||
seen[id] = true
|
||||
// Mark line so the function_definition dispatcher skips this.
|
||||
seen[filePath+"::_method_L"+fmt.Sprint(startLine)] = true
|
||||
|
||||
meta := map[string]any{"receiver": className, "scope_class": className}
|
||||
if ns := enclosingCppNamespace(funcNode, src); ns != "" {
|
||||
meta["scope_ns"] = ns
|
||||
}
|
||||
if rt := cppReturnType(funcNode, src); rt != "" {
|
||||
meta["return_type"] = rt
|
||||
}
|
||||
stampCppSignature(meta, funcNode, src)
|
||||
result.Nodes = append(result.Nodes, &graph.Node{
|
||||
ID: id, Kind: graph.KindMethod, Name: methodName,
|
||||
FilePath: filePath, StartLine: startLine, EndLine: endLine,
|
||||
Language: "cpp",
|
||||
Meta: meta,
|
||||
})
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine,
|
||||
})
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: id, To: classID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: startLine,
|
||||
})
|
||||
}
|
||||
|
||||
// cppReturnType returns the base return type of a C++ function/method node (its
|
||||
// `type` field), stripping reference/pointer/const/template decoration to the
|
||||
// bare type name — the seed for chained-factory receiver inference
|
||||
// (`Foo::make().x()`).
|
||||
func cppReturnType(node *sitter.Node, src []byte) string {
|
||||
t := node.ChildByFieldName("type")
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
rt := strings.TrimSpace(t.Content(src))
|
||||
// Unwrap a smart-pointer / optional return (`unique_ptr<Widget>` → Widget)
|
||||
// so a chained factory call (`make_widget()->draw()`) infers the pointee as
|
||||
// the receiver, not the wrapper.
|
||||
rt = graph.UnwrapCppSmartPointer(rt)
|
||||
rt = strings.TrimPrefix(rt, "const ")
|
||||
rt = strings.TrimRight(rt, " &*")
|
||||
if i := strings.IndexByte(rt, '<'); i >= 0 {
|
||||
rt = strings.TrimSpace(rt[:i])
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
func (e *CppExtractor) emitStruct(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
name := m.Captures["struct.name"].Text
|
||||
def := m.Captures["struct.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: "cpp", Meta: map[string]any{"type_flavor": "struct"},
|
||||
})
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
||||
})
|
||||
e.emitCppStructFieldTypeUse(def.Node, id, filePath, src, result)
|
||||
}
|
||||
|
||||
// emitCppStructFieldTypeUse walks a struct_specifier (or class_specifier)
|
||||
// body's direct field_declaration members and emits EdgeTypedAs from the
|
||||
// owning type node to each member's referenced type. Structs don't get
|
||||
// the inline method/field walk classes do, so this is the field-position
|
||||
// type-use pass for them. Methods nested in a struct are handled
|
||||
// elsewhere; only data members carry a `type` field here.
|
||||
func (e *CppExtractor) emitCppStructFieldTypeUse(structNode *sitter.Node, ownerID, filePath string, src []byte, result *parser.ExtractionResult) {
|
||||
if structNode == nil {
|
||||
return
|
||||
}
|
||||
var body *sitter.Node
|
||||
for i, _nc := 0, int(structNode.NamedChildCount()); i < _nc; i++ {
|
||||
child := structNode.NamedChild(i)
|
||||
if child.Type() == "field_declaration_list" {
|
||||
body = child
|
||||
break
|
||||
}
|
||||
}
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
typeSeen := make(map[string]bool)
|
||||
for i, _nc := 0, int(body.NamedChildCount()); i < _nc; i++ {
|
||||
child := body.NamedChild(i)
|
||||
if child.Type() != "field_declaration" {
|
||||
continue
|
||||
}
|
||||
line := int(child.StartPoint().Row) + 1
|
||||
if tn := child.ChildByFieldName("type"); tn != nil {
|
||||
emitCppTypeUseEdges(ownerID, tn.Content(src), filePath, line, result, typeSeen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CppExtractor) emitEnum(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
name := m.Captures["enum.name"].Text
|
||||
def := m.Captures["enum.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: "cpp", Meta: map[string]any{"type_flavor": "enum"},
|
||||
})
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
||||
})
|
||||
}
|
||||
|
||||
// emitFunction emits a free function. When the same line was already
|
||||
// claimed by the class-body walk (seen "_method_L<line>"), this is a
|
||||
// class method with a bare identifier declarator that was emitted
|
||||
// through addMethodFromNode — skip the duplicate.
|
||||
func (e *CppExtractor) emitFunction(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
name := m.Captures["func.name"].Text
|
||||
def := m.Captures["func.def"]
|
||||
startLine := def.StartLine + 1
|
||||
lineKey := filePath + "::_method_L" + fmt.Sprint(startLine)
|
||||
if seen[lineKey] {
|
||||
return
|
||||
}
|
||||
id := filePath + "::" + name
|
||||
if seen[id] {
|
||||
id = filePath + "::" + name + "_L" + fmt.Sprint(startLine)
|
||||
}
|
||||
if seen[id] {
|
||||
return
|
||||
}
|
||||
seen[id] = true
|
||||
meta := map[string]any{}
|
||||
if ns := enclosingCppNamespace(def.Node, src); ns != "" {
|
||||
meta["scope_ns"] = ns
|
||||
}
|
||||
// Free-function return type (smart-pointer-unwrapped) seeds chained-factory
|
||||
// receiver inference for a bare factory call (`make_widget()->draw()`), the
|
||||
// same way addMethodFromNode seeds `Foo::make().x()`.
|
||||
if rt := cppReturnType(def.Node, src); rt != "" {
|
||||
meta["return_type"] = rt
|
||||
}
|
||||
stampCppSignature(meta, def.Node, src)
|
||||
result.Nodes = append(result.Nodes, &graph.Node{
|
||||
ID: id, Kind: graph.KindFunction, Name: name,
|
||||
FilePath: filePath, StartLine: startLine, EndLine: def.EndLine + 1,
|
||||
Language: "cpp", Meta: meta,
|
||||
})
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *CppExtractor) emitInclude(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult) {
|
||||
pathCap := m.Captures["include.path"]
|
||||
raw := strings.TrimSpace(pathCap.Text)
|
||||
kind := "system"
|
||||
if strings.HasPrefix(raw, `"`) {
|
||||
kind = "quoted"
|
||||
}
|
||||
includePath := 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 --------------------------------------------------------
|
||||
|
||||
// extractFuncName walks a function_definition node to find the function name.
|
||||
// It handles both `identifier` (free functions) and `field_identifier` (methods),
|
||||
// and peels pointer / reference / parenthesized declarator wrappers so a
|
||||
// pointer-return method (`robj *Cls::bar() {…}`, `Widget &make() {…}`) is not
|
||||
// dropped — the function_declarator nests one or two levels below the outer
|
||||
// declarator in those signatures.
|
||||
func extractFuncName(funcNode *sitter.Node, src []byte) string {
|
||||
fd := cppFunctionDeclarator(funcNode.ChildByFieldName("declarator"))
|
||||
if fd == nil {
|
||||
return ""
|
||||
}
|
||||
for j, _nc := 0, int(fd.NamedChildCount()); j < _nc; j++ {
|
||||
gc := fd.NamedChild(j)
|
||||
switch gc.Type() {
|
||||
case "identifier", "field_identifier", "destructor_name", "operator_name":
|
||||
return gc.Content(src)
|
||||
case "qualified_identifier":
|
||||
return lastIdentifier(gc, src)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// cppFunctionDeclarator descends a declarator through pointer / reference /
|
||||
// parenthesized wrappers to the function_declarator that carries the name, or
|
||||
// nil when the declarator does not resolve to a function. Bounds the walk so a
|
||||
// pathological tree cannot loop.
|
||||
func cppFunctionDeclarator(decl *sitter.Node) *sitter.Node {
|
||||
for i := 0; decl != nil && i < 8; i++ {
|
||||
switch decl.Type() {
|
||||
case "function_declarator":
|
||||
return decl
|
||||
case "pointer_declarator", "reference_declarator", "parenthesized_declarator":
|
||||
decl = cppInnerDeclarator(decl)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cppInnerDeclarator returns the declarator nested inside a wrapper declarator:
|
||||
// the `declarator` field when the grammar names it (pointer_declarator), else
|
||||
// the first declarator-shaped named child (reference_declarator and
|
||||
// parenthesized_declarator carry it positionally).
|
||||
func cppInnerDeclarator(decl *sitter.Node) *sitter.Node {
|
||||
if inner := decl.ChildByFieldName("declarator"); inner != nil {
|
||||
return inner
|
||||
}
|
||||
for i, _nc := 0, int(decl.NamedChildCount()); i < _nc; i++ {
|
||||
c := decl.NamedChild(i)
|
||||
switch c.Type() {
|
||||
case "function_declarator", "pointer_declarator", "reference_declarator",
|
||||
"parenthesized_declarator", "identifier", "field_identifier",
|
||||
"qualified_identifier", "destructor_name", "operator_name":
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lastIdentifier extracts the last identifier from a qualified_identifier.
|
||||
func lastIdentifier(node *sitter.Node, src []byte) string {
|
||||
name := ""
|
||||
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
|
||||
child := node.NamedChild(i)
|
||||
switch child.Type() {
|
||||
case "identifier", "field_identifier", "destructor_name":
|
||||
name = child.Content(src)
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// enclosingCppNamespace walks node up through the tree-sitter AST
|
||||
// looking for namespace_definition ancestors and concatenates their
|
||||
// names with "::" (so `namespace a { namespace b { void foo() {} } }`
|
||||
// produces "a::b"). Anonymous namespaces are skipped — a function
|
||||
// inside one still belongs to the surrounding namespace for ADL.
|
||||
//
|
||||
// Stamped onto every function / method / type node so the resolver's
|
||||
// scope-based static resolver can prefer same-namespace candidates
|
||||
// before falling back to directory-locality.
|
||||
func enclosingCppNamespace(node *sitter.Node, src []byte) string {
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
var parts []string
|
||||
for p := node.Parent(); p != nil; p = p.Parent() {
|
||||
if p.Type() != "namespace_definition" {
|
||||
continue
|
||||
}
|
||||
nameNode := p.ChildByFieldName("name")
|
||||
if nameNode == nil {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(nameNode.Content(src))
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
parts = append([]string{name}, parts...)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(parts, "::")
|
||||
}
|
||||
|
||||
// extractCppParentClass returns the name of the direct base class for
|
||||
// a C++ class_specifier, or "" if the class has no base. Used by the
|
||||
// scope-based static resolver to walk the inheritance chain when
|
||||
// resolving `super`-style calls (C++ doesn't have a literal `super`
|
||||
// keyword, but Base::method() qualifications follow the same chain).
|
||||
func extractCppParentClass(classNode *sitter.Node, src []byte) string {
|
||||
if classNode == nil {
|
||||
return ""
|
||||
}
|
||||
for i, _nc := 0, int(classNode.NamedChildCount()); i < _nc; i++ {
|
||||
child := classNode.NamedChild(i)
|
||||
if child.Type() != "base_class_clause" {
|
||||
continue
|
||||
}
|
||||
for j, _nc := 0, int(child.NamedChildCount()); j < _nc; j++ {
|
||||
sub := child.NamedChild(j)
|
||||
switch sub.Type() {
|
||||
case "type_identifier", "qualified_identifier":
|
||||
return strings.TrimSpace(sub.Content(src))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractCppCallArgTypes returns the type-name hints harvested from a
|
||||
// C++ call_expression's argument list, used to seed Argument-Dependent
|
||||
// Lookup. We restrict the harvest to the cases where the argument
|
||||
// type is structurally unambiguous from the call site alone:
|
||||
//
|
||||
// - `new Type(...)` → "Type"
|
||||
// - `Type{...}` → "Type" (compound literal / temporary)
|
||||
// - `Type(arg)` → "Type" (functional cast / explicit ctor)
|
||||
//
|
||||
// Anything else (bare variables, method-chain returns, expressions)
|
||||
// is skipped — ADL is best-effort here, and a partial type list is
|
||||
// strictly better than guessing. The resolver treats an empty hint
|
||||
// set as "no ADL evidence" and falls through to the regular cascade.
|
||||
func extractCppCallArgTypes(callNode *sitter.Node, src []byte) []string {
|
||||
if callNode == nil {
|
||||
return nil
|
||||
}
|
||||
args := callNode.ChildByFieldName("arguments")
|
||||
if args == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
||||
arg := args.NamedChild(i)
|
||||
typeName := cppArgTypeHint(arg, src)
|
||||
if typeName == "" {
|
||||
// Positional placeholder so the overload ranker keeps argument
|
||||
// alignment (an unknown arg is compatible with any param). The ADL
|
||||
// namespace pass ignores "?" (it yields no namespace).
|
||||
typeName = "?"
|
||||
}
|
||||
out = append(out, typeName)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cppArgTypeHint(arg *sitter.Node, src []byte) string {
|
||||
if arg == nil {
|
||||
return ""
|
||||
}
|
||||
switch arg.Type() {
|
||||
case "number_literal":
|
||||
if strings.ContainsAny(arg.Content(src), ".eE") && !strings.HasPrefix(arg.Content(src), "0x") {
|
||||
return "double"
|
||||
}
|
||||
return "int"
|
||||
case "string_literal", "raw_string_literal", "concatenated_string":
|
||||
return "string"
|
||||
case "char_literal":
|
||||
return "char"
|
||||
case "true", "false":
|
||||
return "bool"
|
||||
case "null", "nullptr":
|
||||
return "null"
|
||||
case "new_expression":
|
||||
if t := arg.ChildByFieldName("type"); t != nil {
|
||||
return strings.TrimSpace(t.Content(src))
|
||||
}
|
||||
case "compound_literal_expression":
|
||||
if t := arg.ChildByFieldName("type"); t != nil {
|
||||
return strings.TrimSpace(t.Content(src))
|
||||
}
|
||||
case "call_expression":
|
||||
// Functional-cast `Type(arg)`: the function position is a
|
||||
// type_identifier or qualified_identifier whose text is the
|
||||
// type itself. Distinguishes from regular method calls by
|
||||
// checking that the function position resolves to a type
|
||||
// name (proper-cased, single-segment) — heuristic but safe
|
||||
// because the resolver treats ADL hints as evidence, not
|
||||
// truth.
|
||||
if f := arg.ChildByFieldName("function"); f != nil {
|
||||
switch f.Type() {
|
||||
case "type_identifier", "qualified_identifier":
|
||||
return strings.TrimSpace(f.Content(src))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// cppCallReceiverText returns the receiver expression text of a member call
|
||||
// `recv.method(...)` / `recv->method(...)` -- the object of the call's
|
||||
// field_expression -- so a factory chain (`make().with().build()`) can be
|
||||
// typed by resolveChainType.
|
||||
func cppCallReceiverText(callNode *sitter.Node, src []byte) string {
|
||||
if callNode == nil {
|
||||
return ""
|
||||
}
|
||||
fn := callNode.ChildByFieldName("function")
|
||||
if fn == nil || fn.Type() != "field_expression" {
|
||||
return ""
|
||||
}
|
||||
obj := fn.ChildByFieldName("argument")
|
||||
if obj == nil && fn.NamedChildCount() > 0 {
|
||||
obj = fn.NamedChild(0)
|
||||
}
|
||||
if obj == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(obj.Content(src))
|
||||
}
|
||||
Reference in New Issue
Block a user