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,413 @@
|
||||
package languages
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
"github.com/zzet/gortex/internal/parser"
|
||||
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
||||
)
|
||||
|
||||
// C++ reference-form edges. Beyond declaration-position type uses (which
|
||||
// collectCppTypeUseEdges already materialises as EdgeTypedAs), a C++ type
|
||||
// name appears in several other positions that are genuine cross-file
|
||||
// references but leave no graph edge without a language server:
|
||||
//
|
||||
// - construction — `new Foo(...)` (new_expression) or stack
|
||||
// `Foo x(...)` / `Foo x{...}` (a declaration whose initializer is a
|
||||
// constructor call) → graph.EdgeInstantiates.
|
||||
// - inheritance — `class X : public Base, private Mixin`
|
||||
// (base_class_clause) → graph.EdgeReferences, ref_context=inherit.
|
||||
// - casts — `static_cast<Foo>(x)` / `dynamic_cast<Foo>(x)` /
|
||||
// `reinterpret_cast<Foo>(x)` / `const_cast<Foo>(x)` (a call through a
|
||||
// template_function) and C-style `(Foo)x` (cast_expression) →
|
||||
// EdgeReferences, ref_context=cast.
|
||||
// - scope / static access — `Foo::BAR`, `Foo::method()`
|
||||
// (qualified_identifier whose scope is a Capitalized namespace / type)
|
||||
// → EdgeReferences, ref_context=static_access.
|
||||
// - generic / template arguments — every named type inside a
|
||||
// `template_argument_list` (`std::vector<Foo>`, `std::map<K, Bar>`,
|
||||
// `Foo<Bar>`, nested `std::map<std::string, std::vector<Widget>>`) →
|
||||
// EdgeReferences, ref_context=generic_arg. The declaration-position
|
||||
// canonicaliser keeps only the template head (or unwraps a single
|
||||
// container arg), so the element types of multi-argument / user
|
||||
// generics were otherwise dropped.
|
||||
//
|
||||
// emitCppReferenceForms runs one post-pass tree walk (mirroring
|
||||
// collectCppTypeUseEdges) and emits these edges, attributed to the
|
||||
// enclosing function/method (construction, cast, static access) or to the
|
||||
// enclosing class/struct node (inheritance). Targets are bare
|
||||
// `unresolved::<Type>`; the cpp resolver lands them on a real type node.
|
||||
//
|
||||
// LOAD-BEARING: structural EdgeReferences edges carrying a bare
|
||||
// `unresolved::Name` target are reverted by the cross-package guard
|
||||
// (internal/resolver/cross_pkg_guard.go) at the weak OriginASTInferred /
|
||||
// OriginTextMatched tiers when the target isn't import-reachable. These
|
||||
// edges are AST-grounded facts (the syntax says "X inherits Base"), so
|
||||
// they are stamped graph.OriginASTResolved, which the guard skips. The
|
||||
// EdgeInstantiates edges aren't policed by the guard.
|
||||
//
|
||||
// A strict Capitalization gate keeps the noise out: only leaf type names
|
||||
// that begin with an uppercase letter are emitted, and `std::`-qualified
|
||||
// paths reduce to their trailing Capitalized type (so `std::vector<int>`
|
||||
// and `std::move` contribute nothing, while `std::String` would reduce to
|
||||
// String). Primitives and unparseable spellings are dropped via the same
|
||||
// canonicalizeCppTypeRef + isCppPrimitive helpers the type-use pass uses.
|
||||
|
||||
// cppCastFunctions are the named C++ cast operators whose sole template
|
||||
// argument is the cast-target type. A `call_expression` through a
|
||||
// `template_function` named one of these is a cast, not an ordinary call.
|
||||
var cppCastFunctions = map[string]bool{
|
||||
"static_cast": true,
|
||||
"dynamic_cast": true,
|
||||
"reinterpret_cast": true,
|
||||
"const_cast": true,
|
||||
}
|
||||
|
||||
// emitCppReferenceForms walks the parsed tree once and emits the
|
||||
// construction / inheritance / cast / static-access reference edges
|
||||
// described above. funcRanges attributes body-level forms to their
|
||||
// enclosing function/method; inheritance is attributed to the class node
|
||||
// by name. Deduplicated per (owner, type, line, ref_context) so a form
|
||||
// repeated on one line contributes a single edge.
|
||||
func emitCppReferenceForms(root *sitter.Node, src []byte, filePath, fileID string, funcRanges []funcRange, result *parser.ExtractionResult) {
|
||||
if root == nil {
|
||||
return
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var walk func(n *sitter.Node)
|
||||
walk = func(n *sitter.Node) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
switch n.Type() {
|
||||
case "base_class_clause":
|
||||
emitCppInheritance(n, src, filePath, result, seen)
|
||||
case "new_expression":
|
||||
emitCppConstruction(n, src, filePath, funcRanges, result, seen)
|
||||
case "declaration":
|
||||
emitCppStackConstruction(n, src, filePath, funcRanges, result, seen)
|
||||
case "cast_expression":
|
||||
emitCppCCast(n, src, filePath, funcRanges, result, seen)
|
||||
case "call_expression":
|
||||
emitCppNamedCast(n, src, filePath, funcRanges, result, seen)
|
||||
emitCppQualifiedCall(n, src, filePath, funcRanges, result, seen)
|
||||
case "qualified_identifier":
|
||||
emitCppStaticAccess(n, src, filePath, funcRanges, result, seen)
|
||||
case "template_argument_list":
|
||||
emitCppGenericArgs(n, src, filePath, funcRanges, result, seen)
|
||||
}
|
||||
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
||||
walk(n.NamedChild(i))
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
}
|
||||
|
||||
// emitCppReferenceEdge appends one EdgeReferences from ownerID to
|
||||
// unresolved::<type> at OriginASTResolved, stamping ref_context. The type
|
||||
// text is canonicalised + Capitalization-gated; primitives, lowercase
|
||||
// leaves, and unparseable spellings are skipped. Deduplicated per
|
||||
// (owner, type, line, ref_context).
|
||||
func emitCppReferenceEdge(ownerID, typeText, refContext, filePath string, line int, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
if ownerID == "" {
|
||||
return
|
||||
}
|
||||
t := canonicalizeCppTypeRef(typeText)
|
||||
if t == "" || isCppPrimitive(t) || !isCapitalizedCppType(t) {
|
||||
return
|
||||
}
|
||||
key := ownerID + "\x00" + t + "\x00" + refContext + "\x00" + lineKey(line)
|
||||
if seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: ownerID,
|
||||
To: "unresolved::" + t,
|
||||
Kind: graph.EdgeReferences,
|
||||
FilePath: filePath,
|
||||
Line: line,
|
||||
Origin: graph.OriginASTResolved,
|
||||
Meta: map[string]any{"ref_context": refContext},
|
||||
})
|
||||
}
|
||||
|
||||
// emitCppInstantiateEdge appends one EdgeInstantiates from ownerID to
|
||||
// unresolved::<type>. EdgeInstantiates is not policed by the cross-package
|
||||
// guard, so it carries the OriginASTInferred tier (an AST inference, not
|
||||
// an LSP-checked fact) like the type-use pass. Deduplicated per
|
||||
// (owner, type, line, instantiate).
|
||||
func emitCppInstantiateEdge(ownerID, typeText, filePath string, line int, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
if ownerID == "" {
|
||||
return
|
||||
}
|
||||
t := canonicalizeCppTypeRef(typeText)
|
||||
if t == "" || isCppPrimitive(t) || !isCapitalizedCppType(t) {
|
||||
return
|
||||
}
|
||||
key := ownerID + "\x00" + t + "\x00instantiate\x00" + lineKey(line)
|
||||
if seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
result.Edges = append(result.Edges, &graph.Edge{
|
||||
From: ownerID,
|
||||
To: "unresolved::" + t,
|
||||
Kind: graph.EdgeInstantiates,
|
||||
FilePath: filePath,
|
||||
Line: line,
|
||||
Origin: graph.OriginASTInferred,
|
||||
})
|
||||
}
|
||||
|
||||
// emitCppInheritance handles a base_class_clause: each base type
|
||||
// (`type_identifier`, `template_type`, or `qualified_identifier` child,
|
||||
// skipping the leading access_specifier tokens) is an inherit-context
|
||||
// reference from the enclosing class/struct node.
|
||||
func emitCppInheritance(clause *sitter.Node, src []byte, filePath string, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
owner := cppEnclosingTypeID(clause, src, filePath)
|
||||
if owner == "" {
|
||||
return
|
||||
}
|
||||
line := int(clause.StartPoint().Row) + 1
|
||||
for i, _nc := 0, int(clause.NamedChildCount()); i < _nc; i++ {
|
||||
ch := clause.NamedChild(i)
|
||||
switch ch.Type() {
|
||||
case "type_identifier", "template_type", "qualified_identifier", "dependent_type":
|
||||
emitCppReferenceEdge(owner, ch.Content(src), graph.RefContextInherit, filePath, line, result, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cppEnclosingTypeID returns the node ID of the class/struct that owns a
|
||||
// base_class_clause — its parent class_specifier/struct_specifier's name
|
||||
// field, formed as filePath::Name to match emitClass / emitStruct.
|
||||
func cppEnclosingTypeID(clause *sitter.Node, src []byte, filePath string) string {
|
||||
parent := clause.Parent()
|
||||
if parent == nil {
|
||||
return ""
|
||||
}
|
||||
switch parent.Type() {
|
||||
case "class_specifier", "struct_specifier":
|
||||
if name := parent.ChildByFieldName("name"); name != nil {
|
||||
return filePath + "::" + strings.TrimSpace(name.Content(src))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// emitCppConstruction handles `new Foo(...)`: the new_expression's `type`
|
||||
// field names the constructed type. Attributed to the enclosing
|
||||
// function/method via funcRanges.
|
||||
func emitCppConstruction(n *sitter.Node, src []byte, filePath string, funcRanges []funcRange, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
tn := n.ChildByFieldName("type")
|
||||
if tn == nil {
|
||||
return
|
||||
}
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
owner := findEnclosingFunc(funcRanges, line)
|
||||
emitCppInstantiateEdge(owner, tn.Content(src), filePath, line, result, seen)
|
||||
}
|
||||
|
||||
// emitCppStackConstruction handles stack construction
|
||||
// `Foo x(...)` / `Foo x{...}`: a declaration whose `type` is a named type
|
||||
// and whose init_declarator initialiser is a constructor call
|
||||
// (argument_list) or brace-init (initializer_list). A plain `Foo x;` (no
|
||||
// initialiser) or `int x = 5;` is left to the type-use pass / skipped.
|
||||
func emitCppStackConstruction(n *sitter.Node, src []byte, filePath string, funcRanges []funcRange, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
tn := n.ChildByFieldName("type")
|
||||
if tn == nil {
|
||||
return
|
||||
}
|
||||
if !cppDeclHasCtorInit(n) {
|
||||
return
|
||||
}
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
owner := findEnclosingFunc(funcRanges, line)
|
||||
emitCppInstantiateEdge(owner, tn.Content(src), filePath, line, result, seen)
|
||||
}
|
||||
|
||||
// cppDeclHasCtorInit reports whether a declaration has at least one
|
||||
// init_declarator initialised by a constructor argument list or brace
|
||||
// initializer — the marker that distinguishes a stack construction
|
||||
// (`Foo x(1)`, `Foo x{1}`) from a plain typed local (`Foo x;`, `int x = 5`).
|
||||
func cppDeclHasCtorInit(decl *sitter.Node) bool {
|
||||
for i, _nc := 0, int(decl.NamedChildCount()); i < _nc; i++ {
|
||||
ch := decl.NamedChild(i)
|
||||
if ch.Type() != "init_declarator" {
|
||||
continue
|
||||
}
|
||||
if v := ch.ChildByFieldName("value"); v != nil {
|
||||
switch v.Type() {
|
||||
case "argument_list", "initializer_list":
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// emitCppCCast handles a C-style cast `(Foo)x`: the cast_expression's
|
||||
// `type` field is a type_descriptor naming the cast target.
|
||||
func emitCppCCast(n *sitter.Node, src []byte, filePath string, funcRanges []funcRange, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
tn := n.ChildByFieldName("type")
|
||||
if tn == nil {
|
||||
return
|
||||
}
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
owner := findEnclosingFunc(funcRanges, line)
|
||||
emitCppReferenceEdge(owner, tn.Content(src), graph.RefContextCast, filePath, line, result, seen)
|
||||
}
|
||||
|
||||
// emitCppNamedCast handles `static_cast<Foo>(x)` and the other named cast
|
||||
// operators: a call_expression whose function is a template_function named
|
||||
// one of cppCastFunctions, whose single template argument is the cast
|
||||
// target type.
|
||||
func emitCppNamedCast(n *sitter.Node, src []byte, filePath string, funcRanges []funcRange, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
fn := n.ChildByFieldName("function")
|
||||
if fn == nil || fn.Type() != "template_function" {
|
||||
return
|
||||
}
|
||||
name := fn.ChildByFieldName("name")
|
||||
if name == nil || !cppCastFunctions[strings.TrimSpace(name.Content(src))] {
|
||||
return
|
||||
}
|
||||
args := fn.ChildByFieldName("arguments")
|
||||
if args == nil {
|
||||
return
|
||||
}
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
owner := findEnclosingFunc(funcRanges, line)
|
||||
// The first template argument is the cast target.
|
||||
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
||||
ch := args.NamedChild(i)
|
||||
if ch.Type() == "type_descriptor" {
|
||||
emitCppReferenceEdge(owner, ch.Content(src), graph.RefContextCast, filePath, line, result, seen)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// emitCppQualifiedCall handles `Thing::method()`: a call_expression whose
|
||||
// function is a qualified_identifier with a Capitalized scope — the scope
|
||||
// type is a static-access reference. The trailing member name is left to
|
||||
// the call resolver.
|
||||
func emitCppQualifiedCall(n *sitter.Node, src []byte, filePath string, funcRanges []funcRange, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
fn := n.ChildByFieldName("function")
|
||||
if fn == nil || fn.Type() != "qualified_identifier" {
|
||||
return
|
||||
}
|
||||
scope := cppQualifiedScopeType(fn, src)
|
||||
if scope == "" {
|
||||
return
|
||||
}
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
owner := findEnclosingFunc(funcRanges, line)
|
||||
emitCppReferenceEdge(owner, scope, graph.RefContextStaticAccess, filePath, line, result, seen)
|
||||
}
|
||||
|
||||
// emitCppStaticAccess handles a bare `Foo::BAR` qualified_identifier (a
|
||||
// static-member / scoped-constant read) with a Capitalized scope. Skipped
|
||||
// when the qualified_identifier is the function of a call_expression
|
||||
// (handled by emitCppQualifiedCall) so a `Thing::method()` scope isn't
|
||||
// double-emitted.
|
||||
func emitCppStaticAccess(n *sitter.Node, src []byte, filePath string, funcRanges []funcRange, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
if p := n.Parent(); p != nil && p.Type() == "call_expression" {
|
||||
if fn := p.ChildByFieldName("function"); fn != nil && fn.Equal(n) {
|
||||
return
|
||||
}
|
||||
}
|
||||
scope := cppQualifiedScopeType(n, src)
|
||||
if scope == "" {
|
||||
return
|
||||
}
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
owner := findEnclosingFunc(funcRanges, line)
|
||||
emitCppReferenceEdge(owner, scope, graph.RefContextStaticAccess, filePath, line, result, seen)
|
||||
}
|
||||
|
||||
// emitCppGenericArgs handles a `template_argument_list` (`<Foo>`,
|
||||
// `<std::string, Bar>`, `<int, 5>`): every `type_descriptor` child names a
|
||||
// type argument, which is a genuine reference to that type. Each is
|
||||
// attributed to the enclosing function/method via funcRanges (a top-level
|
||||
// generic outside any function — e.g. a global `std::vector<Foo> g;` — has
|
||||
// no owner and is skipped, matching the type-use pass). Non-type arguments
|
||||
// (integer constants, which the grammar nests as `number_literal` rather
|
||||
// than `type_descriptor`) are not iterated, and primitives / lowercase
|
||||
// leaves are dropped by emitCppReferenceEdge's canonicaliser +
|
||||
// Capitalization gate, so `<int, 5>` and `<std::string>` contribute
|
||||
// nothing. Nesting is handled by the enclosing tree walk, which reaches the
|
||||
// inner `template_argument_list` of `std::map<K, std::vector<Widget>>` as
|
||||
// its own node.
|
||||
func emitCppGenericArgs(n *sitter.Node, src []byte, filePath string, funcRanges []funcRange, result *parser.ExtractionResult, seen map[string]bool) {
|
||||
line := int(n.StartPoint().Row) + 1
|
||||
owner := findEnclosingFunc(funcRanges, line)
|
||||
if owner == "" {
|
||||
return
|
||||
}
|
||||
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
||||
ch := n.NamedChild(i)
|
||||
if ch == nil || ch.Type() != "type_descriptor" {
|
||||
continue
|
||||
}
|
||||
emitCppReferenceEdge(owner, ch.Content(src), graph.RefContextGenericArg, filePath, line, result, seen)
|
||||
}
|
||||
}
|
||||
|
||||
// cppQualifiedScopeType returns the Capitalized type that names the scope
|
||||
// of a qualified_identifier, or "" when the scope isn't a plausible type
|
||||
// reference. The scope is the qualified_identifier's `scope` field (a
|
||||
// namespace_identifier or nested qualified_identifier). `std::`-only and
|
||||
// other lowercase scopes reduce to "" via the canonicalizer's
|
||||
// Capitalization gate; a `std::Foo` style scope reduces to its trailing
|
||||
// Capitalized segment.
|
||||
func cppQualifiedScopeType(qid *sitter.Node, src []byte) string {
|
||||
scope := qid.ChildByFieldName("scope")
|
||||
if scope == nil {
|
||||
return ""
|
||||
}
|
||||
t := canonicalizeCppTypeRef(scope.Content(src))
|
||||
if t == "" || isCppPrimitive(t) || !isCapitalizedCppType(t) {
|
||||
return ""
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// isCapitalizedCppType reports whether a (already-canonicalised) type name
|
||||
// begins with an uppercase ASCII letter — the heuristic that separates a
|
||||
// user type (`Foo`, `Widget`) from a namespace / lowercase identifier
|
||||
// (`std`, `detail`, `move`). C++ has no enforced convention, but the
|
||||
// overwhelming majority of user types are PascalCase, and the gate keeps
|
||||
// the structural reference edges from flooding the graph with namespace /
|
||||
// free-function noise.
|
||||
func isCapitalizedCppType(t string) bool {
|
||||
if t == "" {
|
||||
return false
|
||||
}
|
||||
c := t[0]
|
||||
return c >= 'A' && c <= 'Z'
|
||||
}
|
||||
|
||||
// lineKey renders a line number for the dedup key without an fmt import.
|
||||
func lineKey(line int) string {
|
||||
if line == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b [20]byte
|
||||
i := len(b)
|
||||
neg := line < 0
|
||||
if neg {
|
||||
line = -line
|
||||
}
|
||||
for line > 0 {
|
||||
i--
|
||||
b[i] = byte('0' + line%10)
|
||||
line /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
b[i] = '-'
|
||||
}
|
||||
return string(b[i:])
|
||||
}
|
||||
Reference in New Issue
Block a user