Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 138 additions & 7 deletions crates/codegraph-core/src/extractors/dart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,14 @@ fn handle_dart_class(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
None => return,
};
let class_name = node_text(&name_node, source).to_string();
let mut children: Vec<Definition> = Vec::new();

// Extract methods
if let Some(body) = node
.child_by_field_name("body")
.or_else(|| find_child(node, "class_body"))
{
extract_dart_class_methods(&body, &class_name, source, symbols);
extract_dart_class_methods(&body, &class_name, source, symbols, &mut children);
}

// Extract inheritance
Expand Down Expand Up @@ -124,7 +125,7 @@ fn handle_dart_class(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
decorators: None,
complexity: None,
cfg: None,
children: None,
children: opt_children(children),
bodyless: None,
content_hash: None,
accessor_kind: None,
Expand All @@ -136,6 +137,7 @@ fn extract_dart_class_methods(
class_name: &str,
source: &[u8],
symbols: &mut FileSymbols,
children: &mut Vec<Definition>,
) {
for i in 0..body.child_count() {
if let Some(member) = body.child(i) {
Expand Down Expand Up @@ -185,14 +187,25 @@ fn extract_dart_class_methods(
// shape before skipping — otherwise every
// fixture/codebase using this idiomatic form
// silently loses its constructors/abstract methods.
let bodyless_sig = find_child(&member, "declaration").and_then(|d| {
find_child(&d, "constructor_signature")
.or_else(|| find_child(&d, "method_signature"))
.or_else(|| find_child(&d, "function_signature"))
let field_decl = find_child(&member, "declaration");
let bodyless_sig = field_decl.as_ref().and_then(|d| {
find_child(d, "constructor_signature")
.or_else(|| find_child(d, "method_signature"))
.or_else(|| find_child(d, "function_signature"))
});
match bodyless_sig {
Some(s) => s,
None => continue,
None => {
// Not a bodyless signature — a genuine
// field declaration (#2475). Record its
// identifier(s) as `children` before
// moving on; there is no signature node
// to fall through to below.
if let Some(d) = &field_decl {
extract_dart_field_children(d, source, children);
}
continue;
}
}
}
}
Expand Down Expand Up @@ -224,6 +237,38 @@ fn extract_dart_class_methods(
}
}

/// Push a `Definition` (kind "property") for every identifier declared by a
/// class-field `declaration` node into `children` — mirrors
/// `handle_dart_field_decl_type_map`'s identical `initialized_identifier_list
/// -> initialized_identifier -> identifier` traversal, including its
/// handling of a comma-separated multi-field declaration (`final Foo a, b;`
/// declares BOTH `a` and `b`). Every real field-declaration shape nests its
/// identifier two levels deep — Dart requires a var/final/const/late/type
/// modifier on every field, so `identifier` is never a direct child of
/// `declaration` itself (#2475). Mirrors the TS fix in
/// `extractDartClassMembers`.
fn extract_dart_field_children(decl: &Node, source: &[u8], children: &mut Vec<Definition>) {
let Some(list) = find_child(decl, "initialized_identifier_list") else {
return;
};
let line = start_line(decl);
for i in 0..list.child_count() {
let Some(item) = list.child(i) else {
continue;
};
if item.kind() != "initialized_identifier" {
continue;
}
if let Some(name_node) = find_child(&item, "identifier") {
children.push(child_def(
node_text(&name_node, source).to_string(),
"property",
line,
));
}
}
}

fn find_dart_signature_child<'a>(node: &Node<'a>) -> Option<Node<'a>> {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
Expand Down Expand Up @@ -1886,4 +1931,90 @@ mod tests {
);
}
}

// #2475: every real field-declaration shape nests its identifier TWO
// levels deep (declaration -> initialized_identifier_list ->
// initialized_identifier -> identifier), never a direct child of
// declaration itself — the class Definition's `children` list was
// always empty for real code.
mod class_field_children {
use super::*;

#[test]
fn records_a_final_field_as_a_child() {
let s = parse_dart(
"class UserService {\n final UserRepository _repo;\n UserService(this._repo);\n}",
);
let class = s
.definitions
.iter()
.find(|d| d.name == "UserService")
.expect("missing UserService definition");
let children = class
.children
.as_ref()
.expect("expected UserService to have children");
let field = children.iter().find(|c| c.name == "_repo");
assert!(field.is_some(), "missing _repo child; got: {:?}", children);
assert_eq!(field.unwrap().kind, "property");
}

#[test]
fn records_a_plain_var_field_and_a_late_field() {
let s = parse_dart("class A {\n UserRepository repo;\n late Foo _f;\n}");
let class = s.definitions.iter().find(|d| d.name == "A").unwrap();
let children = class.children.as_ref().expect("expected children");
assert!(children.iter().any(|c| c.name == "repo"));
assert!(children.iter().any(|c| c.name == "_f"));
}

#[test]
fn records_every_identifier_in_a_comma_separated_multi_field_declaration() {
let s = parse_dart("class A {\n final Foo a, b;\n}");
let class = s.definitions.iter().find(|d| d.name == "A").unwrap();
let children = class.children.as_ref().expect("expected children");
assert!(
children.iter().any(|c| c.name == "a"),
"missing a; got: {:?}",
children
);
assert!(
children.iter().any(|c| c.name == "b"),
"missing b; got: {:?}",
children
);
}

#[test]
fn still_records_methods_alongside_fields() {
let s = parse_dart(
"class UserService {\n final UserRepository _repo;\n UserService(this._repo);\n void createUser() {}\n}",
);
assert!(
s.definitions
.iter()
.any(|d| d.name == "UserService.createUser" && d.kind == "method"),
"missing UserService.createUser method definition; got: {:?}",
s.definitions
);
let class = s
.definitions
.iter()
.find(|d| d.name == "UserService")
.unwrap();
let children = class.children.as_ref().expect("expected children");
assert!(children.iter().any(|c| c.name == "_repo"));
}

#[test]
fn a_class_with_no_fields_has_no_children() {
let s = parse_dart("class Empty {\n void run() {}\n}");
let class = s.definitions.iter().find(|d| d.name == "Empty").unwrap();
assert!(
class.children.is_none(),
"expected no children; got: {:?}",
class.children
);
}
}
}
33 changes: 23 additions & 10 deletions src/extractors/dart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,29 @@ function extractDartClassMembers(
// node to read here — inferring one from the initializer is a separate,
// out-of-scope problem — so this is a no-op for that shape.
handleDartFieldDeclTypeMap(member, className, ctx.typeMap);
// Field declarations
for (let j = 0; j < member.childCount; j++) {
const decl = member.child(j);
if (decl?.type === 'identifier') {
children.push({
name: decl.text,
kind: 'property',
line: member.startPosition.row + 1,
});
break;
// Field declarations — every real field-declaration shape (`final Foo
// x;`, `Foo x;`, `late Foo x;`, `Foo? x;`) nests its identifier TWO
// levels deep (`declaration -> initialized_identifier_list ->
// initialized_identifier -> identifier`); Dart requires every field to
// carry a var/final/const/late/type modifier, so `identifier` is never
// a direct child of `declaration` itself — a direct-child scan here
// silently found nothing for any real field, leaving `children` always
// empty (#2475). Mirrors `handleDartFieldDeclTypeMap`'s identical
// traversal, including its handling of a comma-separated multi-field
// declaration (`final Foo a, b;` declares BOTH `a` and `b`).
const identifierList = findChild(member, 'initialized_identifier_list');
if (identifierList) {
for (let j = 0; j < identifierList.childCount; j++) {
const item = identifierList.child(j);
if (item?.type !== 'initialized_identifier') continue;
const nameNode = findChild(item, 'identifier');
if (nameNode) {
children.push({
name: nameNode.text,
kind: 'property',
line: member.startPosition.row + 1,
});
}
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions tests/parsers/dart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,4 +590,65 @@ void main() {
expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'createUser' }));
});
});

