fix(extraction): index TypeScript interface members (#1638) (#1780)

Land upstream #1686 (maxmilian + bompus kernel/CG-28 follow-ups)
onto current main. tree-sitter-typescript interface members
(method_signature / property_signature) were never listed in the
TS extractor, so platform .d.ts APIs had no declaration nodes for
call edges. Mirrors on the Rust kernel path; keeps CG-28 damping
for pure-interface declaration files; filters damped files from
the explore RWR seed set.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 11:27:05 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 8c047342cd
commit ee83636acb
10 changed files with 375 additions and 49 deletions
+27 -2
View File
@@ -302,8 +302,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()
});
+42 -8
View File
@@ -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" | "generator_function_declaration" | "arrow_function" | "function_expression" | "generator_function")
}
@@ -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 {