mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
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:
@@ -49,6 +49,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In JavaScript and TypeScript, an import whose name contains a `$`, like `import items$ from './store'`, `import * as api$ from './api'` or `import { load as load$ } from './data'`, now links the calls made through it, such as `items$.getState().inc()`, to the code it imports, and so does a `$` name in a `require(…)` destructuring or an `export { … } from` barrel. Before, such an import was skipped or bound under a cut-off name (`load` for `load$`), so those calls reached nothing and the functions they call could look unused. A local variable or parameter that reuses an imported `$` name, like `const items$ = …` inside a function, is no longer mistaken for the import. Re-index JavaScript and TypeScript projects after upgrading.
|
||||
- In Dart, calling a parameter or a local variable, like `create(ref, arg)` inside `overrideWith(… create)`, a closure's parameter, a loop variable or a test's mock `listener(0, 1)`, no longer links the call to an unrelated function or constant of the same name in another file, or to a method the class inherits: a Bloc event handler's `emit(…)` is the handler's own `emit` parameter, not `Bloc.emit`. So `codegraph callers` and impact no longer list code that never calls them, while calls the parameter or variable doesn't cover, and calls to a local function, link as before. Re-index Dart projects after upgrading.
|
||||
- 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.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Dart generic calls that tree-sitter-dart parses as two comparisons.
|
||||
*
|
||||
* `ref.read<Repo>(repoProvider)` comes out of the grammar as
|
||||
* `(ref.read < Repo) > (repoProvider)`, a relational expression inside
|
||||
* another, about as often as it comes out as a call: which one depends on the
|
||||
* code around it. `Provider<int>((ref) => 0)`, `int g(Ref ref) =>
|
||||
* ref.read<int>(1);`, `RepositoryProvider<Repo>(create: …)` (named arguments
|
||||
* parse as a record) and `await users.fetch<User>(1)` all can. Neither
|
||||
* extractor recorded a call for that shape, only a member read of `ref.read`
|
||||
* that links nothing, so providers, repository reads and bloc lookups written
|
||||
* with a type argument had no callers. `context.read<CounterBloc>()`, with
|
||||
* empty arguments, parses as a call and always worked.
|
||||
*
|
||||
* The nest is the call when its type argument names a type and it is laid
|
||||
* out as a call is, `<` against the callee and `(` against the `>`; a
|
||||
* comparison is written `a < b`.
|
||||
*
|
||||
* Runs against the native kernel (when built) and the wasm extractor, which
|
||||
* must agree.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
||||
import type { Node } from '../src/types';
|
||||
|
||||
const BACKENDS = ['default', 'wasm'] as const;
|
||||
|
||||
let kernel: string | undefined;
|
||||
|
||||
function useBackend(backend: (typeof BACKENDS)[number]): void {
|
||||
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
|
||||
else delete process.env.CODEGRAPH_KERNEL;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
kernel = process.env.CODEGRAPH_KERNEL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
|
||||
else process.env.CODEGRAPH_KERNEL = kernel;
|
||||
});
|
||||
|
||||
describe('Dart generic calls parsed as comparisons: extraction', () => {
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['dart']);
|
||||
});
|
||||
|
||||
const SOURCE = [
|
||||
'int g(Ref ref) => ref.read<int>(1);',
|
||||
'',
|
||||
'void f(Ref ref) {',
|
||||
' final a = ref.watch<int>(p);',
|
||||
' ref.read<Repo>(repoProvider);',
|
||||
' print(ref.watch<int>(p) + 1);',
|
||||
' final b = a < B > (c);',
|
||||
' final d = a<b>(c);',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
it.each(BACKENDS)('records the call a parsed one gets, at its `<` (%s)', (backend) => {
|
||||
useBackend(backend);
|
||||
const result = extractFromSource('lib/providers.dart', SOURCE, 'dart');
|
||||
const names = new Map(result.nodes.map((n) => [n.id, n.name]));
|
||||
const refs = result.unresolvedReferences
|
||||
.filter((r) => r.referenceKind === 'calls' || r.referenceKind === 'references')
|
||||
.map((r) => `${names.get(r.fromNodeId)} ${r.referenceKind} ${r.referenceName} ${r.line}:${r.column}`)
|
||||
.sort();
|
||||
|
||||
expect(refs).toEqual([
|
||||
'f calls print 6:7',
|
||||
// Line 6 parses as a call: what the recovered calls must look like.
|
||||
'f calls ref.read 5:10',
|
||||
'f calls ref.watch 4:21',
|
||||
'f calls ref.watch 6:17',
|
||||
'f references Ref 3:7',
|
||||
// The type argument is referenced as a parsed one is; `int` is built in.
|
||||
'f references Repo 5:11',
|
||||
'g calls ref.read 1:26',
|
||||
'g references Ref 1:6',
|
||||
]);
|
||||
// No member read of `ref.read` / `ref.watch`, and the comparisons on
|
||||
// lines 7 and 8 stay comparisons.
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dart generic calls parsed as comparisons: graph', () => {
|
||||
const FILES: Record<string, string> = {
|
||||
'pubspec.yaml': 'name: app\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n',
|
||||
'lib/riverpod.dart': `class Ref {
|
||||
T watch<T>(Object provider) => throw 0;
|
||||
T read<T>(Object provider) => throw 0;
|
||||
}
|
||||
|
||||
class Provider<T> {
|
||||
Provider(T Function(Ref ref) create);
|
||||
}
|
||||
`,
|
||||
'lib/data.dart': `class User {}
|
||||
|
||||
class Repo {
|
||||
Repo();
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
Future<T> fetch<T>(int id) => throw 0;
|
||||
}
|
||||
`,
|
||||
'lib/bloc.dart': `class BuildContext {}
|
||||
|
||||
class BlocProvider {
|
||||
static T of<T>(BuildContext context) => throw 0;
|
||||
}
|
||||
|
||||
class CounterCubit {
|
||||
void increment() {}
|
||||
}
|
||||
|
||||
class RepositoryProvider<T> {
|
||||
RepositoryProvider({required T Function(BuildContext context) create});
|
||||
}
|
||||
`,
|
||||
// Every call below parses as two comparisons.
|
||||
'lib/app.dart': `import 'bloc.dart';
|
||||
import 'data.dart';
|
||||
import 'riverpod.dart';
|
||||
|
||||
final seedProvider = Provider<int>((ref) => 0);
|
||||
final repoProvider = Provider<Repo>((ref) => Repo());
|
||||
final countProvider = Provider<int>((Ref ref) => ref.watch<int>(seedProvider));
|
||||
|
||||
int g(Ref ref) => ref.read<int>(countProvider);
|
||||
|
||||
Repo readRepo(Ref ref) {
|
||||
final repo = ref.read<Repo>(repoProvider);
|
||||
return repo;
|
||||
}
|
||||
|
||||
Future<User> loadUser(UserRepository users) async {
|
||||
final user = await users.fetch<User>(1);
|
||||
return user;
|
||||
}
|
||||
|
||||
void tap(BuildContext context) {
|
||||
BlocProvider.of<CounterCubit>(context).increment();
|
||||
}
|
||||
|
||||
Object provide() => RepositoryProvider<Repo>(create: (_) => Repo());
|
||||
|
||||
bool compare(int a, int b, int c) {
|
||||
final spaced = a < b > (c);
|
||||
final lower = a<b>(c);
|
||||
return spaced && lower;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-generic-calls-'));
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cg?.close();
|
||||
cg = undefined;
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it.each(BACKENDS)('link the calls, constructions and type arguments (%s)', async (backend) => {
|
||||
useBackend(backend);
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
const graph = cg;
|
||||
const node = (qualifiedName: string): Node => {
|
||||
const found = graph.getNodesInFile('lib/app.dart').find((n) => n.qualifiedName === qualifiedName);
|
||||
expect(found, qualifiedName).toBeDefined();
|
||||
return found!;
|
||||
};
|
||||
const targets = (from: string, kind: string): string[] =>
|
||||
[...new Set(graph
|
||||
.getOutgoingEdgesFrom([node(from).id])
|
||||
.filter((e) => e.kind === kind)
|
||||
.map((e) => graph.getNode(e.target)!.qualifiedName))]
|
||||
.sort();
|
||||
|
||||
// The issue's shapes: a typed receiver's method, through `=>` and a block.
|
||||
expect(targets('g', 'calls')).toEqual(['Ref::read']);
|
||||
expect(targets('readRepo', 'calls')).toEqual(['Ref::read']);
|
||||
expect(targets('readRepo', 'references')).toContain('Repo');
|
||||
|
||||
// Riverpod providers built with a type argument construct their provider,
|
||||
// and a typed closure parameter still types the call written in it.
|
||||
expect(targets('seedProvider', 'instantiates')).toEqual(['Provider']);
|
||||
expect(targets('repoProvider', 'instantiates')).toEqual(['Provider', 'Repo']);
|
||||
expect(targets('countProvider', 'instantiates')).toEqual(['Provider']);
|
||||
expect(targets('countProvider', 'calls')).toEqual(['Ref::watch']);
|
||||
|
||||
// After `await`, with named arguments, and through a static method.
|
||||
expect(targets('loadUser', 'calls')).toEqual(['UserRepository::fetch']);
|
||||
expect(targets('provide', 'instantiates')).toEqual(['Repo', 'RepositoryProvider']);
|
||||
expect(targets('tap', 'calls')).toEqual(['BlocProvider::of', 'CounterCubit::increment']);
|
||||
expect(targets('tap', 'references')).toContain('CounterCubit');
|
||||
|
||||
// Comparisons are not calls.
|
||||
expect(targets('compare', 'calls')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// Generic calls tree-sitter-dart parses as two comparisons — `ref.read<Repo>(p)`
|
||||
// comes out as `(ref.read < Repo) > (p)` — recovered as calls, and the
|
||||
// comparisons that look like them.
|
||||
import 'package:app/app.dart' as p;
|
||||
|
||||
// Initializers: a constant's, a top-level variable's, a field's.
|
||||
final counterProvider = Provider<int>((ref) => ref.watch<int>(seedProvider));
|
||||
final repoProvider = Provider<Repo>((ref) {
|
||||
final dio = ref.read<Dio>(dioProvider);
|
||||
return Repo(dio);
|
||||
});
|
||||
var cache = Cache<String>(capacity: 10);
|
||||
final routes = [MaterialPageRoute<void>(builder: (_) => const Home())];
|
||||
|
||||
int g(Ref ref) => ref.read<int>(1);
|
||||
|
||||
class Screen {
|
||||
final notifier = ValueNotifier<int>(0);
|
||||
static final route = MaterialPageRoute<void>(builder: (_) => const Screen());
|
||||
static var shared = Notifier<Repo>(repoProvider);
|
||||
|
||||
const Screen();
|
||||
|
||||
Future<void> load(Ref ref, BuildContext context) async {
|
||||
ref.read<Repo>(repoProvider);
|
||||
final user = await ref.read<UserRepository>(userRepoProvider).fetch<User>(id);
|
||||
final map = ref.read<Map<String, Repo>>(mapProvider);
|
||||
final prefixed = ref.read<p.Repo>(repoProvider);
|
||||
final generic = ref.read<p.Box<Repo>>(boxProvider);
|
||||
BlocProvider.of<CounterCubit>(context).increment();
|
||||
final state = BlocProvider.of<CounterCubit>(context).state;
|
||||
final neg = -ref.read<int>(countProvider);
|
||||
final not = !ref.read<bool>(flagProvider);
|
||||
final nested = Wrapper<A>(Inner<B>(seed));
|
||||
final bare = build<Repo>(seed);
|
||||
final dyn = await box.openBox<dynamic>('box');
|
||||
final chained = Factory.create<Repo>(seed).build();
|
||||
final lower = repo.items.where<Item>(isDone).toList();
|
||||
final self = this.read<Repo>(repoProvider);
|
||||
final cascade = Builder<Repo>(seed)..name = 'x';
|
||||
final templ = '${ref.read<Repo>(repoProvider)}';
|
||||
final sum = ref.watch<int>(countProvider) + 1;
|
||||
return show<void>(context: context, builder: (_) => const Screen());
|
||||
}
|
||||
|
||||
void outer(Ref ref) {
|
||||
int inner() => ref.watch<int>(innerProvider);
|
||||
final cb = () => navigator.push<void>(any());
|
||||
inner();
|
||||
}
|
||||
|
||||
bool compare(int a, int b, int c, int d) {
|
||||
// Comparisons, not calls.
|
||||
final spaced = a < B > (c);
|
||||
final lower = a<b>(c);
|
||||
final left = a <B>(c);
|
||||
final right = a<B> (c);
|
||||
final expr = a < b + 1 > (c);
|
||||
final record = a<(B, C)>(d);
|
||||
return a < b && c > d;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,9 @@
|
||||
* second variable left to its function; TortureConstCtors.dart: `const`
|
||||
* constructors and redirecting factories as methods with the unnamed ones
|
||||
* skipped, every redirect-target form, and constructors called with type
|
||||
* arguments, as statements and as expressions) and their CRLF variants
|
||||
* arguments, as statements and as expressions; TortureGenericCalls.dart:
|
||||
* generic calls parsed as two comparisons, recovered as calls) and their CRLF
|
||||
* variants
|
||||
* (derived in-memory — #1329), plus defer and generated-file pins.
|
||||
*
|
||||
* The full-repo sweeps live in scripts/kernel-parity.mjs (shelf/bloc/flutter
|
||||
@@ -129,6 +131,10 @@ describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => {
|
||||
// ones skipped), every redirect-target form, and constructors called
|
||||
// with type arguments in statements and expressions.
|
||||
['TortureConstCtors.dart', 20],
|
||||
// Generic calls the grammar parses as two comparisons (`ref.read<Repo>(p)`
|
||||
// as `(ref.read < Repo) > (p)`): every callee, type-argument and argument
|
||||
// shape, in bodies and initializers, and the comparisons that look alike.
|
||||
['TortureGenericCalls.dart', 10],
|
||||
] as const;
|
||||
|
||||
for (const [file, minNodes] of FIXTURES) {
|
||||
|
||||
+229
-61
@@ -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" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,14 +63,130 @@ function isDartFieldOrTopLevelEntry(node: SyntaxNode): boolean {
|
||||
return owner?.type === 'declaration' && owner.parent !== null && DART_MEMBER_BODIES.has(owner.parent.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)` comes out as
|
||||
* `((ref) => ref).watch<int>(p)`. Dart writes a member access on the arrow
|
||||
* itself in parentheses, so a receiver in that place is never the arrow. It
|
||||
* names the call (`ref.watch`) and a static member's class; a call chained on
|
||||
* that call keeps its bare name (see dartMisparsedGenericCall).
|
||||
*/
|
||||
export function dartReceiverOf(selector: SyntaxNode): SyntaxNode | null {
|
||||
const prev = selector.previousNamedSibling;
|
||||
const body = prev?.type === 'function_expression' ? prev.lastNamedChild : null;
|
||||
return body?.type === 'function_expression_body' ? body.lastNamedChild : prev;
|
||||
}
|
||||
|
||||
/** Dart's lowercase built-in types, which a type argument can name: `<int>`, `<void>`. */
|
||||
const DART_LOWERCASE_TYPES: ReadonlySet<string> = new Set(['int', 'double', 'num', 'bool', 'dynamic', 'void']);
|
||||
|
||||
/** What a callee can sit under in front of a generic call: `await`, `-`, `!`. */
|
||||
const DART_PREFIXED: ReadonlySet<string> = new Set(['unary_expression', 'await_expression']);
|
||||
|
||||
/** A generic call tree-sitter-dart parsed as two comparisons (dartMisparsedGenericCall). */
|
||||
interface DartMisparsedCall {
|
||||
/** The node the callee ends with: what a parsed call's argument part follows. */
|
||||
callee: SyntaxNode;
|
||||
/** The identifier naming the type argument (`Repo` in `<Repo>` and `<p.Repo>`). */
|
||||
typeName: SyntaxNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic call tree-sitter-dart parsed as two comparisons, found from its
|
||||
* `<`. `ref.read<Repo>(repoProvider)`, `Provider<int>((ref) => 0)` and
|
||||
* `DropdownButton<String>(items: items)` come out as `(ref.read < Repo) >
|
||||
* (repoProvider)`: a `relational_expression` inside another, with the
|
||||
* arguments as a `parenthesized_expression`, or a `record_literal` when they
|
||||
* are named. That happens about as often as the call parses as one, because
|
||||
* the grammar breaks the tie by the code around it. Dart reads it as a call,
|
||||
* and so does this when the type argument names a type (`Repo`, `p.Repo`,
|
||||
* `Map<K, V>`, `int`, `void`) and the code is laid out as a call: `<` against
|
||||
* 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.
|
||||
*/
|
||||
export function dartMisparsedGenericCall(lt: SyntaxNode): DartMisparsedCall | undefined {
|
||||
if (lt.type !== 'relational_operator' || lt.firstChild?.type !== '<') return undefined;
|
||||
const inner = lt.parent;
|
||||
const outer = inner?.parent;
|
||||
if (inner?.type !== 'relational_expression' || outer?.type !== 'relational_expression') return undefined;
|
||||
const left = outer.namedChild(0);
|
||||
if (left?.startIndex !== inner.startIndex || left.endIndex !== inner.endIndex) return undefined;
|
||||
const gt = outer.namedChild(1);
|
||||
if (gt?.type !== 'relational_operator' || gt.firstChild?.type !== '>') return undefined;
|
||||
const args = outer.namedChild(2);
|
||||
if (args?.type !== 'parenthesized_expression' && args?.type !== 'record_literal') return undefined;
|
||||
if (args.startIndex !== gt.endIndex) return undefined;
|
||||
const before = lt.previousNamedSibling;
|
||||
if (!before || before.endIndex !== lt.startIndex) return undefined;
|
||||
// The type argument: `T`, `p.T`, `T<…>` or `p.T<…>`, and nothing else.
|
||||
const head = lt.nextNamedSibling;
|
||||
if (head?.type !== 'identifier') return undefined;
|
||||
let typeName = head;
|
||||
let rest = head.nextNamedSibling;
|
||||
if (rest?.type === 'selector' && rest.firstNamedChild?.type === 'unconditional_assignable_selector') {
|
||||
const name = rest.firstNamedChild.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||
if (!name) return undefined;
|
||||
typeName = name;
|
||||
rest = rest.nextNamedSibling;
|
||||
}
|
||||
if (rest?.type === 'selector' && rest.namedChildCount === 1 && rest.firstNamedChild?.type === 'type_arguments') {
|
||||
rest = rest.nextNamedSibling;
|
||||
}
|
||||
if (rest) return undefined;
|
||||
if (!/^[_$]*[A-Z]/.test(typeName.text) && !DART_LOWERCASE_TYPES.has(typeName.text)) return undefined;
|
||||
// `await repo.load<User>(id)` and `-x.size<int>(y)` put the callee under the prefix.
|
||||
let callee: SyntaxNode = before;
|
||||
while (DART_PREFIXED.has(callee.type)) {
|
||||
const last: SyntaxNode | null = callee.lastNamedChild;
|
||||
if (!last) return undefined;
|
||||
callee = last;
|
||||
}
|
||||
return { callee, typeName };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a member access — `.member` on `receiver`, followed by `next` under
|
||||
* `parent` — belongs to a misparsed generic call instead of reading a member:
|
||||
* its callee (`ref.read` in `ref.read<Repo>(p)`, after an `await` too) is
|
||||
* called, and the prefixed type argument (`p.Repo` in `x.read<p.Repo>(y)`)
|
||||
* names a type.
|
||||
*/
|
||||
function dartInMisparsedGenericCall(
|
||||
receiver: SyntaxNode,
|
||||
next: SyntaxNode | null,
|
||||
parent: SyntaxNode | null,
|
||||
): boolean {
|
||||
let after = next;
|
||||
let up = parent;
|
||||
while (!after && up && DART_PREFIXED.has(up.type)) {
|
||||
after = up.nextNamedSibling;
|
||||
up = after ? null : up.parent;
|
||||
}
|
||||
if (after?.type === 'relational_operator' && dartMisparsedGenericCall(after)) return true;
|
||||
if (parent?.type !== 'relational_expression') return false;
|
||||
const before = receiver.previousNamedSibling;
|
||||
return before?.type === 'relational_operator' && dartMisparsedGenericCall(before) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Dart member read — `x.area`, `s?.label`, `Config.instance` — as the ref
|
||||
* `<receiver>.<member>`, positioned on the member's name. Reading a getter runs
|
||||
* it, so the resolver links the read to a getter, as a call, and to nothing
|
||||
* else (#2338). The receiver must be a plain name, the one shape whose type the
|
||||
* resolver can look up; a member that is called (`x.grow()`) is the call's.
|
||||
* resolver can look up; a member that is called (`x.grow()`, or `x.grow<T>(y)`
|
||||
* parsed as comparisons) is the call's. `parent` is the node's parent when the
|
||||
* walker has it: reading `.parent` walks down from the root.
|
||||
*/
|
||||
export function dartMemberRead(node: SyntaxNode): { name: string; node: SyntaxNode } | undefined {
|
||||
export function dartMemberRead(
|
||||
node: SyntaxNode,
|
||||
parent: SyntaxNode | null = node.parent,
|
||||
): { name: string; node: SyntaxNode } | undefined {
|
||||
if (node.type !== 'selector') return undefined;
|
||||
const accessor = node.namedChildren.find((c: SyntaxNode) =>
|
||||
c.type === 'unconditional_assignable_selector' || c.type === 'conditional_assignable_selector'
|
||||
@@ -83,6 +199,7 @@ export function dartMemberRead(node: SyntaxNode): { name: string; node: SyntaxNo
|
||||
if (next?.type === 'selector' && next.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part')) {
|
||||
return undefined;
|
||||
}
|
||||
if (dartInMisparsedGenericCall(receiver, next, parent)) return undefined;
|
||||
return { name: `${receiver.text}.${member.text}`, node: member };
|
||||
}
|
||||
|
||||
@@ -234,6 +351,63 @@ function dartCalleeOfArgPart(argPart: SyntaxNode): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The callee name of the Dart call whose argument part follows `prev`: `run`
|
||||
* for `run(…)`, `obj.method` for `obj.method(…)`, `Foo.create().bar` for a
|
||||
* chain off a capitalized call (#750), and a bare `method` otherwise.
|
||||
*/
|
||||
function dartCallee(prev: SyntaxNode): string | undefined {
|
||||
// Simple function/constructor call: prev is identifier (e.g., runApp(...), MyWidget(...))
|
||||
if (prev.type === 'identifier') {
|
||||
return prev.text;
|
||||
}
|
||||
|
||||
// Method call: prev is selector with accessor (e.g., obj.method(...), Navigator.push(...))
|
||||
if (prev.type === 'selector') {
|
||||
const accessor = prev.namedChildren.find((c: SyntaxNode) =>
|
||||
c.type === 'unconditional_assignable_selector' || c.type === 'conditional_assignable_selector'
|
||||
);
|
||||
if (accessor) {
|
||||
const methodId = accessor.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||
if (methodId) {
|
||||
// Include receiver for first call in chain (receiver is a direct identifier)
|
||||
const accessorPrev = dartReceiverOf(prev);
|
||||
if (accessorPrev?.type === 'identifier') {
|
||||
return accessorPrev.text + '.' + methodId.text;
|
||||
}
|
||||
// A constructor called with type arguments names its type all the
|
||||
// same: `BlocProvider<CounterCubit>.value(…)` → `BlocProvider.value`.
|
||||
const typeName = dartTypeArgumentsReceiver(accessorPrev);
|
||||
if (typeName) return typeName + '.' + methodId.text;
|
||||
// Chained static-factory / fluent call: the receiver is itself a call
|
||||
// (`Foo.create().bar()`), so accessorPrev is that call's argument_part
|
||||
// selector. Encode `<innerCallee>().<method>` so resolution can infer
|
||||
// bar's class from what `Foo.create` RETURNS (#645/#608 mechanism) —
|
||||
// but only when the chain starts with a capitalized type (a companion
|
||||
// factory / static method / constructor); an instance chain
|
||||
// (`obj.foo().bar()`) keeps the bare name (its receiver's type can't
|
||||
// be recovered here).
|
||||
if (accessorPrev?.type === 'selector' &&
|
||||
accessorPrev.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part')) {
|
||||
const innerCallee = dartCalleeOfArgPart(accessorPrev);
|
||||
if (innerCallee && /^[A-Z]/.test(innerCallee)) {
|
||||
return `${innerCallee}().${methodId.text}`;
|
||||
}
|
||||
}
|
||||
return methodId.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// super.method() / this.method(): prev is bare unconditional_assignable_selector
|
||||
if (prev.type === 'unconditional_assignable_selector' || prev.type === 'conditional_assignable_selector') {
|
||||
const methodId = prev.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||
if (methodId) return methodId.text;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const dartExtractor: LanguageExtractor = {
|
||||
functionTypes: ['function_signature'],
|
||||
classTypes: ['class_definition'],
|
||||
@@ -490,57 +664,14 @@ export const dartExtractor: LanguageExtractor = {
|
||||
if (!hasArgPart) return undefined;
|
||||
|
||||
const prev = node.previousNamedSibling;
|
||||
if (!prev) return undefined;
|
||||
return prev ? dartCallee(prev) : undefined;
|
||||
}
|
||||
|
||||
// Simple function/constructor call: prev is identifier (e.g., runApp(...), MyWidget(...))
|
||||
if (prev.type === 'identifier') {
|
||||
return prev.text;
|
||||
}
|
||||
|
||||
// Method call: prev is selector with accessor (e.g., obj.method(...), Navigator.push(...))
|
||||
if (prev.type === 'selector') {
|
||||
const accessor = prev.namedChildren.find((c: SyntaxNode) =>
|
||||
c.type === 'unconditional_assignable_selector' || c.type === 'conditional_assignable_selector'
|
||||
);
|
||||
if (accessor) {
|
||||
const methodId = accessor.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||
if (methodId) {
|
||||
// Include receiver for first call in chain (receiver is a direct identifier)
|
||||
const accessorPrev = prev.previousNamedSibling;
|
||||
if (accessorPrev?.type === 'identifier') {
|
||||
return accessorPrev.text + '.' + methodId.text;
|
||||
}
|
||||
// A constructor called with type arguments names its type all the
|
||||
// same: `BlocProvider<CounterCubit>.value(…)` → `BlocProvider.value`.
|
||||
const typeName = dartTypeArgumentsReceiver(accessorPrev);
|
||||
if (typeName) return typeName + '.' + methodId.text;
|
||||
// Chained static-factory / fluent call: the receiver is itself a call
|
||||
// (`Foo.create().bar()`), so accessorPrev is that call's argument_part
|
||||
// selector. Encode `<innerCallee>().<method>` so resolution can infer
|
||||
// bar's class from what `Foo.create` RETURNS (#645/#608 mechanism) —
|
||||
// but only when the chain starts with a capitalized type (a companion
|
||||
// factory / static method / constructor); an instance chain
|
||||
// (`obj.foo().bar()`) keeps the bare name (its receiver's type can't
|
||||
// be recovered here).
|
||||
if (accessorPrev?.type === 'selector' &&
|
||||
accessorPrev.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part')) {
|
||||
const innerCallee = dartCalleeOfArgPart(accessorPrev);
|
||||
if (innerCallee && /^[A-Z]/.test(innerCallee)) {
|
||||
return `${innerCallee}().${methodId.text}`;
|
||||
}
|
||||
}
|
||||
return methodId.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// super.method() / this.method(): prev is bare unconditional_assignable_selector
|
||||
if (prev.type === 'unconditional_assignable_selector' || prev.type === 'conditional_assignable_selector') {
|
||||
const methodId = prev.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
|
||||
if (methodId) return methodId.text;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
// 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.type === 'relational_operator') {
|
||||
const call = dartMisparsedGenericCall(node);
|
||||
return call ? dartCallee(call.callee) : undefined;
|
||||
}
|
||||
|
||||
// new MyWidget() — explicit constructor call
|
||||
|
||||
@@ -323,9 +323,10 @@ export interface LanguageExtractor {
|
||||
* getter `area` when `area` is one. Returns the ref name (`x.area`) and the
|
||||
* node to position it on, or undefined. Emitted as a `references` ref from
|
||||
* the function the read is in; the resolver links it only to a getter, with
|
||||
* a `calls` edge (a plain field read links nothing).
|
||||
* a `calls` edge (a plain field read links nothing). `parent` is the node's
|
||||
* parent, handed down by the body walker.
|
||||
*/
|
||||
extractMemberRead?: (node: SyntaxNode) => { name: string; node: SyntaxNode } | undefined;
|
||||
extractMemberRead?: (node: SyntaxNode, parent?: SyntaxNode) => { name: string; node: SyntaxNode } | undefined;
|
||||
|
||||
/**
|
||||
* Node types representing a file-level package/namespace declaration
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
|
||||
import { EXTRACTORS } from './languages';
|
||||
import { stripCppTemplateArgs, isCppConstructorDeclaration } from './languages/c-cpp';
|
||||
import { rustImplTypeName } from './languages/rust';
|
||||
import { isDartTypeName, pushDartTypeRefs } from './languages/dart';
|
||||
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeRefs } from './languages/dart';
|
||||
import { LiquidExtractor } from './liquid-extractor';
|
||||
import { RazorExtractor } from './razor-extractor';
|
||||
import { SvelteExtractor } from './svelte-extractor';
|
||||
@@ -5783,8 +5783,12 @@ export class TreeSitterExtractor {
|
||||
if (this.language === 'dart') {
|
||||
if (node.type !== 'selector') return;
|
||||
if (node.namedChildren.some((c: SyntaxNode) => c.type === 'argument_part')) return;
|
||||
const prev = node.previousNamedSibling;
|
||||
const prev = dartReceiverOf(node);
|
||||
if (prev?.type === 'identifier' && /^[A-Z][A-Za-z0-9_]*$/.test(prev.text)) {
|
||||
// `Map` in `x.read<Map<K, V>>(y)` parsed as comparisons is a type
|
||||
// argument, which the body walker references as a type.
|
||||
const before = knownParent?.type === 'relational_expression' ? prev.previousNamedSibling : null;
|
||||
if (before?.type === 'relational_operator' && dartMisparsedGenericCall(before)) return;
|
||||
this.pushStaticMemberRef(prev.text, ownerId, prev);
|
||||
}
|
||||
return;
|
||||
@@ -6600,7 +6604,7 @@ export class TreeSitterExtractor {
|
||||
// A member read that may run code — Dart's `x.area` calls the getter
|
||||
// `area` (#2338). The resolver links it to a getter, as a call, or to
|
||||
// nothing: a plain field read stays a reference that names no symbol.
|
||||
const read = this.extractor!.extractMemberRead?.(node);
|
||||
const read = this.extractor!.extractMemberRead?.(node, parent);
|
||||
if (read) {
|
||||
const readerId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (readerId) {
|
||||
@@ -6615,18 +6619,22 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
|
||||
// A type a Dart body names — a local's declared type, a generic argument
|
||||
// (`Future<Report?>.value(null)`, `context.read<Report>()`), a cast, a
|
||||
// type test — is the function's dependency, as a TS local's annotation
|
||||
// is just below (#2327).
|
||||
if (this.language === 'dart' && nodeType === 'type_identifier' && isDartTypeName(node)) {
|
||||
// (`Future<Report?>.value(null)`, `context.read<Report>()`, and
|
||||
// `ref.read<Report>(p)` when the grammar read that call as comparisons),
|
||||
// a cast, a type test — is the function's dependency, as a TS local's
|
||||
// annotation is just below (#2327).
|
||||
if (this.language === 'dart') {
|
||||
const typeNode = nodeType === 'type_identifier' ? node
|
||||
: nodeType === 'relational_operator' ? dartMisparsedGenericCall(node)?.typeName
|
||||
: undefined;
|
||||
const ownerId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (ownerId) {
|
||||
if (typeNode && ownerId && isDartTypeName(typeNode)) {
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: ownerId,
|
||||
referenceName: getNodeText(node, this.source),
|
||||
referenceName: getNodeText(typeNode, this.source),
|
||||
referenceKind: 'references',
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
line: typeNode.startPosition.row + 1,
|
||||
column: typeNode.startPosition.column,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user