diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index f240c5e79..766769cef 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -270,8 +270,33 @@ 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` (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 { + (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 { 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(