fix(dart): link a call chained on a generic lookup like BlocProvider.of<T>(…) (#750) (#2383)

When a generic call parses as a call (a named argument after a positional
one, as in `(context, listen: false)`, or a type argument like `<A?>`,
`<A, B>` or `<List<A>>`), the Dart extractor records a chain on it as
`BlocProvider.of().increment`. The resolver typed that receiver by what
`BlocProvider.of` is declared to return: `T` for `static T of<T>(…)`,
which names no type, and nothing at all when the lookup comes from a
package, as flutter_bloc and provider do in an app. So
`BlocProvider.of<CounterCubit>(context, listen: false).increment()` and
provider's `Provider.of<Cart>(context, listen: false).add(item)` linked
nothing.

The declared return type is still tried first, so `Foo.create().bar()`
with a concrete return type resolves as before. When it gives no edge,
the chain is read at the call site the way #2376 reads a later link of a
chain: the lookup's own type parameter is the type argument the call
gives, and an outside `of<T>(…)` lookup hands back its T. The edge stays
on the encoded ref, as in Go's fallback (the gin runaway).

The extractors are unchanged. A chain on a call parsed as two
comparisons keeps its bare name (#2375), and now reaches the same
method; the comments that gave the old reason are corrected.

Validated before/after with the native kernel at 3f96f80f, nothing
removed anywhere: felangel/bloc @b9be1e2 0 (its four such sites parse as
comparisons and already linked), rrousselGit/riverpod @4ba1be2 +6
(`X.family<…>(…).call(42)` to `ClassFamily::call`), flutter/samples
@63411527 +2 and brianegan/flutter_architecture_samples @d898d13 +6,
all `Provider.of<T>(context, listen: false).m()`. A bare-name fallback
(as Go does) added nothing on those repos, since the lookups are outside
the project, and linked 2 of the test's 8 chains wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-06 15:29:47 +00:00
committed by GitHub
parent 1b752de2c0
commit d500e58a52
6 changed files with 257 additions and 10 deletions
+5 -4
View File
@@ -105,10 +105,11 @@ interface DartMisparsedCall {
* the callee and `(` against the `>`. A comparison is written `a < b`.
*
* A call chained on it keeps the bare name it had (`increment` in
* `BlocProvider.of<CounterCubit>(context).increment()`) rather than taking
* the `BlocProvider.of().increment` a parsed chain gets: that encoding
* resolves through what `of` returns, and a generic factory like this one
* returns its type parameter, so it would drop the edge the bare name finds.
* `BlocProvider.of<CounterCubit>(context).increment()`), where a parsed
* chain gets `BlocProvider.of().increment`. The resolver gives both the same
* method: it types a bare link from the chain written before it, and an
* encoded chain the same way when `of` declares no type of its own to return
* (`static T of<T>(…)`, or a factory outside the project) (#750).
*/
export function dartMisparsedGenericCall(lt: SyntaxNode): DartMisparsedCall | undefined {
if (lt.type !== 'relational_operator' || lt.firstChild?.type !== '<') return undefined;
+15
View File
@@ -8215,6 +8215,21 @@ export function matchDottedCallChain(
const factoryMethod = inner.slice(lastDot + 1);
if (!factoryClass || !factoryMethod) return null;
const ret = lookupCalleeReturnType(`${factoryClass}::${factoryMethod}`, ref, context);
if (ref.language === 'dart') {
const declared = ret
? resolveMethodOnType(ret, method, ref, context, 0.85, 'instance-method', importedFqnOf(ret, ref, context))
: null;
if (declared) return declared;
// A generic factory returns its type parameter — `static T of<T>(…)` in
// flutter_bloc's BlocProvider — and one outside the project declares
// nothing here, so `BlocProvider.of<CounterCubit>(context).increment()`
// had no type to look `increment` up on. The call site has it: read the
// chain there as a later link of a chain is read (#750), where `of<T>`'s
// T is the type argument the call gives. The edge belongs to this ref, not
// to the bare name it was looked up by (see the gin runaway above).
const link = matchDartChainLink({ ...ref, referenceName: method }, context);
return link ? { ...link, original: ref } : null;
}
if (!ret) {
// Objective-C: a class-message factory — `[X alloc]`, `[X new]`,
// `[X sharedFoo]` — returns an instance of the RECEIVER class `X` by