mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
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:
@@ -51,6 +51,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Dart, `const` constructors and redirecting factories, like flutter_bloc's `const BlocProvider.value(…)` or freezed's `const factory LoginState.loading() = _Loading;`, are now indexed, so a call such as `BlocProvider.value(…)` or `BlocProvider<CounterCubit>.value(…)` links to that constructor instead of another class's member with the same name. A call through a class name now reaches only that class's own constructors and static members, never a look-alike class, a parent class, an instance getter of the same name, or a project method that merely shares its name with an SDK call like `Uri.parse(…)`, and a constructor is no longer linked from a bare name, the end of a call chain or an annotation like `@internal`. Re-index Dart projects after upgrading.
|
||||
- In Dart, a call that passes a type argument and arguments, like `ref.read<Repo>(repoProvider)`, `ref.watch<int>(counterProvider)`, `Provider<User>((ref) => …)`, `BlocProvider.of<CounterCubit>(context)` or `RepositoryProvider<Repo>(create: …)`, now links to the method or class it calls, and the type it passes counts as used there. Depending on the code around it, the parser often read such a call as two comparisons, so it linked nothing, and Riverpod providers, repository reads and bloc lookups were missing from `callers`, impact and `codegraph_explore`. A real comparison like `a < b` is still left alone. Re-index Dart projects after upgrading.
|
||||
- In Dart, a constructor or member written without a body, like flutter_bloc's `const BlocProvider.value(…)`, `Foo._();`, `Point.origin() : x = 0, y = 0;` or an abstract or `external` method, now keeps the `///` documentation and the annotations written above it, the way one with a body already did. Before, that documentation was lost, so searching for words in it didn't find the symbol, and an annotation like `@visibleForTesting` or `@protected` on it wasn't recorded. Re-index Dart projects after upgrading.
|
||||
- In Dart, a call on what a generic lookup returns, like flutter_bloc's `BlocProvider.of<CounterCubit>(context, listen: false).increment()` or Provider's `Provider.of<CartModel>(context, listen: false).add(item)`, now links to that method of the type in the angle brackets, whether the lookup is declared in your project or comes from a package. Before, such a call often linked nothing — for example whenever the lookup passed `listen: false` — so `callers` and impact missed those methods. A lookup that names no type, like `BlocProvider.of(context)`, still links nothing. Re-index Dart projects after upgrading. (#750)
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* A Dart call chained on a generic static factory — bloc's
|
||||
* `BlocProvider.of<CounterCubit>(context, listen: false).increment()`,
|
||||
* `RepositoryProvider.of<TodosRepository>(context).saveTodo(t)` — calls a
|
||||
* member of the type the factory's type argument names.
|
||||
*
|
||||
* When the generic call parses as a call (tree-sitter-dart reads about half
|
||||
* of them as two comparisons, but a named argument or a type argument like
|
||||
* `<A?>` makes it a call), the extractor records the chain as
|
||||
* `BlocProvider.of().increment`, and the resolver typed it by what
|
||||
* `BlocProvider.of` is declared to return. That is `T` (`static T of<T>(…)`),
|
||||
* which names no type, so the call linked nothing. In an app, where
|
||||
* flutter_bloc is a dependency and not in the project, nothing typed it
|
||||
* either. The chain read back from the call site is typed the way a later
|
||||
* link of a chain is (#750): the factory's own type parameter is the type
|
||||
* argument the call gives, and an outside lookup such as `of<T>(…)` hands
|
||||
* back its T. A chain on a call read as two comparisons keeps its bare name
|
||||
* (`increment`), and reaches the same method.
|
||||
*
|
||||
* It never falls back to a member found by its name: `StepCounter.increment`,
|
||||
* next to the call site, is not what `of<CounterCubit>` returns, and a call
|
||||
* with no type argument (`BlocProvider.of(context)`) links nothing.
|
||||
*
|
||||
* Runs against the native kernel (when built) and the wasm extractor, which
|
||||
* must agree.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
const PUBSPEC = 'name: app\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n';
|
||||
|
||||
// A method named like each chained call, nearer the call site than the type
|
||||
// that declares it: the one a guess by name would pick.
|
||||
const DECOY = `class StepCounter {
|
||||
void increment() {}
|
||||
void decrement() {}
|
||||
void saveTodo(Object todo) {}
|
||||
}
|
||||
`;
|
||||
|
||||
/** The bloc repo itself: BlocProvider is declared in the project. */
|
||||
const LIBRARY: Record<string, string> = {
|
||||
'pubspec.yaml': PUBSPEC,
|
||||
'lib/bloc_provider.dart': `class BlocProvider<T> {
|
||||
const BlocProvider();
|
||||
|
||||
static T of<T>(Object context, {bool listen = false}) => throw 0;
|
||||
|
||||
static T? maybeOf<T>(Object context, {bool listen = false}) => null;
|
||||
}
|
||||
`,
|
||||
'lib/bloc.dart': `abstract class Bloc<E, S> {
|
||||
void add(E event) {}
|
||||
}
|
||||
`,
|
||||
'lib/counter/counter_cubit.dart': `class CounterCubit {
|
||||
void increment() {}
|
||||
}
|
||||
`,
|
||||
'lib/counter/counter_bloc.dart': `import '../bloc.dart';
|
||||
|
||||
class CounterBloc extends Bloc<int, int> {}
|
||||
`,
|
||||
'lib/counter/step_counter.dart': DECOY,
|
||||
'lib/counter/counter_page.dart': `import '../bloc_provider.dart';
|
||||
import 'counter_cubit.dart';
|
||||
import 'counter_cubit.dart' as cubits;
|
||||
import 'counter_bloc.dart';
|
||||
|
||||
class Button {
|
||||
const Button({Object? onPressed});
|
||||
}
|
||||
|
||||
class CounterPage {
|
||||
void tap(Object context) {
|
||||
BlocProvider.of<CounterCubit>(context, listen: false).increment();
|
||||
}
|
||||
|
||||
void tapWrapped(Object context) {
|
||||
BlocProvider.of<CounterCubit>(context, listen: false)
|
||||
.increment();
|
||||
}
|
||||
|
||||
void tapPrefixed(Object context) {
|
||||
BlocProvider.of<cubits.CounterCubit>(context, listen: false).increment();
|
||||
}
|
||||
|
||||
void tapMaybe(Object context) {
|
||||
BlocProvider.maybeOf<CounterCubit>(context, listen: false)?.increment();
|
||||
}
|
||||
|
||||
void tapBloc(Object context) {
|
||||
BlocProvider.of<CounterBloc>(context, listen: false).add(1);
|
||||
}
|
||||
|
||||
void tapMissing(Object context) {
|
||||
BlocProvider.of<CounterCubit>(context, listen: false).decrement();
|
||||
}
|
||||
|
||||
void tapInferred(Object context) {
|
||||
BlocProvider.of(context, listen: false).increment();
|
||||
}
|
||||
|
||||
// Parsed as two comparisons, so increment arrives by its bare name.
|
||||
void tapRecovered(Object context) {
|
||||
BlocProvider.of<CounterCubit>(context).increment();
|
||||
}
|
||||
|
||||
Object build(Object context) => Button(
|
||||
onPressed: () => BlocProvider.of<CounterCubit>(context, listen: false).increment(),
|
||||
);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
/** An app: flutter_bloc is a dependency, outside the project. */
|
||||
const APP: Record<string, string> = {
|
||||
'pubspec.yaml': PUBSPEC,
|
||||
'lib/counter/counter_cubit.dart': `class CounterCubit {
|
||||
void increment() {}
|
||||
}
|
||||
`,
|
||||
'lib/counter/step_counter.dart': DECOY,
|
||||
'lib/todos/todos_repository.dart': `class TodosRepository {
|
||||
void saveTodo(Object todo) {}
|
||||
}
|
||||
`,
|
||||
'lib/counter/counter_page.dart': `import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../todos/todos_repository.dart';
|
||||
import 'counter_cubit.dart';
|
||||
|
||||
class CounterPage {
|
||||
void tap(Object context) {
|
||||
BlocProvider.of<CounterCubit>(context, listen: false).increment();
|
||||
}
|
||||
|
||||
void save(Object context, Object todo) {
|
||||
RepositoryProvider.of<TodosRepository>(context, listen: false).saveTodo(todo);
|
||||
}
|
||||
|
||||
void tapInferred(Object context) {
|
||||
BlocProvider.of(context, listen: false).increment();
|
||||
}
|
||||
|
||||
// Parsed as two comparisons, so increment arrives by its bare name.
|
||||
void tapRecovered(Object context) {
|
||||
BlocProvider.of<CounterCubit>(context).increment();
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
describe('Dart chains on a generic static factory', () => {
|
||||
let root = '';
|
||||
let cg: CodeGraph | undefined;
|
||||
let kernel: string | undefined;
|
||||
|
||||
const index = async (files: Record<string, string>, backend: string): Promise<CodeGraph> => {
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
|
||||
else delete process.env.CODEGRAPH_KERNEL;
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
return cg;
|
||||
};
|
||||
|
||||
/** `caller -> Owner::name` for every call edge out of a file. */
|
||||
const callsFrom = (graph: CodeGraph, file: string): string[] => {
|
||||
const ids = graph.getNodesInFile(file).map((n) => n.id);
|
||||
return graph
|
||||
.getOutgoingEdgesFrom(ids)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => `${graph.getNode(e.source)!.name} -> ${graph.getNode(e.target)!.qualifiedName}`)
|
||||
.sort();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-generic-factory-'));
|
||||
kernel = process.env.CODEGRAPH_KERNEL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cg?.close();
|
||||
cg = undefined;
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
|
||||
else process.env.CODEGRAPH_KERNEL = kernel;
|
||||
});
|
||||
|
||||
it.each(['default', 'wasm'])('call a member of the type argument a project factory is given (%s)', async (backend) => {
|
||||
const graph = await index(LIBRARY, backend);
|
||||
expect(callsFrom(graph, 'lib/counter/counter_page.dart')).toEqual([
|
||||
'build -> BlocProvider::of',
|
||||
'build -> CounterCubit::increment',
|
||||
'tap -> BlocProvider::of',
|
||||
'tap -> CounterCubit::increment',
|
||||
// What CounterBloc inherits.
|
||||
'tapBloc -> Bloc::add',
|
||||
'tapBloc -> BlocProvider::of',
|
||||
// No type argument says what `of` returns.
|
||||
'tapInferred -> BlocProvider::of',
|
||||
'tapMaybe -> BlocProvider::maybeOf',
|
||||
'tapMaybe -> CounterCubit::increment',
|
||||
// CounterCubit has no `decrement`; StepCounter's is not this one.
|
||||
'tapMissing -> BlocProvider::of',
|
||||
'tapPrefixed -> BlocProvider::of',
|
||||
'tapPrefixed -> CounterCubit::increment',
|
||||
'tapRecovered -> BlocProvider::of',
|
||||
'tapRecovered -> CounterCubit::increment',
|
||||
'tapWrapped -> BlocProvider::of',
|
||||
'tapWrapped -> CounterCubit::increment',
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(['default', 'wasm'])('call a member of the type argument an outside lookup is given (%s)', async (backend) => {
|
||||
const graph = await index(APP, backend);
|
||||
expect(callsFrom(graph, 'lib/counter/counter_page.dart')).toEqual([
|
||||
'save -> TodosRepository::saveTodo',
|
||||
'tap -> CounterCubit::increment',
|
||||
'tapRecovered -> CounterCubit::increment',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1219,8 +1219,8 @@ impl<'t> Walker<'t> {
|
||||
/// 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).
|
||||
/// its bare name, which the resolver types from the chain written before
|
||||
/// it, as it types a parsed chain's `X().m` (#750).
|
||||
fn misparsed_generic_call(&self, lt: Node<'t>) -> Option<(Node<'t>, Node<'t>)> {
|
||||
if lt.kind() != "relational_operator" || lt.child(0)?.kind() != "<" {
|
||||
return None;
|
||||
|
||||
@@ -754,9 +754,11 @@ recovered form emits what the parsed call does:
|
||||
|
||||
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.
|
||||
chain's `BlocProvider.of().increment`. The resolver gives both the same
|
||||
method: a bare link is typed from the chain written before it, and so is an
|
||||
encoded chain whose inner call declares no type of its own to return — a
|
||||
generic factory's type parameter (`static T of<T>`) or a factory outside the
|
||||
project (#750). Nothing is gained by re-encoding the recovered chain.
|
||||
|
||||
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
|
||||
@@ -765,7 +767,7 @@ 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.
|
||||
`increment` bare, which resolves as above.
|
||||
|
||||
### Static-member / value-read refs — dart branch (:4759-4767), STATIC_MEMBER_LANGS:346
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user