mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
Issue #2338: a Dart getter read (`x.area`, `s.label`) linked nothing, so `codegraph callers area` was empty while `x.grow()` worked, and a member an extension adds to an ENUM (`s.shout()` through `extension ShapeInfo on Shape`) never resolved. Issue #2327: `callers Report` saw only parameter and return types; the type an extension is `on`, field types, and types named in bodies and initializers (`Future<Report?>.value(null)`, riverpod's `final reportProvider = Family<Report?, String>()`) recorded nothing. Causes: - The Dart body walker only emitted calls (a selector with an argument list); a member read is a selector without one, so it produced no ref. - For a typed receiver, resolveMethodOnType looks for `Type::member` and the type's supertype edges; an extension's member is `ShapeInfo::shout`. On a class receiver the call fell through to name matching, which found a unique `twice` by luck; on an enum the "type the project doesn't declare" guard (it only counts classes) returned null. - Type references were taken from function and method signatures only. Fix (TS extractor and the Rust kernel, mirrored): - A member read `x.area` / `x?.area` / `Type.getter` whose receiver is a plain name, and that is not a call, is sent as a `references` ref named `x.area` on the member. The resolver (matchDartMemberRead, ahead of every other strategy) types the receiver — a static read through a project type, a parameter or local in scope, else a field of the enclosing class — and links the getter that type reaches: declared on it or a type it extends, mixes in or implements, else added by an extension on one of those (an unnamed extension only in its own library). The edge is `calls`: a Dart getter is a method node, reading it runs it, and `calls` is what callers, impact and explore's flow follow (as Ruby's paren-less `x.area` and VB.NET's paren-less calls through #2355 already are). A field read, a getter of a type outside the project and an untyped receiver link nothing; there is no name-only fallback. Kotlin/Swift/C#/TS link no instance property reads today. - resolveMethodOnType, for Dart, falls back to a member an extension on the type (or a supertype) adds when no declaration of the type has it. - `references` refs for the type an extension is `on`, a field's declared type and initializer (from its class — Dart fields mint no nodes), an enum field, a top-level variable's type (from the file), a constant's initializer (from the constant), and every type a function body names: locals, generic arguments, casts, type tests, catch clauses. Skipped: a `new`/`const` constructor's class (already instantiates/calls), an import prefix (`p` in `p.Foo`), built-ins, and lowercase names, which in Dart are built-ins (`num`, `dynamic`) or error-recovery artifacts around syntax the grammar predates (dot shorthands). A class whose field holds its own type does not get a self-edge. Verification: __tests__/dart-getter-reads.test.ts (7 tests) and __tests__/dart-type-positions.test.ts (3 tests) fail 8/10 on main (both the TS path and main's kernel) and pass on both arms here; the 2 that pass on main are the controls. New kernel parity fixture TortureReadsTypes.dart (+CRLF); kernel-dart-parity passes with CODEGRAPH_KERNEL_EXPECT=1, and scripts/kernel-parity.mjs over bloc/riverpod/flutter-samples is 0 diffs (2,103 files byte-parity, 300 parse-error files defer as before). tsc and build clean; the issues' repros now list c/a/b for area/label/shout and Holder, ReportX, reportProvider and run for Report. Validation (kernel loaded, main vs this branch): nodes unchanged, 0 edges removed, no new self-loops. - felangel/bloc: 16,372 -> 18,276 edges (+550 getter-read calls, +1,354 type references) - rrousselGit/riverpod: 76,432 -> 81,909 (+598 getter reads, +13 extension-method calls, +4,866 type references) - flutter/samples: 26,133 -> 26,943 (+142 getter reads, +668 type refs) Spot-checked 16 getter reads and 16 type references against source. Index time within noise on riverpod (median of 5 interleaved runs); reads that link nothing are kept for re-resolution like failed calls (riverpod db 92.9 -> 103.2 MB). No EXTRACTION_VERSION bump: it already moved to 28 this release. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -628,12 +628,61 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
});
|
||||
let name = self.text(name_node).to_string();
|
||||
self.create_node("constant", &name, node, Extra { signature, ..Default::default() });
|
||||
let constant = self.create_node("constant", &name, node, Extra { signature, ..Default::default() });
|
||||
// The types its initializer names are the constant's (#2327).
|
||||
if let Some(constant) = constant {
|
||||
let mut value = name_node.next_named_sibling();
|
||||
while let Some(v) = value {
|
||||
self.type_refs_dart(v, constant);
|
||||
value = v.next_named_sibling();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.scan_fn_ref_subtree(node, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// A field's declared type and initializer types are its class's (Dart
|
||||
// fields mint no nodes); a top-level variable's are the file's (#2327).
|
||||
// The visitNode hook's `declaration` / `program` rows (dart.ts).
|
||||
// (Parent lookups walk down from the root, so only the node kinds that
|
||||
// can be these positions ask for theirs.)
|
||||
if node.kind() == "declaration" {
|
||||
let in_body = node
|
||||
.parent()
|
||||
.map(|p| matches!(p.kind(), "class_body" | "extension_body" | "enum_body"))
|
||||
.unwrap_or(false);
|
||||
let mut cursor = node.walk();
|
||||
let kids: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
|
||||
let is_field = in_body
|
||||
&& kids
|
||||
.iter()
|
||||
.any(|c| matches!(c.kind(), "initialized_identifier_list" | "static_final_declaration_list"));
|
||||
if is_field {
|
||||
let owner = self.top_row();
|
||||
for child in kids {
|
||||
match child.kind() {
|
||||
"initialized_identifier_list" => self.initializer_type_refs_dart(child, owner),
|
||||
"static_final_declaration_list" => {}
|
||||
_ => self.type_refs_dart(child, owner),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let top_level_type =
|
||||
matches!(node.kind(), "type_identifier" | "type_arguments" | "function_type" | "record_type");
|
||||
if (top_level_type || node.kind() == "initialized_identifier_list")
|
||||
&& node.parent().map(|p| p.kind() == "program").unwrap_or(false)
|
||||
{
|
||||
let owner = self.top_row();
|
||||
if top_level_type {
|
||||
self.type_refs_dart(node, owner);
|
||||
} else {
|
||||
self.initializer_type_refs_dart(node, owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// maybeCaptureFnRefs (:990) — the double-walk fn-ref twin source.
|
||||
self.maybe_capture_fn_refs(node);
|
||||
|
||||
@@ -1190,6 +1239,14 @@ impl<'t> Walker<'t> {
|
||||
// --- extractInheritance — the dart rows (:5368-5393, :5437-5459) ------
|
||||
|
||||
fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
|
||||
// The type an extension is `on` — a `references` edge (#2327).
|
||||
if node.kind() == "extension_declaration" {
|
||||
let mut oc = node.walk();
|
||||
let on_types: Vec<Node<'t>> = node.children_by_field_name("class", &mut oc).collect();
|
||||
for on_type in on_types {
|
||||
self.type_refs_dart(on_type, class_row);
|
||||
}
|
||||
}
|
||||
let mut cursor = node.walk();
|
||||
let kids: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
|
||||
for child in kids {
|
||||
@@ -1287,6 +1344,100 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- type positions outside signatures (#2327; dart.ts pushDartTypeRefs) ---
|
||||
|
||||
/// isDartTypeName (dart.ts) — UpperCamelCase (`/^[_$]*[A-Z]/`: lowercase
|
||||
/// is a built-in or an error-recovery artifact), not a built-in, not an
|
||||
/// import prefix (`p` in `p.Foo`), not the class a `new`/`const`
|
||||
/// constructor call names.
|
||||
fn is_type_name_dart(&self, node: Node<'t>) -> bool {
|
||||
let text = self.text(node);
|
||||
let upper = text
|
||||
.trim_start_matches(&['_', '$'][..])
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_uppercase())
|
||||
.unwrap_or(false);
|
||||
if !upper || is_builtin_type(text) {
|
||||
return false;
|
||||
}
|
||||
if node.next_sibling().map(|s| s.kind() == ".").unwrap_or(false) {
|
||||
return false;
|
||||
}
|
||||
!node
|
||||
.parent()
|
||||
.map(|p| matches!(p.kind(), "new_expression" | "const_object_expression"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// pushDartTypeRefs (dart.ts) — every type named inside `node`.
|
||||
fn type_refs_dart(&mut self, node: Node<'t>, from_row: u32) {
|
||||
stack_guard!();
|
||||
if node.kind() == "type_identifier" {
|
||||
if self.is_type_name_dart(node) {
|
||||
let name = self.text(node).to_string();
|
||||
self.push_ref_at(from_row, &name, "references", node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut cursor = node.walk();
|
||||
let kids: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
|
||||
for c in kids {
|
||||
self.type_refs_dart(c, from_row);
|
||||
}
|
||||
}
|
||||
|
||||
/// pushDartInitializerTypeRefs (dart.ts) — each `initialized_identifier`'s
|
||||
/// value (every named child after its name).
|
||||
fn initializer_type_refs_dart(&mut self, list: Node<'t>, from_row: u32) {
|
||||
let mut cursor = list.walk();
|
||||
let entries: Vec<Node<'t>> = list.named_children(&mut cursor).collect();
|
||||
for entry in entries {
|
||||
if entry.kind() != "initialized_identifier" {
|
||||
continue;
|
||||
}
|
||||
let mut ec = entry.walk();
|
||||
let parts: Vec<Node<'t>> = entry.named_children(&mut ec).skip(1).collect();
|
||||
for part in parts {
|
||||
self.type_refs_dart(part, from_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// dartMemberRead (dart.ts) — `x.area` / `x?.area` not followed by a call's
|
||||
/// argument part, receiver a plain name: a `references` ref `x.area` on
|
||||
/// `area`, which the resolver links to a getter only (#2338).
|
||||
fn extract_member_read(&mut self, node: Node<'t>) {
|
||||
if node.kind() != "selector" {
|
||||
return;
|
||||
}
|
||||
let mut cursor = node.walk();
|
||||
let accessor = node.named_children(&mut cursor).find(|c| {
|
||||
matches!(c.kind(), "unconditional_assignable_selector" | "conditional_assignable_selector")
|
||||
});
|
||||
let member = accessor.and_then(|a| {
|
||||
let mut ac = a.walk();
|
||||
let found = a.named_children(&mut ac).find(|c| c.kind() == "identifier");
|
||||
found
|
||||
});
|
||||
let Some(member) = member else { return };
|
||||
let Some(receiver) = node.prev_named_sibling() else { return };
|
||||
if receiver.kind() != "identifier" {
|
||||
return;
|
||||
}
|
||||
if let Some(next) = node.next_named_sibling() {
|
||||
if next.kind() == "selector" {
|
||||
let mut nc = next.walk();
|
||||
if next.named_children(&mut nc).any(|c| c.kind() == "argument_part") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let name = format!("{}.{}", self.text(receiver), self.text(member));
|
||||
let reader = self.top_row();
|
||||
self.push_ref_at(reader, &name, "references", member);
|
||||
}
|
||||
|
||||
// --- visitFunctionBody (:5129-5286) — dart rows -----------------------
|
||||
|
||||
fn visit_body(&mut self, node: Node<'t>) {
|
||||
@@ -1308,6 +1459,14 @@ impl<'t> Walker<'t> {
|
||||
|
||||
self.extract_static_member_ref(node);
|
||||
|
||||
// A getter read `x.area` (#2338); a type the body names (#2327).
|
||||
self.extract_member_read(node);
|
||||
if kind == "type_identifier" && self.is_type_name_dart(node) {
|
||||
let name = self.text(node).to_string();
|
||||
let owner = self.top_row();
|
||||
self.push_ref_at(owner, &name, "references", node);
|
||||
}
|
||||
|
||||
if kind == "function_signature" {
|
||||
// Nested named functions (:5245) — extractFunction walks the
|
||||
// nested body itself; the enclosing walker ALSO revisits the
|
||||
|
||||
Reference in New Issue
Block a user