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
62 changes: 59 additions & 3 deletions crates/codegraph-core/src/extractors/dart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1113,8 +1113,18 @@ fn handle_dart_selector(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
/// is `None` for a bare call (Layout C) or when the preceding token isn't a
/// plain identifier/type_identifier (a chained call's intermediate receiver
/// or a subscript-indexed receiver).
/// A `.method` access (`unconditional_assignable_selector`) and a null-aware
/// `?.method` access (`conditional_assignable_selector`) are otherwise
/// identical for call-resolution purposes — both wrap a plain `identifier`
/// naming the method — so every lookup below tries either wrapper (#2476).
/// Mirrors `findDartAssignableSelector` in `src/extractors/dart.ts`.
fn find_dart_assignable_selector<'a>(node: &Node<'a>) -> Option<Node<'a>> {
find_child(node, "unconditional_assignable_selector")
.or_else(|| find_child(node, "conditional_assignable_selector"))
}

fn resolve_dart_selector_call(node: &Node, source: &[u8]) -> Option<(String, Option<String>)> {
if let Some(unconditional) = find_child(node, "unconditional_assignable_selector") {
if let Some(unconditional) = find_dart_assignable_selector(node) {
let id = find_child(&unconditional, "identifier")?;
let receiver = find_dart_selector_receiver(node, source);
return Some((node_text(&id, source).to_string(), receiver));
Expand All @@ -1133,7 +1143,7 @@ fn resolve_dart_selector_call(node: &Node, source: &[u8]) -> Option<(String, Opt
let prev_sibling = prev_sibling?;

if prev_sibling.kind() == "selector" {
let unc2 = find_child(&prev_sibling, "unconditional_assignable_selector")?;
let unc2 = find_dart_assignable_selector(&prev_sibling)?;
let id2 = find_child(&unc2, "identifier")?;
let receiver = find_dart_selector_receiver(&prev_sibling, source);
return Some((node_text(&id2, source).to_string(), receiver));
Expand Down Expand Up @@ -1232,7 +1242,11 @@ fn handle_dart_call_expression(node: &Node, source: &[u8], symbols: &mut FileSym
let name = node_text(&func, source).to_string();
push_simple_call(symbols, node, name);
}
"member_expression" => {
// `null_aware_member_expression` (`a?.b()`) is otherwise identical to
// `member_expression` (`a.b()`) — same `object`/`property` fields —
// just a distinct node kind for the null-aware `?.` accessor
// (confirmed by parsing `a?.b();` with tree-sitter-dart 0.2; #2476).
"member_expression" | "null_aware_member_expression" => {
let Some(property) = func.child_by_field_name("property") else {
return;
};
Expand Down Expand Up @@ -1388,6 +1402,48 @@ mod tests {
}
}

// #2476: `a?.b()` parses its callee as a `null_aware_member_expression`
// node — otherwise identical to `member_expression` (same object/property
// fields), but a distinct node kind `handle_dart_call_expression` never
// matched, so a null-aware method call was silently extracted as ZERO
// calls (not misresolved — dropped entirely).
mod null_aware_calls {
use super::*;

#[test]
fn extracts_a_null_aware_method_call() {
let s = parse_dart("void f() {\n a?.b();\n}");
assert!(
s.calls.iter().any(|c| c.name == "b"),
"expected a call named 'b'; got: {:?}",
s.calls
);
}

#[test]
fn sets_a_this_prefixed_receiver_for_a_bare_identifier_object() {
let s = parse_dart("void f() {\n a?.b();\n}");
let call = s.calls.iter().find(|c| c.name == "b");
assert_eq!(call.and_then(|c| c.receiver.as_deref()), Some("this.a"));
}

#[test]
fn resolves_each_call_in_a_null_aware_chained_sequence() {
let s = parse_dart("void main() {\n obj?.method1()?.method2();\n}");
let names: Vec<&str> = s.calls.iter().map(|c| c.name.as_str()).collect();
assert!(
names.contains(&"method1"),
"missing method1; got: {:?}",
names
);
assert!(
names.contains(&"method2"),
"missing method2; got: {:?}",
names
);
}
}

// #2082: function_signature/method_signature and function_body are
// SIBLING nodes in tree-sitter-dart, not parent-child, so end_line must
// be measured through to the sibling body.
Expand Down
19 changes: 17 additions & 2 deletions src/extractors/dart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,17 @@ interface DartSelectorCall {
receiver?: string;
}

// A `.method` access (`unconditional_assignable_selector`) and a null-aware
// `?.method` access (`conditional_assignable_selector`) are otherwise
// identical for call-resolution purposes — both wrap a plain `identifier`
// naming the method — so every lookup below tries either wrapper (#2476).
function findDartAssignableSelector(node: TreeSitterNode): TreeSitterNode | null {
return (
findChild(node, 'unconditional_assignable_selector') ||
findChild(node, 'conditional_assignable_selector')
);
}

// Look for the identifier this selector belongs to, plus (for a genuine
// `.method` access) its receiver, for typeMap-based call resolution (#2319).
// Three layouts are possible depending on grammar version and call shape:
Expand All @@ -916,8 +927,12 @@ interface DartSelectorCall {
// wrapping call_expression — #2082). This is a bare call, not a
// receiver+method pair — the identifier IS the callee's own name, so
// no receiver is produced.
// A/B both apply identically to a null-aware `?.method` access — confirmed
// by parsing `a?.b();`, which produces the exact same Layout B shape as
// `a.b();` with `conditional_assignable_selector` in place of
// `unconditional_assignable_selector` (#2476).
function resolveDartSelectorCall(node: TreeSitterNode): DartSelectorCall | null {
const unconditional = findChild(node, 'unconditional_assignable_selector');
const unconditional = findDartAssignableSelector(node);
if (unconditional) {
const id = findChild(unconditional, 'identifier');
if (!id) return null;
Expand Down Expand Up @@ -947,7 +962,7 @@ function resolveDartSelectorCall(node: TreeSitterNode): DartSelectorCall | null
if (!prevSibling) return null;

if (prevSibling.type === 'selector') {
const unc2 = findChild(prevSibling, 'unconditional_assignable_selector');
const unc2 = findDartAssignableSelector(prevSibling);
const id2 = unc2 ? findChild(unc2, 'identifier') : null;
if (!id2) return null;
const receiver = findDartSelectorReceiver(prevSibling);
Expand Down
31 changes: 31 additions & 0 deletions tests/parsers/dart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,37 @@ import 'package:flutter/material.dart';`);
});
});

// #2476: `a?.b()` uses a distinct `conditional_assignable_selector` node
// (`?.`) instead of the `unconditional_assignable_selector` (`.`) ordinary
// member access uses — resolveDartSelectorCall's Layout A/B checks only
// looked for the unconditional wrapper, so a null-aware method call was
// silently extracted as ZERO calls (not misresolved — dropped entirely).
describe('#2476: null-aware (conditional) method call extraction', () => {
it('extracts a null-aware method call', () => {
const symbols = parseDart(`void f() {
a?.b();
}`);
expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'b' }));
});

it('sets a this-prefixed receiver for a bare identifier object', () => {
const symbols = parseDart(`void f() {
a?.b();
}`);
const call = symbols.calls.find((c) => c.name === 'b');
expect(call?.receiver).toBe('this.a');
});

it('resolves each call in a null-aware chained sequence', () => {
const symbols = parseDart(`void main() {
obj?.method1()?.method2();
}`);
const names = symbols.calls.map((c) => c.name);
expect(names).toContain('method1');
expect(names).toContain('method2');
});
});

// #2082: multi-line function/method endLine truncation — tree-sitter-dart
// splits a function's signature and body into SIBLING nodes
// (function_signature/method_signature + function_body), not a
Expand Down
Loading