// #2475: every real field-declaration shape nests its identifier TWO
// levels deep (declaration -> initialized_identifier_list ->
// initialized_identifier -> identifier), never a direct child of
// declaration itself — the class definition's `children` list was always
// empty for real code.
describe('#2475: class field children', () => {
it('records a final field as a child', () => {
const symbols = parseDart(`class UserService {
final UserRepository _repo;
UserService(this._repo);
}`);
const cls = symbols.definitions.find((d) => d.name === 'UserService');
expect(cls?.children).toBeDefined();
expect(cls?.children).toContainEqual(
expect.objectContaining({ name: '_repo', kind: 'property' }),
);
});

it('records a plain var field and a late field', () => {
const symbols = parseDart(`class A {
UserRepository repo;
late Foo _f;
}`);
const cls = symbols.definitions.find((d) => d.name === 'A');
const names = cls?.children?.map((c) => c.name) ?? [];
expect(names).toContain('repo');
expect(names).toContain('_f');
});

it('records every identifier in a comma-separated multi-field declaration', () => {
const symbols = parseDart(`class A {
final Foo a, b;
}`);
const cls = symbols.definitions.find((d) => d.name === 'A');
const names = cls?.children?.map((c) => c.name) ?? [];
expect(names).toContain('a');
expect(names).toContain('b');
});

it('still records methods alongside fields', () => {
const symbols = parseDart(`class UserService {
final UserRepository _repo;
UserService(this._repo);
void createUser() {}
}`);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: 'UserService.createUser', kind: 'method' }),
);
const cls = symbols.definitions.find((d) => d.name === 'UserService');
expect(cls?.children).toContainEqual(expect.objectContaining({ name: '_repo' }));
});

it('a class with no fields has no children', () => {
const symbols = parseDart(`class Empty {
void run() {}
}`);
const cls = symbols.definitions.find((d) => d.name === 'Empty');
expect(cls?.children).toBeUndefined();
});
});
});
Loading