fix(dart): link generic calls the parser reads as two comparisons (#2375)

tree-sitter-dart parses a generic call with arguments as two comparisons
about as often as it parses it as a call, depending on the code around it:
`ref.read<Repo>(p)` comes out as
`relational_expression(relational_expression(ref.read < Repo) > (p))`.
Generic constructors (`Provider<int>((ref) => 0)`), named arguments (which
then parse as a `record_literal`) and calls after `await` or a prefix
operator are hit too. Neither extractor recorded a call for that shape,
only a member read of `ref.read` that linked nothing, so providers,
repository reads and bloc lookups written with a type argument had no
callers. Across felangel/bloc, rrousselGit/riverpod and flutter/samples
there are 2,853 such calls, against 2,836 generic calls with arguments
that parse as calls.

- Both extractors (the TS walker and the native kernel) recover the call
  at its `<` when the type argument names a type and the code is laid out
  as a call: `<` against the callee and `(` against the `>`. A comparison
  such as `a < b` is left alone. The recovered call gets what a parsed one
  does: the call at the `<`, a reference to the type argument, and no
  member read of the callee or of a prefixed type argument.
- A call chained on a recovered call keeps its bare name. The
  `BlocProvider.of().increment` encoding a parsed chain gets resolves
  through what `of` returns, which for a generic factory is its type
  parameter, so it would drop the edge the bare name finds.
- When the grammar ends an arrow function in front of a generic call
  (`(ref) => ref.watch<int>(p)` as `((ref) => ref).watch<int>(p)`), the
  receiver is read from the end of the arrow's body, so the call is
  `ref.watch` rather than a bare `watch`.

Validated with the kernel loaded: bloc +85, riverpod +1,580 and
flutter/samples +54 edges, none lost; kernel/wasm parity has 0 diffs on
all three repos.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-06 14:57:23 +00:00
committed by GitHub
parent 652c36b72a
commit 3f96f80fcb
9 changed files with 764 additions and 127 deletions
+229 -61
View File
@@ -108,6 +108,18 @@ fn is_bodiless_ctor(kind: &str) -> bool {
matches!(kind, "constant_constructor_signature" | "redirecting_factory_constructor_signature")
}
/// DART_LOWERCASE_TYPES (dart.ts) — Dart's lowercase built-in types, which a
/// type argument can name: `<int>`, `<void>`.
fn is_lowercase_type(name: &str) -> bool {
matches!(name, "int" | "double" | "num" | "bool" | "dynamic" | "void")
}
/// DART_PREFIXED (dart.ts) — what a callee can sit under in front of a
/// generic call: `await`, `-`, `!`.
fn is_prefixed(kind: &str) -> bool {
matches!(kind, "unary_expression" | "await_expression")
}
struct Scope {
row: u32,
kind: &'static str,
@@ -1048,61 +1060,15 @@ impl<'t> Walker<'t> {
return None;
}
let prev = node.prev_named_sibling()?;
if prev.kind() == "identifier" {
return Some(self.text(prev).to_string());
}
if prev.kind() == "selector" {
let mut pc = prev.walk();
let accessor = prev.named_children(&mut pc).find(|c| {
matches!(
c.kind(),
"unconditional_assignable_selector" | "conditional_assignable_selector"
)
});
if let Some(accessor) = accessor {
let mut ac = accessor.walk();
let method_id = accessor.named_children(&mut ac).find(|c| c.kind() == "identifier");
if let Some(method_id) = method_id {
let accessor_prev = prev.prev_named_sibling();
if let Some(ap) = accessor_prev {
if ap.kind() == "identifier" {
return Some(format!("{}.{}", self.text(ap), self.text(method_id)));
}
// A constructor called with type arguments names
// its type all the same (`BlocProvider<T>.value`).
if let Some(type_name) = self.type_arguments_receiver(ap) {
return Some(format!("{}.{}", type_name, self.text(method_id)));
}
// Chained static-factory: the receiver is itself
// a call — re-encode `<inner>().<method>` when
// the chain starts capitalized (#750).
if ap.kind() == "selector" {
let mut apc = ap.walk();
if ap.named_children(&mut apc).any(|c| c.kind() == "argument_part") {
if let Some(inner) = self.callee_of_arg_part(ap) {
if starts_upper_re().is_match(&inner) {
return Some(format!("{}().{}", inner, self.text(method_id)));
}
}
}
}
}
return Some(self.text(method_id).to_string());
}
}
}
// super.method() / this.method(): prev is a bare accessor.
if matches!(
prev.kind(),
"unconditional_assignable_selector" | "conditional_assignable_selector"
) {
let mut pc = prev.walk();
let id = prev.named_children(&mut pc).find(|c| c.kind() == "identifier");
if let Some(id) = id {
return Some(self.text(id).to_string());
}
}
return None;
return self.callee_name(prev);
}
// A generic call the grammar read as two comparisons
// (`ref.read<Repo>(p)`), named at its `<`, where a parsed call's
// argument part starts.
if node.kind() == "relational_operator" {
let (callee, _) = self.misparsed_generic_call(node)?;
return self.callee_name(callee);
}
// new_expression arm — DEAD in practice (the INSTANTIATION branch
@@ -1147,6 +1113,189 @@ impl<'t> Walker<'t> {
None
}
/// dartReceiverOf (dart.ts) — the receiver of the member access
/// `selector`: the node in front of it, or the end of an arrow function's
/// body when the grammar ended the arrow in front of a generic call
/// (`(ref) => ref.watch<int>(p)` → `((ref) => ref).watch<int>(p)`).
fn receiver_of(&self, selector: Node<'t>) -> Option<Node<'t>> {
let prev = selector.prev_named_sibling();
let body = prev
.filter(|p| p.kind() == "function_expression")
.and_then(|p| p.named_child(p.named_child_count().checked_sub(1)?));
match body {
Some(body) if body.kind() == "function_expression_body" => {
body.named_child(body.named_child_count().checked_sub(1)?)
}
_ => prev,
}
}
/// dartCallee (dart.ts) — the callee name of the call whose argument part
/// follows `prev`.
fn callee_name(&self, prev: Node<'t>) -> Option<String> {
if prev.kind() == "identifier" {
return Some(self.text(prev).to_string());
}
if prev.kind() == "selector" {
let mut pc = prev.walk();
let accessor = prev.named_children(&mut pc).find(|c| {
matches!(
c.kind(),
"unconditional_assignable_selector" | "conditional_assignable_selector"
)
});
if let Some(accessor) = accessor {
let mut ac = accessor.walk();
let method_id = accessor.named_children(&mut ac).find(|c| c.kind() == "identifier");
if let Some(method_id) = method_id {
let accessor_prev = self.receiver_of(prev);
if let Some(ap) = accessor_prev {
if ap.kind() == "identifier" {
return Some(format!("{}.{}", self.text(ap), self.text(method_id)));
}
// A constructor called with type arguments names
// its type all the same (`BlocProvider<T>.value`).
if let Some(type_name) = self.type_arguments_receiver(ap) {
return Some(format!("{}.{}", type_name, self.text(method_id)));
}
// Chained static-factory: the receiver is itself
// a call — re-encode `<inner>().<method>` when
// the chain starts capitalized (#750).
if ap.kind() == "selector" {
let mut apc = ap.walk();
if ap.named_children(&mut apc).any(|c| c.kind() == "argument_part") {
if let Some(inner) = self.callee_of_arg_part(ap) {
if starts_upper_re().is_match(&inner) {
return Some(format!("{}().{}", inner, self.text(method_id)));
}
}
}
}
}
return Some(self.text(method_id).to_string());
}
}
}
// super.method() / this.method(): prev is a bare accessor.
if matches!(
prev.kind(),
"unconditional_assignable_selector" | "conditional_assignable_selector"
) {
let mut pc = prev.walk();
let id = prev.named_children(&mut pc).find(|c| c.kind() == "identifier");
if let Some(id) = id {
return Some(self.text(id).to_string());
}
}
None
}
/// dartMisparsedGenericCall (dart.ts) — a generic call tree-sitter-dart
/// parsed as two comparisons, `(ref.read < Repo) > (p)`, found from its
/// `<`: the node its callee ends with (under any `await` or prefix
/// operator) and the identifier naming its type argument. Recovered when
/// the type argument names a type and the code is laid out as a call —
/// `<` against the callee, `(` against the `>`. A call chained on it keeps
/// its bare name (the `X().m` encoding resolves through a generic
/// factory's type parameter and finds nothing).
fn misparsed_generic_call(&self, lt: Node<'t>) -> Option<(Node<'t>, Node<'t>)> {
if lt.kind() != "relational_operator" || lt.child(0)?.kind() != "<" {
return None;
}
let inner = lt.parent()?;
let outer = inner.parent()?;
if inner.kind() != "relational_expression" || outer.kind() != "relational_expression" {
return None;
}
let left = outer.named_child(0)?;
if left.start_byte() != inner.start_byte() || left.end_byte() != inner.end_byte() {
return None;
}
let gt = outer.named_child(1)?;
if gt.kind() != "relational_operator" || gt.child(0)?.kind() != ">" {
return None;
}
let args = outer.named_child(2)?;
if !matches!(args.kind(), "parenthesized_expression" | "record_literal") || args.start_byte() != gt.end_byte() {
return None;
}
let before = lt.prev_named_sibling()?;
if before.end_byte() != lt.start_byte() {
return None;
}
// The type argument: `T`, `p.T`, `T<…>` or `p.T<…>`, and nothing else.
let head = lt.next_named_sibling()?;
if head.kind() != "identifier" {
return None;
}
let mut type_name = head;
let mut rest = head.next_named_sibling();
if let Some(sel) = rest {
let accessor = sel.named_child(0).filter(|a| a.kind() == "unconditional_assignable_selector");
if sel.kind() == "selector" {
if let Some(accessor) = accessor {
let mut ac = accessor.walk();
let name = accessor.named_children(&mut ac).find(|c| c.kind() == "identifier");
type_name = name?;
rest = sel.next_named_sibling();
}
}
}
if let Some(sel) = rest {
let only_type_args = sel.named_child_count() == 1
&& sel.named_child(0).map(|t| t.kind() == "type_arguments").unwrap_or(false);
if sel.kind() == "selector" && only_type_args {
rest = sel.next_named_sibling();
}
}
if rest.is_some() {
return None;
}
let name = self.text(type_name);
if !is_upper_camel(name) && !is_lowercase_type(name) {
return None;
}
// `await repo.load<User>(id)` and `-x.size<int>(y)` put the callee
// under the prefix.
let mut callee = before;
while is_prefixed(callee.kind()) {
let count = callee.named_child_count();
callee = callee.named_child(count.checked_sub(1)?)?;
}
Some((callee, type_name))
}
/// dartInMisparsedGenericCall (dart.ts) — whether a member access
/// (`.member` on `receiver`, followed by `next` under `parent`) belongs to
/// a misparsed generic call: its callee is called, and a prefixed type
/// argument (`p.Repo`) names a type.
fn in_misparsed_generic_call(
&self,
receiver: Node<'t>,
next: Option<Node<'t>>,
parent: Option<Node<'t>>,
) -> bool {
let mut after = next;
let mut up = parent;
while after.is_none() {
let Some(u) = up.filter(|u| is_prefixed(u.kind())) else { break };
after = u.next_named_sibling();
up = if after.is_some() { None } else { u.parent() };
}
if let Some(a) = after {
if a.kind() == "relational_operator" && self.misparsed_generic_call(a).is_some() {
return true;
}
}
if parent.map(|p| p.kind()) != Some("relational_expression") {
return false;
}
receiver
.prev_named_sibling()
.map(|b| b.kind() == "relational_operator" && self.misparsed_generic_call(b).is_some())
.unwrap_or(false)
}
/// dartCalleeOfArgPart (dart.ts:100-116).
fn callee_of_arg_part(&self, arg_part: Node<'t>) -> Option<String> {
let prev = arg_part.prev_named_sibling()?;
@@ -1213,8 +1362,15 @@ impl<'t> Walker<'t> {
if node.named_children(&mut cursor).any(|c| c.kind() == "argument_part") {
return;
}
let Some(prev) = node.prev_named_sibling() else { return };
let Some(prev) = self.receiver_of(node) else { return };
if prev.kind() == "identifier" && cap_ident_re().is_match(self.text(prev)) {
// `Map` in `x.read<Map<K, V>>(y)` parsed as comparisons is a type
// argument, which the body walker references as a type.
if let Some(before) = prev.prev_named_sibling() {
if before.kind() == "relational_operator" && self.misparsed_generic_call(before).is_some() {
return;
}
}
let name = self.text(prev).to_string();
// NO callee-of-call skip — `ConfigT.load()` double-emits
// (references + calls). Position = the IDENTIFIER (receiver).
@@ -1510,7 +1666,8 @@ impl<'t> Walker<'t> {
if receiver.kind() != "identifier" {
return;
}
if let Some(next) = node.next_named_sibling() {
let next = node.next_named_sibling();
if let Some(next) = next {
if next.kind() == "selector" {
let mut nc = next.walk();
if next.named_children(&mut nc).any(|c| c.kind() == "argument_part") {
@@ -1518,6 +1675,11 @@ impl<'t> Walker<'t> {
}
}
}
// The callee of a generic call the grammar read as comparisons
// (`ref.read<Repo>(p)`) is called; a prefixed type argument names a type.
if self.in_misparsed_generic_call(receiver, next, node.parent()) {
return;
}
let name = format!("{}.{}", self.text(receiver), self.text(member));
let reader = self.top_row();
self.push_ref_at(reader, &name, "references", member);
@@ -1544,12 +1706,18 @@ impl<'t> Walker<'t> {
self.extract_static_member_ref(node);
// A getter read `x.area` (#2338); a type the body names (#2327).
// A getter read `x.area` (#2338); a type the body names (#2327) — the
// type argument of a generic call parsed as comparisons included.
self.extract_member_read(node);
if kind == "type_identifier" && self.is_type_name_dart(node) {
let name = self.text(node).to_string();
let type_node = match kind {
"type_identifier" => Some(node),
"relational_operator" => self.misparsed_generic_call(node).map(|(_, type_name)| type_name),
_ => None,
};
if let Some(type_node) = type_node.filter(|t| self.is_type_name_dart(*t)) {
let name = self.text(type_node).to_string();
let owner = self.top_row();
self.push_ref_at(owner, &name, "references", node);
self.push_ref_at(owner, &name, "references", type_node);
}
if kind == "function_signature" {