mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
2004 lines
78 KiB
Rust
2004 lines
78 KiB
Rust
//! C / C++ extraction — a faithful Rust port of `TreeSitterExtractor`'s c/cpp
|
|
//! paths (src/extraction/tree-sitter.ts) plus languages/c-cpp.ts, one dual-
|
|
//! language module flagged like tsjs/ (checklist:
|
|
//! docs/design/ccpp-kernel-port-checklist.md — read it before editing).
|
|
//!
|
|
//! The seven preParse blanking passes are NOT here: the TS route point
|
|
//! (src/extraction/kernel/index.ts) applies `extractor.preParse` before the
|
|
//! kernel call, so this walker receives the SAME blanked bytes the wasm
|
|
//! extractor parses (all blanks are equal-length-space replacements — every
|
|
//! offset survives). `.metal`/`.cu`/`.cuh` arrive as language 'cpp' with their
|
|
//! dialect blanks already applied.
|
|
//!
|
|
//! Quirks mirrored bug-for-bug (each pinned by the parity gates):
|
|
//! - cpp namespace prefix stack (#1291): named `namespace a::b {` pushes the
|
|
//! name AS WRITTEN onto the qualifiedName prefix; anonymous falls through.
|
|
//! No namespace NODE is minted (#1093 crowd-out).
|
|
//! - out-of-line `Cls::method` defs: name = LAST `::` segment of the
|
|
//! declarator's qualified_identifier (BFS that skips parameter_list +
|
|
//! trailing_return_type), receiver = the template-stripped qualifier,
|
|
//! qualifiedName composed against the namespace prefix with the re-spelled-
|
|
//! prefix anchor rule; owner `contains` edge to the FIRST earlier
|
|
//! struct/class/enum/trait of the receiver's bare name.
|
|
//! - macro-name salvage: recoverCppMacroDefinedName (ALL-CAPS macro def whose
|
|
//! real name is the lone first argument) at resolveName, and
|
|
//! recoverMangledCppName (glued "Ret name" → last token) as the universal
|
|
//! post-hoc net for BOTH c and cpp.
|
|
//! - `class MACRO Name` misparse residue: isMisparsedFunction drops the
|
|
//! phantom function (name starts `namespace`, C++ keywords, or the bodyless
|
|
//! class/struct `type` + non-function_declarator shape, #946/#1061) but
|
|
//! still walks the body.
|
|
//! - C file-scope variables: init/pointer/array declarators only — a BARE
|
|
//! identifier declarator is the macro-prototype misparse and is skipped
|
|
//! (loses uninit scalars by design); cpp declarations instead take the TS
|
|
//! GENERIC fallback (direct identifier children only → `int x;` extracts,
|
|
//! `int x = 5;` does not — bug-for-bug).
|
|
//! - inheritance quirk: extractInheritance recurses into
|
|
//! field_declaration_list, where a field_declaration with no DIRECT
|
|
//! field_identifier child (pointer/array/method members) but a direct
|
|
//! type_identifier emits an `extends` ref to that type (the Go-embedding
|
|
//! branch matching c/cpp shapes). Kept: the parity gate pins today's graph.
|
|
//! - static-member/value-read pass (cpp only): `field_expression` is in
|
|
//! MEMBER_ACCESS_TYPES (listed for Scala, same node kind in cpp), so
|
|
//! `Capitalized.member` / `Capitalized->member` VALUE reads emit
|
|
//! `references` refs; qualified_identifier is checked too but its scope
|
|
//! child is namespace_identifier/template_type/…, never a plain
|
|
//! identifier, so it can't emit.
|
|
//! - explicit operator calls (#1247) ride an ERROR child — but has_error()
|
|
//! defers the whole file to wasm, so the ported branch is a faithful no-op
|
|
//! here; kept so an error-free shape (if a grammar bump ever produces one)
|
|
//! stays parity-true.
|
|
//! - local fn-pointer fan-out (#932-adjacent): `auto k = &fn<…>;` records
|
|
//! per-caller targets (insertion-ordered, branch reassignments accumulate);
|
|
//! a later bare `k(args)` emits one `calls` ref PER target and suppresses
|
|
//! the local name. Template args stripped like base-class refs (#1043).
|
|
//! - stack construction (#1035): cpp `declaration` with class-like named
|
|
//! `type` and an init_declarator whose value is argument_list /
|
|
//! initializer_list → `instantiates` (most-vexing-parse excluded).
|
|
//! - value-reference edges: C only (VALUE_REF_LANGS has 'c', not 'cpp') —
|
|
//! shadow prune via init_declarator counts, MAX_VALUE_REF_NODES cap,
|
|
//! CODEGRAPH_VALUE_REFS=0 kill switch.
|
|
//! - fn-ref capture (#756): cFamilySpec for both; cpp adds addressOfOnly
|
|
//! (bare identifiers only qualify in file-scope value/list positions).
|
|
//!
|
|
//! Files with parse errors defer to wasm (`defer:`) — error recovery is
|
|
//! encoding-dependent and the wasm recovery is canonical.
|
|
|
|
use crate::buffers::{
|
|
build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
|
|
RefRow, StrRef, Tables, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR,
|
|
};
|
|
use crate::docstring::preceding_docstring;
|
|
use crate::ids;
|
|
use crate::textutil as util;
|
|
use regex::Regex;
|
|
use std::collections::{HashMap, HashSet, VecDeque};
|
|
use std::sync::OnceLock;
|
|
use tree_sitter::{Node, Parser};
|
|
|
|
const MAX_VALUE_REF_NODES: usize = 20_000;
|
|
|
|
// --- compiled regexes (JS \w/\s spelled as ASCII classes for parity) ---------
|
|
|
|
/// recoverCppMacroDefinedName: macro-shaped parsed name.
|
|
fn macro_shaped_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$").unwrap())
|
|
}
|
|
fn has_lower_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"[a-z]").unwrap())
|
|
}
|
|
/// normalizeCppReturnType: smart-pointer/optional unwrap.
|
|
fn ret_wrapper_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| {
|
|
Regex::new(r"\b(?:std\s*::\s*)?(?:unique_ptr|shared_ptr|weak_ptr|optional)\s*<\s*([^,>]+?)\s*>")
|
|
.unwrap()
|
|
})
|
|
}
|
|
fn ret_keyword_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"\b(?:const|volatile|typename|struct|class|enum)\b").unwrap())
|
|
}
|
|
fn angle_group_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"<[^>]*>").unwrap())
|
|
}
|
|
fn ptr_ref_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"[*&]+").unwrap())
|
|
}
|
|
fn ws_run_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"\s+").unwrap())
|
|
}
|
|
fn simple_ident_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap())
|
|
}
|
|
/// recoverMangledCppName's `Ret (name)` idiom guard.
|
|
fn ret_paren_name_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"^\S+\s+\([A-Za-z_][A-Za-z0-9_]*\)").unwrap())
|
|
}
|
|
/// Operator-call receiver: simple identifier / dotted member chain.
|
|
fn operator_receiver_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_.]*$").unwrap())
|
|
}
|
|
/// Symbolic operator tail (`/^[^\w\s]/` in JS).
|
|
fn symbolic_op_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"^[^A-Za-z0-9_\s]").unwrap())
|
|
}
|
|
/// extractStaticMemberRef's capitalized-receiver test.
|
|
fn capitalized_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"^[A-Z][A-Za-z0-9_]*$").unwrap())
|
|
}
|
|
/// normalizeValue's qualified `&Cls::m` member-pointer test (`/^[A-Za-z_][\w:]*$/`).
|
|
fn qualified_ref_re() -> &'static Regex {
|
|
static RE: OnceLock<Regex> = OnceLock::new();
|
|
RE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_:]*$").unwrap())
|
|
}
|
|
|
|
/// CPP_NON_CLASS_RETURN (languages/c-cpp.ts).
|
|
fn is_non_class_return(name: &str) -> bool {
|
|
matches!(
|
|
name,
|
|
"void" | "bool" | "char" | "short" | "int" | "long" | "float" | "double" | "unsigned"
|
|
| "signed" | "size_t" | "ssize_t" | "auto" | "wchar_t" | "char8_t" | "char16_t"
|
|
| "char32_t" | "int8_t" | "int16_t" | "int32_t" | "int64_t" | "uint8_t" | "uint16_t"
|
|
| "uint32_t" | "uint64_t" | "intptr_t" | "uintptr_t" | "nullptr_t"
|
|
)
|
|
}
|
|
|
|
/// CPP_PRIMITIVE_NAMES (languages/c-cpp.ts) — recoverMangledCppName's guard.
|
|
fn is_cpp_primitive_name(name: &str) -> bool {
|
|
matches!(
|
|
name,
|
|
"bool" | "void" | "int" | "char" | "short" | "long" | "float" | "double" | "unsigned"
|
|
| "signed" | "wchar_t" | "char8_t" | "char16_t" | "char32_t" | "char_t" | "size_t"
|
|
| "auto" | "const" | "struct" | "class" | "enum" | "union" | "typename"
|
|
)
|
|
}
|
|
|
|
/// NAME_STOPLIST (function-ref.ts).
|
|
fn is_stoplisted(name: &str) -> bool {
|
|
matches!(
|
|
name,
|
|
"this" | "self" | "super" | "null" | "nil" | "true" | "false" | "undefined" | "new"
|
|
| "NULL" | "nullptr" | "None"
|
|
)
|
|
}
|
|
|
|
/// LITERAL_RECEIVER_TYPES (tree-sitter.ts) — full set; membership is what the
|
|
/// TS code tests even though only a few kinds occur in the c/cpp grammars.
|
|
fn is_literal_receiver(kind: &str) -> bool {
|
|
matches!(
|
|
kind,
|
|
"string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal"
|
|
| "template_string" | "concatenated_string" | "formatted_string" | "f_string"
|
|
| "line_string_literal" | "string_content" | "heredoc_body"
|
|
| "number" | "number_literal" | "integer" | "integer_literal" | "float"
|
|
| "float_literal" | "int_literal" | "decimal_integer_literal" | "real_literal"
|
|
| "char_literal" | "character_literal" | "rune_literal" | "regex" | "regex_literal"
|
|
| "true" | "false" | "boolean_literal" | "bool_literal" | "none" | "null" | "nil"
|
|
| "null_literal" | "undefined"
|
|
| "list" | "list_literal" | "array" | "array_literal" | "array_creation_expression"
|
|
| "dictionary" | "dict_literal" | "object" | "tuple" | "set"
|
|
)
|
|
}
|
|
|
|
/// stripCppTemplateArgs (languages/c-cpp.ts): depth-counted removal of every
|
|
/// balanced `<…>` group; `<` and `>` never reach the output.
|
|
fn strip_cpp_template_args(name: &str) -> String {
|
|
if !name.contains('<') {
|
|
return name.to_string();
|
|
}
|
|
let mut out = String::with_capacity(name.len());
|
|
let mut depth = 0u32;
|
|
for ch in name.chars() {
|
|
if ch == '<' {
|
|
depth += 1;
|
|
} else if ch == '>' {
|
|
depth = depth.saturating_sub(1);
|
|
} else if depth == 0 {
|
|
out.push(ch);
|
|
}
|
|
}
|
|
out.trim().to_string()
|
|
}
|
|
|
|
/// recoverMangledCppName (languages/c-cpp.ts) — universal post-hoc salvage for
|
|
/// a name still mangled by an unblanked macro ("Ret name" → "name").
|
|
fn recover_mangled_cpp_name(name: String) -> String {
|
|
if !name.chars().any(|c| c.is_whitespace())
|
|
|| name.starts_with("operator")
|
|
|| name.starts_with('~')
|
|
{
|
|
return name;
|
|
}
|
|
if ret_paren_name_re().is_match(&name) {
|
|
return name; // `Ret (name)` idiom — leave alone
|
|
}
|
|
let before_params = match name.find('(') {
|
|
Some(i) => &name[..i],
|
|
None => &name[..],
|
|
};
|
|
// (JS: `beforeParams.trim().split(/\s+/)` — split_whitespace already
|
|
// ignores leading/trailing whitespace, so no explicit trim.)
|
|
let candidate = before_params.split_whitespace().last().unwrap_or("");
|
|
if candidate.is_empty()
|
|
|| !simple_ident_re().is_match(candidate)
|
|
|| is_cpp_primitive_name(candidate)
|
|
{
|
|
return name;
|
|
}
|
|
candidate.to_string()
|
|
}
|
|
|
|
/// normalizeCppReturnType (languages/c-cpp.ts).
|
|
fn normalize_cpp_return_type(raw: &str) -> Option<String> {
|
|
let mut t = raw.trim().to_string();
|
|
if t.is_empty() {
|
|
return None;
|
|
}
|
|
if let Some(c) = ret_wrapper_re().captures(&t) {
|
|
if let Some(inner) = c.get(1) {
|
|
t = inner.as_str().to_string();
|
|
}
|
|
}
|
|
let t = ret_keyword_re().replace_all(&t, " ");
|
|
let t = angle_group_re().replace_all(&t, " ");
|
|
let t = ptr_ref_re().replace_all(&t, " ");
|
|
let t = ws_run_re().replace_all(&t, " ");
|
|
let t = t.trim();
|
|
if t.is_empty() {
|
|
return None;
|
|
}
|
|
let parts: Vec<&str> = t.split("::").filter(|p| !p.is_empty()).collect();
|
|
let last = *parts.last()?;
|
|
if is_non_class_return(last) || !simple_ident_re().is_match(last) {
|
|
return None;
|
|
}
|
|
Some(last.to_string())
|
|
}
|
|
|
|
/// JS `String.replace(/->/g,'.').replace(/\s+/g,'')` used on receivers.
|
|
fn arrow_dot_no_ws(s: &str) -> String {
|
|
s.replace("->", ".").chars().filter(|c| !c.is_whitespace()).collect()
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum Variant {
|
|
C,
|
|
Cpp,
|
|
}
|
|
|
|
struct Scope {
|
|
row: u32,
|
|
kind: &'static str,
|
|
name: String,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Extra {
|
|
docstring: Option<String>,
|
|
signature: Option<String>,
|
|
visibility: Option<u8>,
|
|
is_exported: Option<bool>,
|
|
return_type: Option<String>,
|
|
qualified_name: Option<String>,
|
|
}
|
|
|
|
struct ValueScope<'t> {
|
|
row: u32,
|
|
node: Node<'t>,
|
|
name: String,
|
|
}
|
|
|
|
/// Capture mode for a fn-ref candidate (gate policy keys on it).
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum Mode {
|
|
Args,
|
|
Rhs,
|
|
Value,
|
|
List,
|
|
Varinit,
|
|
}
|
|
|
|
struct Cand {
|
|
from: u32,
|
|
name: String,
|
|
mode: Mode,
|
|
explicit_ref: bool,
|
|
line: u32,
|
|
column_byte: usize,
|
|
row: usize,
|
|
}
|
|
|
|
/// Per-node metadata for the receiver-method owner lookup (mirrors the TS
|
|
/// side's scan over `this.nodes` — FIRST match wins, earlier-in-file only).
|
|
struct NodeMeta {
|
|
kind: &'static str,
|
|
name: String,
|
|
}
|
|
|
|
pub struct Walker<'t> {
|
|
src: &'t str,
|
|
file_path: &'t str,
|
|
variant: Variant,
|
|
line_starts: Vec<usize>,
|
|
arena: Arena,
|
|
tables: Tables,
|
|
stack: Vec<Scope>,
|
|
nodes_meta: Vec<NodeMeta>,
|
|
node_ids: Vec<String>,
|
|
/// C/C++ enclosing `namespace ns { … }` names (cpp only ever non-empty).
|
|
namespace_prefix: Vec<String>,
|
|
/// cppLocalFnPtrs: caller row → local name → insertion-ordered targets.
|
|
local_fn_ptrs: HashMap<u32, HashMap<String, Vec<String>>>,
|
|
defined_fn_names: HashSet<String>,
|
|
imported_names: HashSet<String>,
|
|
fn_ref_cands: Vec<Cand>,
|
|
fs_values: HashMap<String, u32>,
|
|
fs_value_counts: HashMap<String, u32>,
|
|
value_scopes: Vec<ValueScope<'t>>,
|
|
}
|
|
|
|
pub fn extract(file_path: &str, source: &str, language: &str) -> Result<EmitOut, String> {
|
|
let variant = match language {
|
|
"c" => Variant::C,
|
|
"cpp" => Variant::Cpp,
|
|
other => return Err(format!("ccpp walker got language '{other}'")),
|
|
};
|
|
let grammar = crate::langs::grammar_for(language).ok_or("no c/cpp grammar")?;
|
|
let t0 = std::time::Instant::now();
|
|
let mut parser = Parser::new();
|
|
parser
|
|
.set_language(&grammar)
|
|
.map_err(|e| format!("set_language({language}) failed: {e}"))?;
|
|
let tree = parser
|
|
.parse(source, None)
|
|
.ok_or_else(|| "parser returned null tree".to_string())?;
|
|
// Measurement hatch (parity sweeps only — never set in production): skip
|
|
// the defer so the sweep can QUANTIFY how often UTF-8 vs UTF-16 error
|
|
// recovery actually diverges on this language's erroring files.
|
|
let no_defer = std::env::var("CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT").as_deref() == Ok("1");
|
|
if tree.root_node().has_error() && !no_defer {
|
|
return Err("defer: parse tree contains errors — wasm recovery is canonical".to_string());
|
|
}
|
|
|
|
let mut w = Walker {
|
|
src: source,
|
|
file_path,
|
|
variant,
|
|
line_starts: util::line_starts(source),
|
|
arena: Arena::default(),
|
|
tables: Tables::default(),
|
|
stack: Vec::new(),
|
|
nodes_meta: Vec::new(),
|
|
node_ids: Vec::new(),
|
|
namespace_prefix: Vec::new(),
|
|
local_fn_ptrs: HashMap::new(),
|
|
defined_fn_names: HashSet::new(),
|
|
imported_names: HashSet::new(),
|
|
fn_ref_cands: Vec::new(),
|
|
fs_values: HashMap::new(),
|
|
fs_value_counts: HashMap::new(),
|
|
value_scopes: Vec::new(),
|
|
};
|
|
|
|
let line_count = source.bytes().filter(|b| *b == b'\n').count() as u32 + 1;
|
|
let base_name = file_path.rsplit(['/', '\\']).next().unwrap_or(file_path);
|
|
let mut flags = BoolFlags::default();
|
|
flags.set(FLAG_IS_EXPORTED, false);
|
|
let file_id = w.arena.put(&ids::file_node_id(file_path));
|
|
let name_ref = w.arena.put(base_name);
|
|
let qn_ref = w.arena.put(file_path);
|
|
w.tables.push_node(&NodeRow {
|
|
kind: node_kind_index("file").unwrap(),
|
|
visibility: 0,
|
|
flags,
|
|
start_line: 1,
|
|
end_line: line_count,
|
|
start_column: 0,
|
|
end_column: 0,
|
|
name: name_ref,
|
|
qualified_name: qn_ref,
|
|
id: file_id,
|
|
docstring: NONE_STR,
|
|
signature: NONE_STR,
|
|
decorators: NONE_STR,
|
|
type_parameters: NONE_STR,
|
|
return_type: NONE_STR,
|
|
extra_json: NONE_STR,
|
|
});
|
|
w.nodes_meta.push(NodeMeta { kind: "file", name: base_name.to_string() });
|
|
w.node_ids.push(ids::file_node_id(file_path));
|
|
w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
|
|
|
|
w.visit_node(tree.root_node());
|
|
w.flush_fn_ref_candidates();
|
|
w.flush_value_refs(tree.root_node());
|
|
w.stack.pop();
|
|
|
|
let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
|
let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
|
|
Ok(EmitOut {
|
|
meta,
|
|
nodes: w.tables.nodes,
|
|
edges: w.tables.edges,
|
|
refs: w.tables.refs,
|
|
arena: w.arena.into_vec(),
|
|
})
|
|
}
|
|
|
|
impl<'t> Walker<'t> {
|
|
fn text(&self, node: Node) -> &'t str {
|
|
&self.src[node.byte_range()]
|
|
}
|
|
fn line_of(&self, node: Node) -> u32 {
|
|
node.start_position().row as u32 + 1
|
|
}
|
|
fn col_of(&self, node: Node) -> u32 {
|
|
util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
|
|
}
|
|
fn end_col_of(&self, node: Node) -> u32 {
|
|
util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
|
|
}
|
|
fn top_row(&self) -> u32 {
|
|
self.stack.last().map(|s| s.row).unwrap_or(0)
|
|
}
|
|
fn inside_class_like(&self) -> bool {
|
|
self.stack
|
|
.last()
|
|
.map(|s| matches!(s.kind, "class" | "struct" | "union" | "interface" | "trait" | "enum" | "module"))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn push_ref_at(&mut self, from_row: u32, name: &str, kind_code: u8, node: Node) {
|
|
let name_ref = self.arena.put(name);
|
|
self.tables.push_ref(&RefRow {
|
|
from_idx: from_row,
|
|
kind: kind_code,
|
|
line: self.line_of(node),
|
|
column: self.col_of(node),
|
|
reference_name: name_ref,
|
|
candidates: NONE_STR,
|
|
from_id_str: NONE_STR,
|
|
});
|
|
if kind_code == edge_kind_index("imports").unwrap() {
|
|
if util::simple_name().is_match(name) {
|
|
self.imported_names.insert(name.to_string());
|
|
} else if let Some(c) = util::qualified_import().captures(name) {
|
|
self.imported_names.insert(c[1].to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, extra: Extra) -> Option<u32> {
|
|
if name.is_empty() {
|
|
return None;
|
|
}
|
|
let start_line = self.line_of(node);
|
|
let id = ids::node_id(self.file_path, kind, name, start_line);
|
|
// (c/cpp define no resolveBody hook, so createNode's endLine extension
|
|
// for sibling-body grammars never fires — endLine is the node's own.)
|
|
let end_line = node.end_position().row as u32 + 1;
|
|
|
|
let qualified = extra.qualified_name.unwrap_or_else(|| {
|
|
let mut parts: Vec<&str> = self.namespace_prefix.iter().map(|s| s.as_str()).collect();
|
|
for s in &self.stack {
|
|
if s.kind != "file" {
|
|
parts.push(&s.name);
|
|
}
|
|
}
|
|
let mut qn = parts.join("::");
|
|
if !qn.is_empty() {
|
|
qn.push_str("::");
|
|
}
|
|
qn.push_str(name);
|
|
qn
|
|
});
|
|
|
|
let mut flags = BoolFlags::default();
|
|
if let Some(v) = extra.is_exported {
|
|
flags.set(FLAG_IS_EXPORTED, v);
|
|
}
|
|
let name_ref = self.arena.put(name);
|
|
let qn_ref = self.arena.put(&qualified);
|
|
let id_ref = self.arena.put(&id);
|
|
let doc_ref = opt_str(&mut self.arena, extra.docstring.as_deref());
|
|
let sig_ref = opt_str(&mut self.arena, extra.signature.as_deref());
|
|
let ret_ref = opt_str(&mut self.arena, extra.return_type.as_deref());
|
|
let row = self.tables.push_node(&NodeRow {
|
|
kind: node_kind_index(kind).unwrap(),
|
|
visibility: extra.visibility.unwrap_or(0),
|
|
flags,
|
|
start_line,
|
|
end_line,
|
|
start_column: self.col_of(node),
|
|
end_column: self.end_col_of(node),
|
|
name: name_ref,
|
|
qualified_name: qn_ref,
|
|
id: id_ref,
|
|
docstring: doc_ref,
|
|
signature: sig_ref,
|
|
decorators: NONE_STR,
|
|
type_parameters: NONE_STR,
|
|
return_type: ret_ref,
|
|
extra_json: NONE_STR,
|
|
});
|
|
self.nodes_meta.push(NodeMeta { kind, name: name.to_string() });
|
|
self.node_ids.push(id);
|
|
|
|
let parent_row = self.top_row();
|
|
self.tables.push_edge(&EdgeRow {
|
|
source_idx: parent_row,
|
|
target_idx: row,
|
|
kind: edge_kind_index("contains").unwrap(),
|
|
provenance: 0,
|
|
line: NONE,
|
|
column: NONE,
|
|
metadata_json: NONE_STR,
|
|
source_id_str: NONE_STR,
|
|
target_id_str: NONE_STR,
|
|
});
|
|
|
|
if kind == "function" || kind == "method" {
|
|
self.defined_fn_names.insert(name.to_string());
|
|
}
|
|
// captureValueRefScope (capture is variant-agnostic like the TS side;
|
|
// flushValueRefs gates on the language — C only).
|
|
let target_kind_ok = kind == "constant" || kind == "variable";
|
|
if target_kind_ok
|
|
&& util::utf16_len(name) >= 3
|
|
&& util::has_upper_or_underscore().is_match(name)
|
|
{
|
|
let parent_ok = self
|
|
.stack
|
|
.last()
|
|
.map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "union" | "enum"))
|
|
.unwrap_or(false);
|
|
if parent_ok {
|
|
self.fs_values.insert(name.to_string(), row);
|
|
*self.fs_value_counts.entry(name.to_string()).or_insert(0) += 1;
|
|
}
|
|
}
|
|
if matches!(kind, "function" | "method" | "constant" | "variable") {
|
|
self.value_scopes.push(ValueScope { row, node, name: name.to_string() });
|
|
}
|
|
Some(row)
|
|
}
|
|
|
|
// --- name extraction -----------------------------------------------------
|
|
|
|
/// extractName: extractNameRaw + the universal recoverMangledName net
|
|
/// (wired for BOTH c and cpp in languages/c-cpp.ts).
|
|
fn extract_name(&self, node: Node) -> String {
|
|
recover_mangled_cpp_name(self.extract_name_raw(node))
|
|
}
|
|
|
|
/// extractNameRaw for the c/cpp extractor configs (nameField 'declarator';
|
|
/// cpp resolveName = extractCppQualifiedMethodName).
|
|
fn extract_name_raw(&self, node: Node) -> String {
|
|
if self.variant == Variant::Cpp {
|
|
if let Some(hook) = self.extract_cpp_qualified_method_name(node) {
|
|
return hook;
|
|
}
|
|
}
|
|
if let Some(name_node) = node.child_by_field_name("declarator") {
|
|
let mut resolved = name_node;
|
|
// Unwrap pointer/reference declarators (`int* f()`, `T& f()`).
|
|
while matches!(resolved.kind(), "pointer_declarator" | "reference_declarator") {
|
|
let inner = resolved
|
|
.child_by_field_name("declarator")
|
|
.or_else(|| resolved.named_child(0));
|
|
match inner {
|
|
Some(i) => resolved = i,
|
|
None => break,
|
|
}
|
|
}
|
|
// C++ conversion operator: `operator <type>`.
|
|
if resolved.kind() == "operator_cast" {
|
|
return match resolved.named_child(0) {
|
|
Some(t) => format!("operator {}", self.text(t).trim()),
|
|
None => self.text(resolved).to_string(),
|
|
};
|
|
}
|
|
if resolved.kind() == "function_declarator" || resolved.kind() == "declarator" {
|
|
let inner = resolved
|
|
.child_by_field_name("declarator")
|
|
.or_else(|| resolved.named_child(0));
|
|
return match inner {
|
|
Some(i) => self.text(i).to_string(),
|
|
None => self.text(resolved).to_string(),
|
|
};
|
|
}
|
|
return self.text(resolved).to_string();
|
|
}
|
|
for i in 0..node.named_child_count() {
|
|
if let Some(c) = node.named_child(i) {
|
|
if matches!(c.kind(), "identifier" | "type_identifier" | "simple_identifier" | "constant") {
|
|
return self.text(c).to_string();
|
|
}
|
|
}
|
|
}
|
|
"<anonymous>".to_string()
|
|
}
|
|
|
|
/// extractCppQualifiedMethodName (languages/c-cpp.ts:75).
|
|
fn extract_cpp_qualified_method_name(&self, node: Node) -> Option<String> {
|
|
if let Some(n) = self.recover_cpp_macro_defined_name(node) {
|
|
return Some(n);
|
|
}
|
|
let declarator = node.child_by_field_name("declarator")?;
|
|
let qid = find_declarator_qualified_id(declarator)?;
|
|
let text = self.text(qid).trim();
|
|
let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
|
|
parts.last().map(|s| s.to_string())
|
|
}
|
|
|
|
/// recoverCppMacroDefinedName (languages/c-cpp.ts:49).
|
|
fn recover_cpp_macro_defined_name(&self, node: Node) -> Option<String> {
|
|
if node.kind() != "function_definition" {
|
|
return None;
|
|
}
|
|
let declarator = node.child_by_field_name("declarator")?;
|
|
if declarator.kind() != "function_declarator" {
|
|
return None;
|
|
}
|
|
let inner = declarator.child_by_field_name("declarator")?;
|
|
if inner.kind() != "identifier" {
|
|
return None;
|
|
}
|
|
let macro_name = self.text(inner);
|
|
if !macro_shaped_re().is_match(macro_name) {
|
|
return None;
|
|
}
|
|
let params = declarator.child_by_field_name("parameters")?;
|
|
if params.named_child_count() < 2 {
|
|
return None;
|
|
}
|
|
let lone_ident_text = |p: Node| -> Option<&'t str> {
|
|
if p.kind() == "parameter_declaration"
|
|
&& p.named_child_count() == 1
|
|
&& p.named_child(0).map(|c| c.kind() == "type_identifier").unwrap_or(false)
|
|
{
|
|
Some(self.text(p.named_child(0).unwrap()))
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
let name = params.named_child(0).and_then(lone_ident_text)?;
|
|
if !has_lower_re().is_match(name) {
|
|
return None;
|
|
}
|
|
for i in 1..params.named_child_count() {
|
|
if let Some(p) = params.named_child(i) {
|
|
if lone_ident_text(p).is_some() {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
Some(name.to_string())
|
|
}
|
|
|
|
/// extractCppReceiverType (languages/c-cpp.ts:86).
|
|
fn receiver_type_of(&self, node: Node) -> Option<String> {
|
|
let declarator = node.child_by_field_name("declarator")?;
|
|
let qid = find_declarator_qualified_id(declarator)?;
|
|
let text = self.text(qid).trim();
|
|
let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
|
|
if parts.len() <= 1 {
|
|
return None;
|
|
}
|
|
let receiver = strip_cpp_template_args(&parts[..parts.len() - 1].join("::"));
|
|
if receiver.is_empty() {
|
|
None
|
|
} else {
|
|
Some(receiver)
|
|
}
|
|
}
|
|
|
|
/// extractCppReturnType: the `type` field, normalized.
|
|
fn return_type_of(&self, node: Node) -> Option<String> {
|
|
let type_node = node.child_by_field_name("type")?;
|
|
normalize_cpp_return_type(self.text(type_node))
|
|
}
|
|
|
|
/// cppExtractor.getVisibility: the FIRST access_specifier among the
|
|
/// parent's children decides (document order, not nearest-preceding —
|
|
/// bug-for-bug with the TS loop).
|
|
fn visibility_of(&self, node: Node) -> Option<u8> {
|
|
let parent = node.parent()?;
|
|
for i in 0..parent.child_count() {
|
|
let Some(child) = parent.child(i) else { continue };
|
|
if child.kind() == "access_specifier" {
|
|
let text = self.text(child);
|
|
if text.contains("public") {
|
|
return Some(1);
|
|
}
|
|
if text.contains("private") {
|
|
return Some(2);
|
|
}
|
|
if text.contains("protected") {
|
|
return Some(3);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// cExtractor.isConst: any named `type_qualifier` child reading "const".
|
|
fn is_const_declaration(&self, node: Node) -> bool {
|
|
(0..node.named_child_count())
|
|
.filter_map(|i| node.named_child(i))
|
|
.any(|c| c.kind() == "type_qualifier" && self.text(c) == "const")
|
|
}
|
|
|
|
/// cppExtractor.isMisparsedFunction (languages/c-cpp.ts:811). cpp only.
|
|
fn is_misparsed_function(&self, name: &str, node: Node) -> bool {
|
|
if self.variant != Variant::Cpp {
|
|
return false;
|
|
}
|
|
if name.starts_with("namespace") {
|
|
return true;
|
|
}
|
|
if matches!(name, "switch" | "if" | "for" | "while" | "do" | "case" | "return") {
|
|
return true;
|
|
}
|
|
is_macro_misparsed_type_decl(node)
|
|
}
|
|
|
|
/// composeReceiverQualifiedName (tree-sitter.ts:1424).
|
|
fn compose_receiver_qualified_name(&self, receiver_type: &str, name: &str) -> String {
|
|
let base = format!("{receiver_type}::{name}");
|
|
if self.namespace_prefix.is_empty() {
|
|
return base;
|
|
}
|
|
let receiver_head = receiver_type.split("::").next().unwrap_or("");
|
|
let anchor = self.namespace_prefix.iter().position(|p| p == receiver_head);
|
|
let prefix: &[String] = match anchor {
|
|
Some(i) => &self.namespace_prefix[..i],
|
|
None => &self.namespace_prefix[..],
|
|
};
|
|
if prefix.is_empty() {
|
|
base
|
|
} else {
|
|
format!("{}::{}", prefix.join("::"), base)
|
|
}
|
|
}
|
|
|
|
// --- visitNode -----------------------------------------------------------
|
|
|
|
fn visit_node(&mut self, node: Node<'t>) {
|
|
let kind = node.kind();
|
|
let mut skip_children = false;
|
|
|
|
// C++ namespace blocks: prefix-only, no node (#1291/#1093). Anonymous
|
|
// namespaces fall through to the generic walk.
|
|
if self.variant == Variant::Cpp && kind == "namespace_definition" {
|
|
let ns_name = node
|
|
.child_by_field_name("name")
|
|
.map(|n| self.text(n).to_string())
|
|
.unwrap_or_default();
|
|
if !ns_name.is_empty() {
|
|
self.namespace_prefix.push(ns_name);
|
|
for i in 0..node.named_child_count() {
|
|
if let Some(c) = node.named_child(i) {
|
|
self.visit_node(c);
|
|
}
|
|
}
|
|
self.namespace_prefix.pop();
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.maybe_capture_fn_refs(node);
|
|
|
|
if kind == "function_definition" {
|
|
// functionTypes for both; cpp's methodTypes also lists it, so
|
|
// inside a class-like scope it extracts as a method.
|
|
if self.inside_class_like() && self.variant == Variant::Cpp {
|
|
self.extract_method(node);
|
|
} else {
|
|
self.extract_function(node);
|
|
}
|
|
skip_children = true;
|
|
} else if self.variant == Variant::Cpp && kind == "class_specifier" {
|
|
self.extract_class(node);
|
|
skip_children = true;
|
|
} else if kind == "struct_specifier" {
|
|
self.extract_aggregate(node, "struct");
|
|
skip_children = true;
|
|
} else if kind == "union_specifier" {
|
|
self.extract_aggregate(node, "union");
|
|
skip_children = true;
|
|
} else if kind == "enum_specifier" {
|
|
self.extract_enum(node);
|
|
skip_children = true;
|
|
} else if kind == "type_definition"
|
|
|| (self.variant == Variant::Cpp && kind == "alias_declaration")
|
|
{
|
|
skip_children = self.extract_type_alias(node);
|
|
} else if kind == "declaration" && !self.inside_class_like() {
|
|
self.extract_variable(node);
|
|
self.scan_fn_ref_subtree(node, 0);
|
|
skip_children = true;
|
|
} else if kind == "preproc_include" {
|
|
self.extract_import(node);
|
|
} else if kind == "call_expression" {
|
|
self.extract_call(node);
|
|
} else if kind == "new_expression" {
|
|
// INSTANTIATION_KINDS: cpp `new Foo(...)`. (No anonymous-class
|
|
// body exists under new_expression in this grammar; children are
|
|
// still walked for nested calls.)
|
|
self.extract_instantiation(node);
|
|
}
|
|
|
|
if !skip_children {
|
|
for i in 0..node.named_child_count() {
|
|
if let Some(c) = node.named_child(i) {
|
|
self.visit_node(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- extractors ----------------------------------------------------------
|
|
|
|
fn extract_function(&mut self, node: Node<'t>) {
|
|
// Receiver present (out-of-line `Cls::method` def) → method instead.
|
|
if self.variant == Variant::Cpp && self.receiver_type_of(node).is_some() {
|
|
self.extract_method(node);
|
|
return;
|
|
}
|
|
|
|
let name = self.extract_name(node);
|
|
if name == "<anonymous>" {
|
|
if let Some(body) = node.child_by_field_name("body") {
|
|
self.visit_function_body(body);
|
|
}
|
|
return;
|
|
}
|
|
// Misparse artifacts: drop the node, still walk the body (#946/#1061).
|
|
if self.is_misparsed_function(&name, node) {
|
|
if let Some(body) = node.child_by_field_name("body") {
|
|
self.visit_function_body(body);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let extra = Extra {
|
|
docstring: preceding_docstring(node, self.src),
|
|
visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
|
|
return_type: self.return_type_of(node),
|
|
..Extra::default()
|
|
};
|
|
let Some(row) = self.create_node("function", &name, node, extra) else { return };
|
|
// (extractTypeAnnotations + extractDecoratorsFor are structural no-ops
|
|
// for c/cpp: not in TYPE_ANNOTATION_LANGUAGES, and the decorator node
|
|
// kinds never appear as direct children/preceding siblings in these
|
|
// grammars — `attribute` only occurs under attribute_declaration.)
|
|
self.stack.push(Scope { row, kind: "function", name });
|
|
if let Some(body) = node.child_by_field_name("body") {
|
|
self.visit_function_body(body);
|
|
}
|
|
self.stack.pop();
|
|
}
|
|
|
|
fn extract_method(&mut self, node: Node<'t>) {
|
|
let receiver_type = if self.variant == Variant::Cpp { self.receiver_type_of(node) } else { None };
|
|
|
|
if !self.inside_class_like() && receiver_type.is_none() {
|
|
// (object-literal parents don't occur in c/cpp) — treat as function.
|
|
self.extract_function(node);
|
|
return;
|
|
}
|
|
|
|
let name = self.extract_name(node);
|
|
if self.is_misparsed_function(&name, node) {
|
|
if let Some(body) = node.child_by_field_name("body") {
|
|
self.visit_function_body(body);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let extra = Extra {
|
|
docstring: preceding_docstring(node, self.src),
|
|
visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
|
|
return_type: self.return_type_of(node),
|
|
qualified_name: receiver_type
|
|
.as_ref()
|
|
.map(|r| self.compose_receiver_qualified_name(r, &name)),
|
|
..Extra::default() // extractMethod passes no isExported
|
|
};
|
|
let Some(row) = self.create_node("method", &name, node, extra) else { return };
|
|
|
|
// Out-of-line def: contains edge from the FIRST earlier-in-file
|
|
// struct/class/enum/trait node of the receiver's name.
|
|
if let Some(receiver_type) = &receiver_type {
|
|
if !self.inside_class_like() {
|
|
let owner_row = self
|
|
.nodes_meta
|
|
.iter()
|
|
.position(|m| {
|
|
m.name == *receiver_type
|
|
&& matches!(m.kind, "struct" | "union" | "class" | "enum" | "trait")
|
|
})
|
|
.map(|i| i as u32);
|
|
if let Some(owner_row) = owner_row {
|
|
self.tables.push_edge(&EdgeRow {
|
|
source_idx: owner_row,
|
|
target_idx: row,
|
|
kind: edge_kind_index("contains").unwrap(),
|
|
provenance: 0,
|
|
line: NONE,
|
|
column: NONE,
|
|
metadata_json: NONE_STR,
|
|
source_id_str: NONE_STR,
|
|
target_id_str: NONE_STR,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
self.stack.push(Scope { row, kind: "method", name });
|
|
if let Some(body) = node.child_by_field_name("body") {
|
|
self.visit_function_body(body);
|
|
}
|
|
self.stack.pop();
|
|
}
|
|
|
|
/// extractClass for cpp class_specifier (skipBodilessClass, #1093).
|
|
fn extract_class(&mut self, node: Node<'t>) {
|
|
let Some(body) = node.child_by_field_name("body") else { return };
|
|
let name = self.extract_name(node);
|
|
let extra = Extra {
|
|
docstring: preceding_docstring(node, self.src),
|
|
visibility: self.visibility_of(node),
|
|
..Extra::default()
|
|
};
|
|
let Some(row) = self.create_node("class", &name, node, extra) else { return };
|
|
self.extract_inheritance(node, row);
|
|
self.stack.push(Scope { row, kind: "class", name });
|
|
for i in 0..body.named_child_count() {
|
|
if let Some(c) = body.named_child(i) {
|
|
self.visit_node(c);
|
|
}
|
|
}
|
|
self.stack.pop();
|
|
}
|
|
|
|
/// Extract a struct-like declaration while preserving its semantic kind.
|
|
fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) {
|
|
let Some(body) = node.child_by_field_name("body") else { return };
|
|
let name = self.extract_name(node);
|
|
let extra = Extra {
|
|
docstring: preceding_docstring(node, self.src),
|
|
visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
|
|
..Extra::default()
|
|
};
|
|
let Some(row) = self.create_node(kind, &name, node, extra) else { return };
|
|
self.extract_inheritance(node, row);
|
|
self.stack.push(Scope { row, kind, name });
|
|
for i in 0..body.named_child_count() {
|
|
if let Some(c) = body.named_child(i) {
|
|
self.visit_node(c);
|
|
}
|
|
}
|
|
self.stack.pop();
|
|
}
|
|
|
|
fn extract_enum(&mut self, node: Node<'t>) {
|
|
let Some(body) = node.child_by_field_name("body") else { return };
|
|
let name = self.extract_name(node);
|
|
let extra = Extra {
|
|
docstring: preceding_docstring(node, self.src),
|
|
visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
|
|
..Extra::default()
|
|
};
|
|
let Some(row) = self.create_node("enum", &name, node, extra) else { return };
|
|
self.extract_inheritance(node, row);
|
|
self.stack.push(Scope { row, kind: "enum", name });
|
|
for i in 0..body.named_child_count() {
|
|
let Some(child) = body.named_child(i) else { continue };
|
|
if child.kind() == "enumerator" {
|
|
self.extract_enum_members(child);
|
|
} else {
|
|
self.visit_node(child);
|
|
}
|
|
}
|
|
self.stack.pop();
|
|
}
|
|
|
|
/// extractEnumMembers: enumerator's `name` field (C/C++ always has one;
|
|
/// the TS fallbacks for other grammars are unreachable here).
|
|
fn extract_enum_members(&mut self, node: Node<'t>) {
|
|
if let Some(name_node) = node.child_by_field_name("name") {
|
|
let name = self.text(name_node).to_string();
|
|
self.create_node("enum_member", &name, node, Extra::default());
|
|
}
|
|
}
|
|
|
|
/// extractTypeAlias for type_definition / alias_declaration. Returns true
|
|
/// when children were consumed (typedef struct/enum bodies).
|
|
fn extract_type_alias(&mut self, node: Node<'t>) -> bool {
|
|
let name = self.extract_name(node);
|
|
if name == "<anonymous>" {
|
|
return false;
|
|
}
|
|
let docstring = preceding_docstring(node, self.src);
|
|
|
|
// resolveTypeAliasKind: first child that is an enum/struct specifier
|
|
// WITH a body decides the node kind (anon inner specifier takes the
|
|
// typedef's name).
|
|
let mut resolved: Option<&'static str> = None;
|
|
for i in 0..node.named_child_count() {
|
|
let Some(child) = node.named_child(i) else { continue };
|
|
if child.kind() == "enum_specifier" && child.child_by_field_name("body").is_some() {
|
|
resolved = Some("enum");
|
|
break;
|
|
}
|
|
if child.kind() == "struct_specifier" && child.child_by_field_name("body").is_some() {
|
|
resolved = Some("struct");
|
|
break;
|
|
}
|
|
if child.kind() == "union_specifier" && child.child_by_field_name("body").is_some() {
|
|
resolved = Some("union");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if matches!(resolved, Some("struct") | Some("union")) {
|
|
let kind = resolved.unwrap();
|
|
let Some(row) = self.create_node(
|
|
kind,
|
|
&name,
|
|
node,
|
|
Extra { docstring, ..Extra::default() },
|
|
) else {
|
|
return true;
|
|
};
|
|
self.stack.push(Scope { row, kind, name });
|
|
let type_child = node
|
|
.child_by_field_name("type")
|
|
.or_else(|| self.find_child_by_kind(node, "struct_specifier"))
|
|
.or_else(|| self.find_child_by_kind(node, "union_specifier"));
|
|
if let Some(tc) = type_child {
|
|
self.extract_inheritance(tc, row);
|
|
let body = tc.child_by_field_name("body").unwrap_or(tc);
|
|
for i in 0..body.named_child_count() {
|
|
if let Some(c) = body.named_child(i) {
|
|
self.visit_node(c);
|
|
}
|
|
}
|
|
}
|
|
self.stack.pop();
|
|
return true;
|
|
}
|
|
|
|
if resolved == Some("enum") {
|
|
let Some(row) = self.create_node(
|
|
"enum",
|
|
&name,
|
|
node,
|
|
Extra { docstring, ..Extra::default() },
|
|
) else {
|
|
return true;
|
|
};
|
|
self.stack.push(Scope { row, kind: "enum", name });
|
|
if let Some(inner) = self.find_child_by_kind(node, "enum_specifier") {
|
|
self.extract_inheritance(inner, row);
|
|
if let Some(body) = inner.child_by_field_name("body") {
|
|
for i in 0..body.named_child_count() {
|
|
let Some(child) = body.named_child(i) else { continue };
|
|
if child.kind() == "enumerator" {
|
|
self.extract_enum_members(child);
|
|
} else {
|
|
self.visit_node(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.stack.pop();
|
|
return true;
|
|
}
|
|
|
|
self.create_node("type_alias", &name, node, Extra { docstring, ..Extra::default() });
|
|
false
|
|
}
|
|
|
|
fn find_child_by_kind(&self, node: Node<'t>, kind: &str) -> Option<Node<'t>> {
|
|
(0..node.named_child_count())
|
|
.filter_map(|i| node.named_child(i))
|
|
.find(|c| c.kind() == kind)
|
|
}
|
|
|
|
/// extractVariable: C takes the dedicated branch (file-scope declarators,
|
|
/// tree-sitter.ts:2795); cpp takes the TS GENERIC fallback (direct
|
|
/// identifier children).
|
|
fn extract_variable(&mut self, node: Node<'t>) {
|
|
let is_const = self.variant == Variant::C && self.is_const_declaration(node);
|
|
let kind: &'static str = if is_const { "constant" } else { "variable" };
|
|
let docstring = preceding_docstring(node, self.src);
|
|
// isExported?.() ?? false — EXPLICIT false (tri-state flag set).
|
|
let is_exported = Some(false);
|
|
|
|
if self.variant == Variant::C {
|
|
if has_function_ancestor(node) {
|
|
return;
|
|
}
|
|
for i in 0..node.named_child_count() {
|
|
let Some(child) = node.named_child(i) else { continue };
|
|
if !matches!(
|
|
child.kind(),
|
|
"init_declarator" | "pointer_declarator" | "array_declarator"
|
|
) {
|
|
continue;
|
|
}
|
|
let Some(name_node) = c_declarator_identifier(child) else { continue };
|
|
let name = self.text(name_node);
|
|
if name.is_empty() {
|
|
continue;
|
|
}
|
|
let value_node = if child.kind() == "init_declarator" {
|
|
child.child_by_field_name("value")
|
|
} else {
|
|
None
|
|
};
|
|
let signature = value_node.map(|v| util::init_signature(self.text(v)));
|
|
self.create_node(
|
|
kind,
|
|
name,
|
|
child,
|
|
Extra {
|
|
docstring: docstring.clone(),
|
|
signature,
|
|
is_exported,
|
|
..Extra::default()
|
|
},
|
|
);
|
|
}
|
|
} else {
|
|
// Generic fallback: direct identifier children only (`int x;`
|
|
// extracts; `int x = 5;` nests in an init_declarator and does not).
|
|
for i in 0..node.named_child_count() {
|
|
let Some(child) = node.named_child(i) else { continue };
|
|
if child.kind() != "identifier" {
|
|
continue;
|
|
}
|
|
let name = self.text(child).to_string();
|
|
if !name.is_empty() && name != "<anonymous>" {
|
|
self.create_node(
|
|
kind,
|
|
&name,
|
|
child,
|
|
Extra { docstring: docstring.clone(), is_exported, ..Extra::default() },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// extractImport via the c/cpp extractImport hook: `#include <sys.h>` /
|
|
/// `#include "local.h"`. A hook miss (`#include MACRO`) extracts nothing.
|
|
fn extract_import(&mut self, node: Node<'t>) {
|
|
let import_text = self.text(node).trim().to_string();
|
|
let module_name: Option<String> =
|
|
if let Some(sys) = self.find_child_by_kind(node, "system_lib_string") {
|
|
let t = self.text(sys);
|
|
let t = t.strip_prefix('<').unwrap_or(t);
|
|
let t = t.strip_suffix('>').unwrap_or(t);
|
|
Some(t.to_string())
|
|
} else if let Some(lit) = self.find_child_by_kind(node, "string_literal") {
|
|
self.find_child_by_kind(lit, "string_content").map(|sc| self.text(sc).to_string())
|
|
} else {
|
|
None
|
|
};
|
|
let Some(module_name) = module_name else { return };
|
|
self.create_node(
|
|
"import",
|
|
&module_name,
|
|
node,
|
|
Extra { signature: Some(import_text), ..Extra::default() },
|
|
);
|
|
if !module_name.is_empty() {
|
|
let parent = self.top_row();
|
|
self.push_ref_at(parent, &module_name, edge_kind_index("imports").unwrap(), node);
|
|
}
|
|
}
|
|
|
|
// --- calls / instantiation ----------------------------------------------
|
|
|
|
fn extract_call(&mut self, node: Node<'t>) {
|
|
if self.stack.is_empty() {
|
|
return;
|
|
}
|
|
let caller_row = self.top_row();
|
|
let func = node
|
|
.child_by_field_name("function")
|
|
.or_else(|| node.named_child(0));
|
|
let calls_kind = edge_kind_index("calls").unwrap();
|
|
|
|
// C++ explicit operator call `a.operator+(b)` (#1247): the
|
|
// operator_name hides in an ERROR child. (has_error() defers such
|
|
// files to wasm, so this scan is a faithful no-op today.)
|
|
if self.variant == Variant::Cpp {
|
|
if let Some(func) = func {
|
|
let mut operator_name = String::new();
|
|
'err: for i in 0..node.named_child_count() {
|
|
let Some(child) = node.named_child(i) else { continue };
|
|
if child.kind() != "ERROR" {
|
|
continue;
|
|
}
|
|
for j in 0..child.named_child_count() {
|
|
if let Some(op) = child.named_child(j) {
|
|
if op.kind() == "operator_name" {
|
|
operator_name = self.text(op).to_string();
|
|
break 'err;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !operator_name.is_empty() {
|
|
let sym = operator_name["operator".len()..].trim().to_string();
|
|
if symbolic_op_re().is_match(&sym) {
|
|
let compact: String = sym.chars().filter(|c| !c.is_whitespace()).collect();
|
|
operator_name = format!("operator{compact}");
|
|
}
|
|
let receiver = arrow_dot_no_ws(self.text(func));
|
|
if receiver != "this" && !operator_receiver_re().is_match(&receiver) {
|
|
return;
|
|
}
|
|
let callee = if receiver == "this" {
|
|
operator_name
|
|
} else {
|
|
format!("{receiver}.{operator_name}")
|
|
};
|
|
self.push_ref_at(caller_row, &callee, calls_kind, node);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut callee_name = String::new();
|
|
if let Some(func) = func {
|
|
if func.kind() == "field_expression" {
|
|
// `obj.method()` / `ptr->method()` — the `field` field.
|
|
let property = func
|
|
.child_by_field_name("property")
|
|
.or_else(|| func.child_by_field_name("field"))
|
|
.or_else(|| func.named_child(1));
|
|
if let Some(property) = property {
|
|
let method_name = self.text(property);
|
|
let receiver = func
|
|
.child_by_field_name("object")
|
|
.or_else(|| func.child_by_field_name("operand"))
|
|
.or_else(|| func.child_by_field_name("argument"))
|
|
.or_else(|| func.named_child(0));
|
|
if let Some(r) = receiver {
|
|
if is_literal_receiver(r.kind()) {
|
|
return; // #1230: literal receivers emit nothing
|
|
}
|
|
}
|
|
match receiver.map(|r| r.kind()) {
|
|
Some("identifier") | Some("simple_identifier") | Some("field_identifier") => {
|
|
let receiver_name = self.text(receiver.unwrap());
|
|
if !matches!(receiver_name, "self" | "this" | "cls" | "super") {
|
|
callee_name = format!("{receiver_name}.{method_name}");
|
|
} else {
|
|
callee_name = method_name.to_string();
|
|
}
|
|
}
|
|
Some("call_expression") => {
|
|
// Call-result receiver (#645/#608): re-encode as
|
|
// `<innerCallee>().<method>` — C/C++ re-encode any inner.
|
|
let inner_fn = receiver.unwrap().child_by_field_name("function");
|
|
let inner_callee =
|
|
inner_fn.map(|f| arrow_dot_no_ws(self.text(f))).unwrap_or_default();
|
|
if !inner_callee.is_empty() {
|
|
callee_name = format!("{inner_callee}().{method_name}");
|
|
} else {
|
|
callee_name = method_name.to_string();
|
|
}
|
|
}
|
|
_ => {
|
|
callee_name = method_name.to_string();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Bare / qualified / templated / parenthesized callee.
|
|
callee_name = self.text(func).to_string();
|
|
}
|
|
}
|
|
|
|
if !callee_name.is_empty() {
|
|
// `(*fp)(x)` → `fp` (parenthesized-conversion normalization).
|
|
if let Some(c) = util::paren_conversion().captures(&callee_name) {
|
|
callee_name = c[1].to_string();
|
|
}
|
|
}
|
|
|
|
// Template-arg strip on callees (`fn<T, 256>(args)`, `ns::fn<T>()`).
|
|
if !callee_name.is_empty() && callee_name.contains('<') && !callee_name.contains("operator")
|
|
{
|
|
callee_name = strip_cpp_template_args(&callee_name);
|
|
}
|
|
|
|
// Local fn-pointer fan-out: a bare callee bound earlier from `&fn`
|
|
// emits one calls ref PER recorded target (insertion order).
|
|
if !callee_name.is_empty()
|
|
&& self.variant == Variant::Cpp
|
|
&& simple_ident_re().is_match(&callee_name)
|
|
{
|
|
let targets = self
|
|
.local_fn_ptrs
|
|
.get(&caller_row)
|
|
.and_then(|locals| locals.get(&callee_name))
|
|
.cloned();
|
|
if let Some(targets) = targets {
|
|
if !targets.is_empty() {
|
|
for target in &targets {
|
|
self.push_ref_at(caller_row, target, calls_kind, node);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !callee_name.is_empty() {
|
|
self.push_ref_at(caller_row, &callee_name, calls_kind, node);
|
|
}
|
|
}
|
|
|
|
/// extractInstantiation: `new Foo(...)` and stack constructions (both
|
|
/// read the type from the `type` field; template args + qualifiers strip).
|
|
fn extract_instantiation(&mut self, node: Node<'t>) {
|
|
if self.stack.is_empty() {
|
|
return;
|
|
}
|
|
let from = self.top_row();
|
|
let ctor = node
|
|
.child_by_field_name("constructor")
|
|
.or_else(|| node.child_by_field_name("type"))
|
|
.or_else(|| node.child_by_field_name("name"))
|
|
.or_else(|| node.named_child(0));
|
|
let Some(ctor) = ctor else { return };
|
|
|
|
let mut class_name = self.text(ctor).to_string();
|
|
if let Some(lt) = class_name.find('<') {
|
|
if lt > 0 {
|
|
class_name.truncate(lt);
|
|
}
|
|
}
|
|
// Keep the trailing identifier of `ns::Foo` / `a.Foo`.
|
|
let last_dot = class_name.rfind('.').map(|i| i as isize).unwrap_or(-1);
|
|
let last_colons = class_name.rfind("::").map(|i| i as isize).unwrap_or(-1);
|
|
let cut = last_dot.max(last_colons);
|
|
if cut >= 0 {
|
|
class_name = class_name[(cut as usize + 1)..].to_string();
|
|
if class_name.starts_with(':') || class_name.starts_with('.') {
|
|
class_name.remove(0);
|
|
}
|
|
}
|
|
let class_name = class_name.trim().to_string();
|
|
if !class_name.is_empty() {
|
|
self.push_ref_at(from, &class_name, edge_kind_index("instantiates").unwrap(), node);
|
|
}
|
|
}
|
|
|
|
/// isCppStackConstruction (#1035).
|
|
fn is_cpp_stack_construction(&self, node: Node) -> bool {
|
|
let Some(type_node) = node.child_by_field_name("type") else { return false };
|
|
if !matches!(
|
|
type_node.kind(),
|
|
"type_identifier" | "template_type" | "qualified_identifier"
|
|
) {
|
|
return false;
|
|
}
|
|
for i in 0..node.named_child_count() {
|
|
let Some(child) = node.named_child(i) else { continue };
|
|
if child.kind() != "init_declarator" {
|
|
continue;
|
|
}
|
|
if let Some(value) = child.child_by_field_name("value") {
|
|
if matches!(value.kind(), "argument_list" | "initializer_list") {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// recordCppFnPtrBinding (tree-sitter.ts:5089).
|
|
fn record_cpp_fn_ptr_binding(&mut self, local_name: &str, value: Option<Node>) {
|
|
let Some(value) = value else { return };
|
|
if value.kind() != "pointer_expression" {
|
|
return;
|
|
}
|
|
if value.child(0).map(|c| c.kind() != "&").unwrap_or(true) {
|
|
return; // `*p` dereference, not address-of
|
|
}
|
|
let arg = value
|
|
.child_by_field_name("argument")
|
|
.or_else(|| value.named_child(0));
|
|
let Some(arg) = arg else { return };
|
|
if !matches!(arg.kind(), "identifier" | "template_function" | "qualified_identifier") {
|
|
return;
|
|
}
|
|
if self.stack.is_empty() {
|
|
return;
|
|
}
|
|
let caller_row = self.top_row();
|
|
let target = strip_cpp_template_args(self.text(arg));
|
|
if target.is_empty() || target == local_name {
|
|
return;
|
|
}
|
|
let targets = self
|
|
.local_fn_ptrs
|
|
.entry(caller_row)
|
|
.or_default()
|
|
.entry(local_name.to_string())
|
|
.or_default();
|
|
if !targets.contains(&target) {
|
|
targets.push(target); // Set semantics, insertion-ordered
|
|
}
|
|
}
|
|
|
|
/// extractStaticMemberRef — cpp only (c is not in STATIC_MEMBER_LANGS).
|
|
/// In this grammar the firing shape is `field_expression` (listed in
|
|
/// MEMBER_ACCESS_TYPES for Scala — same node kind here): a capitalized
|
|
/// simple receiver's value read.
|
|
fn extract_static_member_ref(&mut self, node: Node<'t>) {
|
|
if self.variant != Variant::Cpp {
|
|
return;
|
|
}
|
|
if self.stack.is_empty() {
|
|
return;
|
|
}
|
|
if !matches!(node.kind(), "field_expression" | "qualified_identifier") {
|
|
return;
|
|
}
|
|
// Skip `Type.method()` — the access is a call's callee, already linked.
|
|
if let Some(parent) = node.parent() {
|
|
if parent.kind() == "call_expression" {
|
|
let callee = parent
|
|
.child_by_field_name("function")
|
|
.or_else(|| parent.child_by_field_name("method"))
|
|
.or_else(|| parent.named_child(0));
|
|
if let Some(callee) = callee {
|
|
if callee.start_byte() == node.start_byte() {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let recv = node
|
|
.child_by_field_name("object")
|
|
.or_else(|| node.child_by_field_name("expression"))
|
|
.or_else(|| node.child_by_field_name("scope"))
|
|
.or_else(|| node.named_child(0));
|
|
let Some(recv) = recv else { return };
|
|
if !matches!(
|
|
recv.kind(),
|
|
"identifier" | "type_identifier" | "simple_identifier" | "name" | "scoped_type_identifier"
|
|
) {
|
|
return;
|
|
}
|
|
let text = self.text(recv);
|
|
if capitalized_re().is_match(text) {
|
|
let owner = self.top_row();
|
|
let name = text.to_string();
|
|
self.push_ref_at(owner, &name, edge_kind_index("references").unwrap(), recv);
|
|
}
|
|
}
|
|
|
|
// --- function bodies -----------------------------------------------------
|
|
|
|
fn visit_function_body(&mut self, body: Node<'t>) {
|
|
self.visit_for_calls_and_structure(body);
|
|
}
|
|
|
|
fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
|
|
let kind = node.kind();
|
|
self.maybe_capture_fn_refs(node);
|
|
|
|
if kind == "call_expression" {
|
|
self.extract_call(node);
|
|
} else if kind == "new_expression" {
|
|
self.extract_instantiation(node);
|
|
}
|
|
|
|
// C++ stack construction `Calculator calc(0)` / `Widget w{1,2}` (#1035).
|
|
if kind == "declaration"
|
|
&& self.variant == Variant::Cpp
|
|
&& self.is_cpp_stack_construction(node)
|
|
{
|
|
self.extract_instantiation(node);
|
|
}
|
|
|
|
// C++ local fn-pointer bindings: declarations and branch reassignments.
|
|
if self.variant == Variant::Cpp && !self.stack.is_empty() {
|
|
if kind == "declaration" {
|
|
for i in 0..node.named_child_count() {
|
|
let Some(child) = node.named_child(i) else { continue };
|
|
if child.kind() != "init_declarator" {
|
|
continue;
|
|
}
|
|
let Some(decl) = child.child_by_field_name("declarator") else { continue };
|
|
if decl.kind() != "identifier" {
|
|
continue;
|
|
}
|
|
let local = self.text(decl).to_string();
|
|
self.record_cpp_fn_ptr_binding(&local, child.child_by_field_name("value"));
|
|
}
|
|
} else if kind == "assignment_expression" {
|
|
if let Some(left) = node.child_by_field_name("left") {
|
|
if left.kind() == "identifier" {
|
|
let local = self.text(left).to_string();
|
|
self.record_cpp_fn_ptr_binding(&local, node.child_by_field_name("right"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Static-member / value-read: `Foo.BAR`, `Foo->x` (cpp).
|
|
self.extract_static_member_ref(node);
|
|
|
|
// Nested NAMED functions become their own nodes.
|
|
if kind == "function_definition" {
|
|
let nested_name = self.extract_name(node);
|
|
if !nested_name.is_empty() && nested_name != "<anonymous>" {
|
|
self.extract_function(node);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Structural nodes inside bodies (local classes; macro-misparse rescue).
|
|
if self.variant == Variant::Cpp && kind == "class_specifier" {
|
|
self.extract_class(node);
|
|
return;
|
|
}
|
|
if kind == "struct_specifier" {
|
|
self.extract_aggregate(node, "struct");
|
|
return;
|
|
}
|
|
if kind == "union_specifier" {
|
|
self.extract_aggregate(node, "union");
|
|
return;
|
|
}
|
|
if kind == "enum_specifier" {
|
|
self.extract_enum(node);
|
|
return;
|
|
}
|
|
|
|
for i in 0..node.named_child_count() {
|
|
if let Some(c) = node.named_child(i) {
|
|
self.visit_for_calls_and_structure(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- inheritance ---------------------------------------------------------
|
|
|
|
/// extractInheritance — the branches whose node kinds occur in the c/cpp
|
|
/// grammars: base_class_clause (#1043), the field_declaration Go-embedding
|
|
/// shape, and the field_declaration_list recursion that reaches it.
|
|
fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
|
|
let extends_kind = edge_kind_index("extends").unwrap();
|
|
for i in 0..node.named_child_count() {
|
|
let Some(child) = node.named_child(i) else { continue };
|
|
match child.kind() {
|
|
"base_class_clause" => {
|
|
for j in 0..child.named_child_count() {
|
|
let Some(t) = child.named_child(j) else { continue };
|
|
if matches!(
|
|
t.kind(),
|
|
"type_identifier" | "qualified_identifier" | "template_type"
|
|
) {
|
|
let name = strip_cpp_template_args(self.text(t));
|
|
self.push_ref_at(class_row, &name, extends_kind, t);
|
|
}
|
|
}
|
|
}
|
|
"field_declaration" => {
|
|
let has_field_identifier = (0..child.named_child_count())
|
|
.filter_map(|j| child.named_child(j))
|
|
.any(|c| c.kind() == "field_identifier");
|
|
if !has_field_identifier {
|
|
let type_id = (0..child.named_child_count())
|
|
.filter_map(|j| child.named_child(j))
|
|
.find(|c| c.kind() == "type_identifier");
|
|
if let Some(type_id) = type_id {
|
|
let name = self.text(type_id).to_string();
|
|
self.push_ref_at(class_row, &name, extends_kind, type_id);
|
|
}
|
|
}
|
|
}
|
|
"field_declaration_list" | "class_heritage" => {
|
|
self.extract_inheritance(child, class_row);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- fn-ref capture (#756, cFamilySpec) ----------------------------------
|
|
|
|
/// maybeCaptureFnRefs + captureFnRefCandidates for the cFamily dispatch:
|
|
/// argument_list(args), assignment_expression(rhs:right),
|
|
/// init_declarator(varinit:value), initializer_list(list),
|
|
/// initializer_pair(value:value).
|
|
fn maybe_capture_fn_refs(&mut self, node: Node<'t>) {
|
|
let mode = match node.kind() {
|
|
"argument_list" => Mode::Args,
|
|
"assignment_expression" => Mode::Rhs,
|
|
"init_declarator" => Mode::Varinit,
|
|
"initializer_list" => Mode::List,
|
|
"initializer_pair" => Mode::Value,
|
|
_ => return,
|
|
};
|
|
if self.stack.is_empty() {
|
|
return;
|
|
}
|
|
let from = self.top_row();
|
|
|
|
let mut values: Vec<Node<'t>> = Vec::new();
|
|
match mode {
|
|
Mode::Args | Mode::List => {
|
|
for i in 0..node.named_child_count() {
|
|
if let Some(c) = node.named_child(i) {
|
|
values.push(c);
|
|
}
|
|
}
|
|
}
|
|
Mode::Rhs => {
|
|
if let Some(rhs) = node.child_by_field_name("right") {
|
|
// Param-storage skip: `o->cb = cb` (LHS last name == RHS).
|
|
let lhs_text = node
|
|
.child_by_field_name("left")
|
|
.map(|l| self.text(l))
|
|
.unwrap_or("");
|
|
let lhs_last = util::lhs_last_name()
|
|
.captures(lhs_text)
|
|
.and_then(|c| c.get(1))
|
|
.map(|m| m.as_str());
|
|
if !(lhs_last.is_some() && lhs_last == Some(self.text(rhs).trim())) {
|
|
values.push(rhs);
|
|
}
|
|
}
|
|
}
|
|
Mode::Value => {
|
|
let v = node.child_by_field_name("value").or_else(|| {
|
|
if node.named_child_count() > 0 {
|
|
node.named_child(node.named_child_count() - 1)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
if let Some(v) = v {
|
|
values.push(v);
|
|
}
|
|
}
|
|
Mode::Varinit => {
|
|
// (init_declarator has no name/pattern field — no destructure skip)
|
|
if let Some(v) = node.child_by_field_name("value") {
|
|
values.push(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
for v in values {
|
|
let explicit_ref = v.kind() != "identifier"; // !idTypes.has(type)
|
|
self.normalize_fn_ref_value(v, from, mode, explicit_ref, 0);
|
|
}
|
|
}
|
|
|
|
/// normalizeValue for cFamilySpec: bare identifiers, and the
|
|
/// pointer_expression unwrap (`&fn`; `&Cls::m` keeps the qualified name).
|
|
fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, mode: Mode, explicit_ref: bool, depth: u32) {
|
|
if depth > 4 {
|
|
return;
|
|
}
|
|
match v.kind() {
|
|
"identifier" => {
|
|
let name = self.text(v);
|
|
if name.is_empty() || is_stoplisted(name) {
|
|
return;
|
|
}
|
|
self.push_fn_ref_cand(from, name.to_string(), mode, explicit_ref, v);
|
|
}
|
|
"pointer_expression" => {
|
|
// `&x` is a function value; `*x` is a data read.
|
|
if v.child(0).map(|c| c.kind() != "&").unwrap_or(true) {
|
|
return;
|
|
}
|
|
let Some(inner) = v.child_by_field_name("argument") else { return };
|
|
if inner.kind() == "qualified_identifier" {
|
|
let text = self.text(inner).trim();
|
|
if qualified_ref_re().is_match(text) && !is_stoplisted(text) {
|
|
self.push_fn_ref_cand(from, text.to_string(), mode, explicit_ref, inner);
|
|
}
|
|
return;
|
|
}
|
|
self.normalize_fn_ref_value(inner, from, mode, explicit_ref, depth + 1);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn push_fn_ref_cand(&mut self, from: u32, name: String, mode: Mode, explicit_ref: bool, node: Node) {
|
|
let p = node.start_position();
|
|
self.fn_ref_cands.push(Cand {
|
|
from,
|
|
name,
|
|
mode,
|
|
explicit_ref,
|
|
line: p.row as u32 + 1,
|
|
column_byte: node.start_byte(),
|
|
row: p.row,
|
|
});
|
|
}
|
|
|
|
/// scanFnRefSubtree: capture-only walk of subtrees the main walkers skip
|
|
/// (variable-declaration initializers). Halts at nested functions/lambdas.
|
|
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
|
|
if depth > 12 {
|
|
return;
|
|
}
|
|
if depth > 0
|
|
&& matches!(
|
|
node.kind(),
|
|
"function_definition" | "arrow_function" | "function_expression"
|
|
| "lambda_literal" | "lambda_expression"
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
self.maybe_capture_fn_refs(node);
|
|
for i in 0..node.named_child_count() {
|
|
if let Some(c) = node.named_child(i) {
|
|
self.scan_fn_ref_subtree(c, depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// flushFnRefCandidates with the cFamily gate policy: value/list positions
|
|
/// at FILE scope skip the same-file/import gate (C has no symbol imports);
|
|
/// cpp additionally requires explicit `&` forms outside those positions.
|
|
fn flush_fn_ref_candidates(&mut self) {
|
|
let cands = std::mem::take(&mut self.fn_ref_cands);
|
|
if cands.is_empty() || util::is_generated_file(self.file_path) {
|
|
return;
|
|
}
|
|
let address_of_only = self.variant == Variant::Cpp;
|
|
let mut seen: HashSet<(String, String)> = HashSet::new();
|
|
for c in cands {
|
|
let at_file_scope = self.node_ids[c.from as usize].starts_with("file:");
|
|
if address_of_only
|
|
&& !c.explicit_ref
|
|
&& !(at_file_scope && matches!(c.mode, Mode::Value | Mode::List))
|
|
{
|
|
continue;
|
|
}
|
|
if !c.name.starts_with("this.") && !c.name.contains("::") {
|
|
let skip_gate = matches!(c.mode, Mode::Value | Mode::List) && at_file_scope;
|
|
if !skip_gate
|
|
&& !self.defined_fn_names.contains(&c.name)
|
|
&& !self.imported_names.contains(&c.name)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
if !seen.insert((self.node_ids[c.from as usize].clone(), c.name.clone())) {
|
|
continue;
|
|
}
|
|
let column = util::col16(self.src, &self.line_starts, c.row, c.column_byte);
|
|
let name_ref = self.arena.put(&c.name);
|
|
self.tables.push_ref(&RefRow {
|
|
from_idx: c.from,
|
|
kind: FUNCTION_REF_CODE,
|
|
line: c.line,
|
|
column,
|
|
reference_name: name_ref,
|
|
candidates: NONE_STR,
|
|
from_id_str: NONE_STR,
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- value refs (C only: VALUE_REF_LANGS has 'c', not 'cpp') -------------
|
|
|
|
fn flush_value_refs(&mut self, root: Node<'t>) {
|
|
let scopes = std::mem::take(&mut self.value_scopes);
|
|
let mut targets = std::mem::take(&mut self.fs_values);
|
|
let counts = std::mem::take(&mut self.fs_value_counts);
|
|
if self.variant != Variant::C {
|
|
return;
|
|
}
|
|
if std::env::var("CODEGRAPH_VALUE_REFS").as_deref() == Ok("0") {
|
|
return;
|
|
}
|
|
if targets.is_empty() || scopes.is_empty() || util::is_generated_file(self.file_path) {
|
|
return;
|
|
}
|
|
|
|
// Shadow prune — the C declarator shape is init_declarator (a
|
|
// file-scope const AND the local that shadows it both count).
|
|
let mut decl_counts: HashMap<&str, u32> = HashMap::new();
|
|
let mut dstack: Vec<Node> = vec![root];
|
|
let mut dvisited = 0usize;
|
|
while let Some(n) = dstack.pop() {
|
|
if dvisited >= MAX_VALUE_REF_NODES {
|
|
break;
|
|
}
|
|
dvisited += 1;
|
|
if n.kind() == "init_declarator" {
|
|
if let Some(name_node) = c_declarator_identifier(n) {
|
|
if matches!(name_node.kind(), "identifier" | "simple_identifier") {
|
|
let nm = self.text(name_node);
|
|
if targets.contains_key(nm) {
|
|
*decl_counts.entry(nm).or_insert(0) += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for i in 0..n.named_child_count() {
|
|
if let Some(c) = n.named_child(i) {
|
|
dstack.push(c);
|
|
}
|
|
}
|
|
}
|
|
let shadowed: Vec<String> = decl_counts
|
|
.iter()
|
|
.filter(|(nm, c)| **c > counts.get(**nm).copied().unwrap_or(1))
|
|
.map(|(nm, _)| nm.to_string())
|
|
.collect();
|
|
for nm in shadowed {
|
|
targets.remove(&nm);
|
|
}
|
|
if targets.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let refs_kind = edge_kind_index("references").unwrap();
|
|
for scope in &scopes {
|
|
let mut seen: HashSet<&str> = HashSet::new();
|
|
let mut stack: Vec<Node> = vec![scope.node];
|
|
// (No Dart/Pascal sibling-body pull-in: c/cpp bodies are children.)
|
|
let mut visited = 0usize;
|
|
while let Some(n) = stack.pop() {
|
|
if visited >= MAX_VALUE_REF_NODES {
|
|
break;
|
|
}
|
|
visited += 1;
|
|
if matches!(n.kind(), "identifier" | "constant" | "name" | "simple_identifier") {
|
|
let ref_name = self.text(n);
|
|
if let Some(&target_row) = targets.get(ref_name) {
|
|
let target_id = self.node_ids[target_row as usize].as_str();
|
|
if target_id != self.node_ids[scope.row as usize]
|
|
&& ref_name != scope.name
|
|
&& !seen.contains(&target_id)
|
|
{
|
|
seen.insert(target_id);
|
|
let meta = self.arena.put(r#"{"valueRef":true}"#);
|
|
self.tables.push_edge(&EdgeRow {
|
|
source_idx: scope.row,
|
|
target_idx: target_row,
|
|
kind: refs_kind,
|
|
provenance: 0,
|
|
line: NONE,
|
|
column: NONE,
|
|
metadata_json: meta,
|
|
source_id_str: NONE_STR,
|
|
target_id_str: NONE_STR,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for i in 0..n.named_child_count() {
|
|
if let Some(c) = n.named_child(i) {
|
|
stack.push(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- free helpers ------------------------------------------------------------
|
|
|
|
/// findDeclaratorQualifiedId (languages/c-cpp.ts:13): BFS for the declarator's
|
|
/// `qualified_identifier`, skipping parameter_list + trailing_return_type so a
|
|
/// qualified PARAMETER type can't be mistaken for the method name.
|
|
fn find_declarator_qualified_id(declarator: Node) -> Option<Node> {
|
|
let mut queue: VecDeque<Node> = VecDeque::new();
|
|
queue.push_back(declarator);
|
|
while let Some(current) = queue.pop_front() {
|
|
if current.kind() == "qualified_identifier" {
|
|
return Some(current);
|
|
}
|
|
for i in 0..current.named_child_count() {
|
|
if let Some(child) = current.named_child(i) {
|
|
if child.kind() != "parameter_list" && child.kind() != "trailing_return_type" {
|
|
queue.push_back(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// cDeclaratorIdentifier (tree-sitter.ts:234): resolve the declared identifier
|
|
/// through init/pointer/array/parenthesized declarator wrappers; a
|
|
/// function_declarator means prototype/fn-ptr — null. (The C grammar's
|
|
/// parenthesized_declarator exposes no `declarator` field, so that arm always
|
|
/// terminates — bug-for-bug with getChildByField returning null there.)
|
|
fn c_declarator_identifier(node: Node) -> Option<Node> {
|
|
let mut cur = Some(node);
|
|
let mut guard = 0;
|
|
while let Some(n) = cur {
|
|
guard += 1;
|
|
if guard > 12 {
|
|
return None;
|
|
}
|
|
match n.kind() {
|
|
"identifier" => return Some(n),
|
|
"function_declarator" => return None,
|
|
"init_declarator" | "pointer_declarator" | "array_declarator"
|
|
| "parenthesized_declarator" => {
|
|
cur = n.child_by_field_name("declarator");
|
|
}
|
|
_ => return None,
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// isMacroMisparsedTypeDecl (languages/c-cpp.ts:261): `class MACRO Name {…}`
|
|
/// misparse residue — bodyless class/struct specifier in `type` + a
|
|
/// non-function_declarator declarator.
|
|
fn is_macro_misparsed_type_decl(node: Node) -> bool {
|
|
let Some(type_node) = node.child_by_field_name("type") else { return false };
|
|
if type_node.kind() != "class_specifier" && type_node.kind() != "struct_specifier" {
|
|
return false;
|
|
}
|
|
let has_body = (0..type_node.named_child_count())
|
|
.filter_map(|i| type_node.named_child(i))
|
|
.any(|c| c.kind() == "field_declaration_list");
|
|
if has_body {
|
|
return false;
|
|
}
|
|
if let Some(declarator) = node.child_by_field_name("declarator") {
|
|
if declarator.kind() == "function_declarator" {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// hasFunctionAncestor (tree-sitter.ts:295).
|
|
fn has_function_ancestor(node: Node) -> bool {
|
|
let mut p = node.parent();
|
|
while let Some(n) = p {
|
|
if n.kind() == "function_definition" {
|
|
return true;
|
|
}
|
|
p = n.parent();
|
|
}
|
|
false
|
|
}
|
|
|
|
fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
|
|
match s {
|
|
Some(s) => arena.put(s),
|
|
None => NONE_STR,
|
|
}
|
|
}
|