From eb62d3cce02312937e2e206b2c2b95465011c9a9 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sat, 5 Sep 2026 22:47:51 -0600 Subject: [PATCH 1/2] fix(kernel): mirror interface members (#1638) on the Rust path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TS half of this branch indexes property_signature / method_signature, but typescript and tsx are both in DEFAULT_ROUTED, so on any install carrying a codegraph-kernel.node the Rust walker replaces extraction and interface members stay unindexed. is_method_type matched only method_definition and TS public_field_definition, with no signature node type anywhere on this side. Four edits, mirroring the TS extractor one for one: - method_signature joins is_method_type (typescriptExtractor.methodTypes). - New is_property_type for property_signature (propertyTypes). It carries no value, so it is always a property and never reaches classify_ts_class_member. - New is_signature_method_type, guarding the method branch with `&& (!is_signature_method_type(kind) || self.inside_class_like())`. This mirrors SIGNATURE_METHOD_NODE_TYPES: inside_class_like already treats an interface as class-like, so without the guard a bare `type Handle = { stop(): void }` takes extract_method's "no class-like parent, so treat it as a free function" fallback and the file gains a phantom top-level `function stop` beside the real Handle::stop. - The branch matching property_signature and method_signature together, which hung their type annotations off the enclosing interface, becomes the property branch. The references edges survive — extract_method and extract_property each call extract_type_annotations — and now anchor on the member: Api::fetch -> PageId instead of Api -> PageId. extract_property reads the `type` field only for real field definitions and otherwise takes the generic child scan, which is the wasm behaviour including its quirk of repeating the member name rather than naming the type (#808 fixed the field case only). Parity is the contract, so correcting that has to move both sides in one commit; raised on the PR. Verified with scripts/kernel-parity.mjs over src, __tests__ and ui (626 files, wasm totals 16,308 nodes / 17,353 edges / 100,384 refs): before 452/626 byte-parity, 169 files with diffs (2,386 property and 1,174 method nodes missing in kernel, 3,560 contains edges, ~3k references on each side) after 619/626 byte-parity, 2 files with diffs Both remaining files are Dart fixtures that diverge identically before this change; no TS or TSX file diverges. --- codegraph-kernel/src/tsjs/extractors.rs | 26 ++++++++++++- codegraph-kernel/src/tsjs/mod.rs | 50 +++++++++++++++++++++---- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index f240c5e79..9973b3275 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -270,8 +270,30 @@ impl<'t> Walker<'t> { let name = self.text(name_node).to_string(); // TS/JS field definitions carry an explicit `type` field; the generic - // scan is for other languages (#808). - let type_text = node.child_by_field_name("type").map(|t| { + // scan is for other languages (#808). A `property_signature` is NOT a + // field definition, so it takes the generic scan here exactly as it does + // in extractProperty — and that scan stops on the `property_identifier`, + // making the signature repeat the name (`counts counts`) instead of + // naming the type. Reading the `type` field for it would be the better + // signature, but the two paths have to agree, so improving it is a + // change to both sides at once. + let is_ts_js_field = matches!(node.kind(), "public_field_definition" | "field_definition"); + let type_node = if is_ts_js_field { + node.child_by_field_name("type") + } else { + (0..node.named_child_count()).filter_map(|i| node.named_child(i)).find(|c| { + !matches!( + c.kind(), + "modifier" + | "modifiers" + | "identifier" + | "accessor_list" + | "accessors" + | "equals_value_clause" + ) + }) + }; + let type_text = type_node.map(|t| { let raw = self.text(t); raw.strip_prefix(':').unwrap_or(raw).trim_start().to_string() }); diff --git a/codegraph-kernel/src/tsjs/mod.rs b/codegraph-kernel/src/tsjs/mod.rs index afe6361d5..a76c4d485 100644 --- a/codegraph-kernel/src/tsjs/mod.rs +++ b/codegraph-kernel/src/tsjs/mod.rs @@ -55,10 +55,32 @@ impl Variant { /// typescriptExtractor.methodTypes / javascriptExtractor.methodTypes. fn is_method_type(v: Variant, kind: &str) -> bool { kind == "method_definition" - || (v.is_ts() && kind == "public_field_definition") + || (v.is_ts() && matches!(kind, "public_field_definition" | "method_signature")) || (!v.is_ts() && kind == "field_definition") } +/// typescriptExtractor.propertyTypes. The interface counterpart of +/// `public_field_definition`: it carries no value, so it is always a property +/// and never goes through classify_ts_class_member (#1638). +fn is_property_type(v: Variant, kind: &str) -> bool { + v.is_ts() && kind == "property_signature" +} + +/// Method node types that spell a SIGNATURE — a declaration with no body (#1638). +/// +/// They are a method of whatever type declares them and nothing on their own, so +/// they must not take `extract_method`'s "no class-like parent, so treat it as a +/// free function" fallback. The other method types can: a `method_definition` +/// outside a class really is a function. This one appears outside a class only +/// inside a type literal (`type Handle = { stop(): void }`), whose members +/// `extract_ts_type_alias_members` already extracts and attaches to the alias +/// (#359) — take the fallback and the file gains a phantom top-level +/// `function stop` beside the real `Handle::stop`. Mirrors the TS extractor's +/// SIGNATURE_METHOD_NODE_TYPES (extraction/tree-sitter.ts). +fn is_signature_method_type(kind: &str) -> bool { + kind == "method_signature" +} + fn is_function_type(kind: &str) -> bool { matches!(kind, "function_declaration" | "arrow_function" | "function_expression") } @@ -622,7 +644,9 @@ impl<'t> Walker<'t> { } else if is_class_type(self.variant, kind) { self.extract_class(node); skip_children = true; - } else if is_method_type(self.variant, kind) { + } else if is_method_type(self.variant, kind) + && (!is_signature_method_type(kind) || self.inside_class_like()) + { if classify_ts_class_member(node) == Member::Property { let prop = self.extract_property(node); if let (Some((row, name)), Some(value)) = (prop, node.child_by_field_name("value")) { @@ -664,12 +688,22 @@ impl<'t> Walker<'t> { self.extract_call(node); } else if kind == "new_expression" { self.extract_instantiation(node); - } else if self.variant.is_ts() - && matches!(kind, "property_signature" | "method_signature") - && self.inside_class_like() - { - let parent = self.top_row(); - self.extract_type_annotations(node, parent); + } else if is_property_type(self.variant, kind) && self.inside_class_like() { + // NOTE: `property_signature` / `method_signature` used to be handled + // here together, hanging their type annotations off the ENCLOSING + // INTERFACE — the only anchor available while the members themselves + // went unextracted. Since #1638 `method_signature` is a method type + // and `property_signature` a property type, so the method branch + // above claims the first (under the same inside_class_like guard + // this branch had) and this one extracts the second as a real node. + // The `references` edges survive — extract_method and + // extract_property each call extract_type_annotations — but now hang + // off the member, the more precise anchor: `Api::fetch → PageId` + // says which member wants the type, where `Api → PageId` only said + // the file did. + self.extract_property(node); + self.scan_fn_ref_subtree(node, 0); + skip_children = true; } if !skip_children { From b7cb38f41f40bca86432c6070f0e37ff286e3616 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sat, 5 Sep 2026 22:53:03 -0600 Subject: [PATCH 2/2] fix(extraction): an interface property's signature names its type, not its name twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `interface Stats { counts: Record }` extracted `counts` with signature "counts counts". extractProperty reads the explicit `type` field only for public_field_definition / field_definition and otherwise takes a generic named-child scan (#808, aimed at fields whose other children are the name and an initializer VALUE). A property_signature missed that test, so it took the scan — and the scan's exclusion list covers `identifier` but not the `property_identifier` an interface member is named with, so it stopped on the name node and the type annotation was never read. #808 targeted field definitions carrying initializer values; interface members could not reach this code path when it was written, so this is a gap rather than a decision. Fixed on both paths in one commit: the kernel and wasm extractors have to agree or scripts/kernel-parity.mjs fails, and the previous commit's port had mirrored the quirk deliberately for that reason. The test is named explicitly rather than folded into the field test, so no other language's property_declaration moves off the generic scan. The whole affected set is nodes #1638 introduces — before it no node existed for a property_signature on either path — so no signature that ships today changes. Verified, both paths, on `interface Stats { counts: Record; label: string; fetch(id: string): Promise }`: wasm counts => "Record counts" kernel counts => "Record counts" scripts/kernel-parity.mjs over src, __tests__ and ui holds at 619/626 byte-parity, the same 2 pre-existing Dart fixtures. --- codegraph-kernel/src/tsjs/extractors.rs | 19 +++++++++++-------- src/extraction/tree-sitter.ts | 12 +++++++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index 9973b3275..766769cef 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -270,14 +270,17 @@ impl<'t> Walker<'t> { let name = self.text(name_node).to_string(); // TS/JS field definitions carry an explicit `type` field; the generic - // scan is for other languages (#808). A `property_signature` is NOT a - // field definition, so it takes the generic scan here exactly as it does - // in extractProperty — and that scan stops on the `property_identifier`, - // making the signature repeat the name (`counts counts`) instead of - // naming the type. Reading the `type` field for it would be the better - // signature, but the two paths have to agree, so improving it is a - // change to both sides at once. - let is_ts_js_field = matches!(node.kind(), "public_field_definition" | "field_definition"); + // scan is for other languages (#808). A `property_signature` (an + // interface member, #1638) carries a `type` field and no value, so it + // reads the type field too: the generic scan's exclusion list covers + // `identifier` but not the `property_identifier` an interface member is + // named with, so it would stop on the name and make the signature repeat + // it (`counts counts`) instead of naming the type. Mirrors + // extractProperty's isTsJsField. + let is_ts_js_field = matches!( + node.kind(), + "public_field_definition" | "field_definition" | "property_signature" + ); let type_node = if is_ts_js_field { node.child_by_field_name("type") } else { diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 8dbaee09b..3328d56c6 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -2049,8 +2049,18 @@ export class TreeSitterExtractor { // and the initializer VALUE, which the generic finder below would // wrongly pick — so fields use the type field only (#808). Other // languages (C# property_declaration) keep the generic scan. + // + // A `property_signature` (an interface member, #1638) carries a `type` + // field and no value, so it reads the type field too. It cannot take the + // generic scan: that scan's exclusion list covers `identifier` but not the + // `property_identifier` an interface member is named with, so it stops on + // the name and `interface Stats { counts: Record }` yields + // `signature: "counts counts"` instead of the type. Named explicitly + // rather than folded into the field test so no other language's + // `property_declaration` moves off the generic scan. const isTsJsField = - node.type === 'public_field_definition' || node.type === 'field_definition'; + node.type === 'public_field_definition' || node.type === 'field_definition' + || node.type === 'property_signature'; const typeNode = isTsJsField ? getChildByField(node, 'type') : node.namedChildren.find(