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
+41
View File
@@ -701,6 +701,7 @@ extract-mini.txt):
| `BlocProviderT<CounterT>.value(…);` as a statement | args selector; prev = `.value` selector; accessorPrev = a selector holding only `type_arguments`, whose prev is the identifier | `BlocProviderT.value` (dartTypeArgumentsReceiver, since 2026-10-06 — bare `value` before); the `.value().tail` re-encode reads it the same way |
| `=> BlocProviderT<CounterT>.value(…)`, and as an initializer, argument or `return` value | `constructor_invocation` (type_identifier [`.` type_identifier] type_arguments `.` identifier arguments) | `BlocProviderT.value` at the node position — the LAST type_identifier, so `p.X<T>.named()` → `X.named` (since 2026-10-06; nothing before) |
| `generic<int>(5)` | args selector (type args ride argument_part) | bare `generic` |
| `ref.read<Repo>(p)`, `Provider<int>((ref) => 0)`, `X<T>(name: v)` parsed as two comparisons, `(ref.read < Repo) > (p)` (§Misparsed generic calls) | the `<` relational_operator (dartMisparsedGenericCall) | the parsed call's ref — `ref.read` / `Provider` / `X`, at the `<` |
| `await fetch()` | recursion through unary/await_expression | `fetch` at the selector |
| `throw StateError('bad')` | recursion | `StateError` |
| `'sum ${a + compute()}'` | template_substitution recursion | `compute` (interpolation calls EMIT); `$name` → identifier_dollar_escaped → nothing |
@@ -712,6 +713,46 @@ extractCall (:3684), LITERAL_RECEIVER_TYPES (:373-388), SKIP_RECEIVERS, the
parenthesized-conversion regex (:4530), template-strip — ALL UNREACHABLE for
dart (callTypes empty). Do not port them.
#### Misparsed generic calls (dartMisparsedGenericCall / misparsed_generic_call)
tree-sitter-dart parses a generic call with arguments as two comparisons about
as often as it parses it as a call — the GLR tie goes either way with the code
around it: `final a = ref.watch<int>(p);`, `ref.read<Repo>(p);`, `int g(Ref
ref) => ref.read<int>(1);` and `Provider<int>((ref) => 0)` come out as
`relational_expression(relational_expression(<callee> < <type>) > <args>)`,
while `print(ref.watch<int>(p) + 1)`, `obj.a.b<Repo>(p)` and
`ref.read<Repo>(a, b)` parse as calls (2,853 nests vs 2,836 parsed generic
calls with arguments across bloc, riverpod and flutter/samples). The args are
a `parenthesized_expression`, or a `record_literal` when named
(`DropdownButton<String>(items: …)`); the type part is `identifier`, a `.Name`
selector (`p.Repo`) and/or a type_arguments-only selector (`Map<K, V>`); the
callee part sits under `unary_expression` / `await_expression` for `await` and
prefix operators. Recovered at the `<` when the type names a type (`/^[_$]*[A-Z]/`
or `int double num bool dynamic void`), `<` touches the callee and `(` touches
the `>`; `a < B > (c)`, `a<b>(c)` and `a <B>(c)` stay comparisons. The
recovered form emits what the parsed call does:
- `calls` at the `<` (the parsed call's argument_part starts there);
- `references <Type>` for the type argument, via isDartTypeName (§Type refs);
the static-member path skips `Map` in `<Map<K, V>>` so it is not doubled;
- no member read of the callee (`ref.read`, also under `await`) or of a
prefixed type (`p.Repo`).
A call chained on a recovered one keeps its bare name (`increment` in
`BlocProvider.of<CounterCubit>(context).increment()`), unlike the parsed
chain's `BlocProvider.of().increment`: that encoding resolves through `of`'s
return type, which for generic factories is the type parameter, so it would
lose the edge the bare name finds.
The grammar can also end an arrow function in front of a generic call:
`(ref) => ref.watch<int>(p)` → `((ref) => ref).watch<int>(p)`, in the
relational and the selector form alike. dartReceiverOf / receiver_of read a
receiver in that place from the end of the arrow's body (Dart writes a member
access on an arrow itself in parentheses), for callee names and static-member
refs: `ref.watch`, `BlocProvider.of`. dartCalleeOfArgPart / callee_of_arg_part
do not, so `() => BlocProvider.of<CounterCubit>(context).increment()` keeps
`increment` bare, for the reason above.
### Static-member / value-read refs — dart branch (:4759-4767), STATIC_MEMBER_LANGS:346
Called from the body walker only (:5218). The DART-SPECIFIC branch (the