Files
codegraph/codegraph-kernel/src/lib.rs
T
Colby Mchenry a6c62d77df feat(kernel): R7b PHP walker — php module, tree-sitter-php 0.24.2 bump, php default-routed (#1380)
Fourth and final R7b batch-2 port, checklist-first recipe
(docs/design/php-kernel-port-checklist.md).

Grammar bump first, validated standalone with the diff ENUMERATED + CLASSIFIED
(unlike rust/ruby the php bump is NOT graph-neutral): tree-sitter-php ^0.22
(tree-sitter-wasms, 2023) → v0.24.2, the full HTML-interleaving `php` variant
(the walker calls LANGUAGE_PHP, never PHP_ONLY) — crate pinned =0.24.2, wasm
built from tag 5b5627f's checked-in php/src/parser.c + scanner.c + shared
common/scanner.h (all sha-matched against the crates.io tarball, ABI 14→15).
Old-vs-new full-init diffs decompose completely into: (1) the anonymous_class
wrapper shape (anon-class nodes/methods re-shape — 2,532 rows), (2) grouped
nested-clause skip (absent in the gate repos, fixture-pinned), (3) 32
formerly-erroring files parsing clean (monolog Level.php, symfony
Request/Response with 8.4 property hooks), (4) a survey-missed category found
at gate time: the 8.4 parenthesis-free `new X()->m()` chaining misparse fix
(86 garbage instantiates refs disappear, precision-positive), plus resolution
RIPPLE proven mechanically (every remaining ref-table flip pairs 1:1 with a
resolved edge on the opposite side; node rows byte-stable outside 1/3/4).

Walker (java.rs chassis + the php specifics) preserves bug-for-bug: the
visitNode hook (const_declaration at ANY scope → bare `constant` nodes, values
never walked; trait-use → implements refs WITH filePath via the ruby port's
REF_FLAG_FILE_PATH wire slot), FIRST-namespace whole-file scoping (braced
namespaces scope nothing; namespaced files DROP top-level const value-ref
targets), the import trio (single/aliased/grouped incl. the nested-clause
skip, include/require static-literal-only, `Foo\Bar::Baz` use refs), the
call-encoding zoo (DOT-joined scoped calls, `this->prop.m` #1251 encoding,
`Cls::factory().m` fluent with inner args dropped, nullsafe `?->` emitting
nothing, unsuppressed literal receivers), interface multi-extends
first-base-only drop, anon-class methods as file-level functions (top) or
vanishing (in-body), property type-hints emitting no field refs, the
final-modifier-as-type signature quirk, HOF-gated string callables
(skipGate) + array callables, and the `name`-node value-ref reader.

Gates: sweeps 0-diff monolog 217/217, laravel-framework 3007/3008, symfony
10726/10737 (13,950 files byte-parity; 12 deferrals = exactly the predicted
genuinely-broken fixtures, ≈0–0.1%); full-init dumps byte-identical ×3
(16.1k/354.2k/702.8k lines); kernel-php-parity suite (torture + drupal
.module + leading-HTML fixtures, CRLF variants, wire-flag pin, defer) + php
grammar-parity row; full suite 2,622 green ×2 under CODEGRAPH_KERNEL_EXPECT=1.
DEFAULT_ROUTED += php (13 languages).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:08:22 -05:00

232 lines
8.0 KiB
Rust

//! codegraph-kernel — native extraction kernel (napi-rs).
//!
//! Replaces ONLY the parse+extract walk inside the parse workers, behind the
//! existing `ExtractionResult` contract. Input `(filePath, content, language)`
//! per file; output flat typed buffers — one boundary crossing per file.
//! Everything downstream (resolution, synthesis, frameworks, MCP) is
//! untouched and consumes the decoded result exactly as before.
//!
//! Calls are synchronous by design: the existing `ParseWorkerPool` workers
//! already parallelize per-file, so each worker thread drives its own kernel
//! call (do NOT rebuild the pool on the Rust side — see the migration plan §3).
//!
//! Per-language extraction lives in a dedicated walker module (tsjs/ for
//! typescript/tsx/javascript/jsx) that mirrors the TS extractor for behavioral
//! parity — verified by scripts/kernel-parity.mjs and the §5 gate.
#![deny(clippy::all)]
mod buffers;
mod ccpp;
mod cfnptr;
mod csharp;
mod docstring;
mod ids;
mod go;
mod java;
mod langs;
mod php;
mod ruby;
mod rustlang;
mod textutil;
mod python;
mod tsjs;
use napi::bindgen_prelude::*;
use napi_derive::napi;
/// The five flat tables for one file. See buffers.rs for the byte layout;
/// `src/extraction/kernel/layout.ts` is the TS mirror.
#[napi(object)]
pub struct ExtractBuffers {
pub meta: Buffer,
pub nodes: Buffer,
pub edges: Buffer,
pub refs: Buffer,
pub arena: Buffer,
}
/// Wire-contract description — the TS loader verifies this against
/// src/types.ts before routing anything to the kernel, so an out-of-date
/// `.node` degrades to the wasm path instead of mis-decoding.
#[napi(object)]
pub struct ContractInfo {
pub abi_version: u32,
pub kernel_version: String,
pub node_kinds: Vec<String>,
pub edge_kinds: Vec<String>,
/// Languages this binary can extract (routing is still TS-side policy).
pub languages: Vec<String>,
}
/// Grammar identity for the grammar-source-parity gate: the wasm grammar and
/// the native grammar must expose identical node-kind/field tables, or
/// kernel-vs-fallback routing would be non-deterministic.
#[napi(object)]
pub struct GrammarInfo {
pub abi_version: u32,
pub node_kind_count: u32,
pub field_count: u32,
pub node_kinds: Vec<String>,
pub field_names: Vec<String>,
}
#[napi]
pub fn contract_info() -> ContractInfo {
ContractInfo {
abi_version: buffers::KERNEL_ABI_VERSION as u32,
kernel_version: env!("CARGO_PKG_VERSION").to_string(),
node_kinds: buffers::NODE_KINDS.iter().map(|s| s.to_string()).collect(),
edge_kinds: buffers::EDGE_KINDS.iter().map(|s| s.to_string()).collect(),
languages: langs::LANGUAGES.iter().map(|s| s.to_string()).collect(),
}
}
#[napi]
pub fn grammar_info(language: String) -> Option<GrammarInfo> {
let lang = langs::grammar_for(&language)?;
let node_kind_count = lang.node_kind_count();
let field_count = lang.field_count();
let node_kinds = (0..node_kind_count)
.map(|i| lang.node_kind_for_id(i as u16).unwrap_or("").to_string())
.collect();
// Field ids are 1-based in tree-sitter.
let field_names = (1..=field_count)
.map(|i| lang.field_name_for_id(i as u16).unwrap_or("").to_string())
.collect();
Some(GrammarInfo {
abi_version: lang.abi_version() as u32,
node_kind_count: node_kind_count as u32,
field_count: field_count as u32,
node_kinds,
field_names,
})
}
/// One struct node's extent for the cFnPtr sweep (mirror of the TS caller's
/// `{ id, startLine, endLine }`, with `endLine ?? startLine` applied TS-side).
#[napi(object)]
pub struct CfnptrStructIn {
pub id: String,
pub start_line: u32,
pub end_line: u32,
}
#[napi(object)]
pub struct CfnptrFileIn {
/// RAW file text, exactly as the resolver's readFile returned it.
pub text: String,
pub structs: Vec<CfnptrStructIn>,
}
#[napi(object)]
pub struct CfnptrField {
pub name: String,
pub index: u32,
pub ptr: bool,
#[napi(js_name = "type")]
pub ty: String,
}
#[napi(object)]
pub struct CfnptrStructOut {
pub id: String,
pub parsed: bool,
pub fields: Vec<CfnptrField>,
}
/// The cFnPtr extraction-sweep facts for one file — see cfnptr.rs (and the
/// TS synthesizer's `FileFacts`) for field semantics.
#[napi(object)]
pub struct CfnptrFacts {
pub fn_ptr_typedefs: Vec<String>,
pub fn_type_typedefs: Vec<String>,
pub structs: Vec<CfnptrStructOut>,
pub inline_ptr: bool,
pub inline_types: Vec<String>,
pub inline_tags: Vec<String>,
pub init_tokens: Vec<String>,
pub array_elems: Vec<String>,
pub alias_names: Vec<String>,
pub d_pairs: Vec<String>,
pub dispatch_fields: Vec<String>,
pub array_dispatch_names: Vec<String>,
pub includes: Vec<String>,
}
/// Batched cFnPtr extraction sweep (task #5 step 2): one call scans a batch
/// of files and returns their collected facts, amortizing the NAPI boundary.
/// Feature-detected by the TS loader — absent on older binaries, where the
/// synthesizer keeps its JS sweep.
#[napi]
pub fn cfnptr_scan_files(files: Vec<CfnptrFileIn>) -> Vec<CfnptrFacts> {
files
.into_iter()
.map(|f| {
let structs: Vec<cfnptr::StructExtent> = f
.structs
.into_iter()
.map(|s| cfnptr::StructExtent { id: s.id, start_line: s.start_line, end_line: s.end_line })
.collect();
let facts = cfnptr::scan_file(&f.text, &structs);
CfnptrFacts {
fn_ptr_typedefs: facts.fn_ptr_typedefs,
fn_type_typedefs: facts.fn_type_typedefs,
structs: facts
.structs
.into_iter()
.map(|s| CfnptrStructOut {
id: s.id,
parsed: s.parsed,
fields: s
.fields
.into_iter()
.map(|fl| CfnptrField { name: fl.name, index: fl.index, ptr: fl.ptr, ty: fl.ty })
.collect(),
})
.collect(),
inline_ptr: facts.inline_ptr,
inline_types: facts.inline_types,
inline_tags: facts.inline_tags,
init_tokens: facts.init_tokens,
array_elems: facts.array_elems,
alias_names: facts.alias_names,
d_pairs: facts.d_pairs,
dispatch_fields: facts.dispatch_fields,
array_dispatch_names: facts.array_dispatch_names,
includes: facts.includes,
}
})
.collect()
}
/// Debug/differential hook: the native `stripCommentsForRegex(text, 'c')`.
/// Exists so the strip differential oracle can pin the Rust stripper against
/// the TS reference directly.
#[napi]
pub fn cfnptr_strip_c(text: String) -> String {
String::from_utf8_lossy(&cfnptr::strip_c(text.as_bytes())).into_owned()
}
#[napi]
pub fn extract_file(file_path: String, content: String, language: String) -> Result<ExtractBuffers> {
let out = match language.as_str() {
"java" => java::extract(&file_path, &content).map_err(Error::from_reason)?,
"python" => python::extract(&file_path, &content).map_err(Error::from_reason)?,
"go" => go::extract(&file_path, &content).map_err(Error::from_reason)?,
"c" | "cpp" => ccpp::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
"rust" => rustlang::extract(&file_path, &content).map_err(Error::from_reason)?,
"csharp" => csharp::extract(&file_path, &content).map_err(Error::from_reason)?,
"ruby" => ruby::extract(&file_path, &content).map_err(Error::from_reason)?,
"php" => php::extract(&file_path, &content).map_err(Error::from_reason)?,
_ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
};
Ok(ExtractBuffers {
meta: out.meta.into(),
nodes: out.nodes.into(),
edges: out.edges.into(),
refs: out.refs.into(),
arena: out.arena.into(),
})
}