fix(dart): index const constructors and redirecting factories; a call through a type stays on that type (#2380)

* fix(dart): index const constructors and redirecting factories; a call through a type stays on that type

Both Dart extractors (the TS walker and the native kernel) skipped
`const` constructors and redirecting factories (`const Foo.bar(...)`,
`const factory Foo.bar() = _Bar;`), which parse as signature kinds of
their own. A call such as flutter_bloc's `BlocProvider.value(...)` then
found no method on BlocProvider, and the resolver guessed another
type's `value`: on felangel/bloc, 89 calls linked to
`RepositoryProvider::value`.

Extraction (TS + kernel, at parity):
- Named const constructors and redirecting factories are methods named
  by the constructor, returning their class, with a parameters-only
  signature; the unnamed ones stay unindexed. The name is read only
  from before the parameter list, so a redirect target
  (`= _Impl.named`) never names it.
- A redirecting factory references the class it redirects to, not the
  target constructor's name or an import prefix.
- A constructor called with type arguments keeps its type:
  `BlocProvider<CounterCubit>.value(...)` is `BlocProvider.value`, as a
  statement and as an expression (`constructor_invocation`, which
  produced no call at all before).

Resolver:
- A Dart call through a type's name reaches only that type's own
  constructor, static member or static constant, or nothing: no guess
  by name, no inherited lookup, no same-file look-alike, no instance
  member of the same name. SDK calls like `Uri.parse` no longer land
  on a project `parse` past an `extension on Uri`.
- A named constructor is never reached by a bare name, the last link
  of a chain, a function value or an annotation (riverpod's 210
  `@internal` annotations).

Validated on felangel/bloc @b9be1e2 and rrousselGit/riverpod @4ba1be2
with the kernel loaded: bloc 18,338 -> 18,495 edges, riverpod 85,606
-> 85,854, every removed edge triaged. Afterwards every edge into a
named constructor is written `Owner.ctor`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(dart): read a type's static members one way for calls and chains

- A call through a type's name now finds its member with dartStaticMember,
  the lookup a chain's static link uses since #2376, so both pick the same
  member: the call site's own file first, then the nearest copy.
- isDartStaticMember's constructor test is isDartConstructor, anchored at
  the declaration: an instance getter declared just above a same-named
  factory (`Object? get error` over `const factory AsyncValue.error(...)`)
  read the factory from the next lines and counted as static.
- clearNameMatcherMemos drops DART_CONSTRUCTORS with the other
  source-derived Dart memos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-06 14:48:06 +00:00
committed by GitHub
parent 4d6168a701
commit 652c36b72a
9 changed files with 660 additions and 71 deletions
+1
View File
@@ -48,6 +48,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In VB.NET, reading or setting a field or property through a variable, parameter, field, `Me` or `MyBase`, like `x.Normal = 3`, `Me._h.Title`, `.Value` inside a `With` block or `.Switch` in `New BoolParam With {…}`, now counts as a use of that member, and so do calls inside a `With` block, functions called without parentheses and reads through a class whose name starts in lower case. Before, `codegraph callers` on an instance field or property missed most of its uses. Only a member that the value's declared type has or inherits is linked, nothing when that type comes from outside your project, and a value named after its class, like `theme As Theme`, counts as a use of the class only when it reads a `Shared` member. Re-index VB.NET projects after upgrading.
- 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.
## [1.6.2] - 2026-10-03
+277
View File
@@ -0,0 +1,277 @@
/**
* Dart `const` constructors and redirecting factories are methods, and a call
* through a type's name is to that type's own member or to nothing.
*
* `const Foo.bar(…)` and `const factory Foo.bar() = _Bar;` parse as signature
* kinds of their own, which neither extractor indexed. So flutter_bloc's
* `BlocProvider.value(…)` found no `value` on BlocProvider, and the resolver
* guessed the one other `value` — RepositoryProvider's non-const constructor —
* for all 89 calls in felangel/bloc.
*
* Dart inherits no static member and no constructor, so a call through a type
* the project declares never means another type's method: not a same-named
* one guessed by name, not a supertype's, not a same-file look-alike's. And
* the SDK's `Uri.parse(…)` is not a project `parse` because the project has an
* `extension on Uri`.
*
* A constructor is only ever called through its class. Written with type
* arguments — `BlocProvider<CounterCubit>.value(…)`, most of bloc's tests —
* the call still names the type, and a bare name or a chain's last link
* (`box.inner.value(1)`) never means a constructor.
*
* 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';
import type { Node } from '../src/types';
const FILES: Record<string, string> = {
'pubspec.yaml': 'name: app\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n',
'lib/bloc_provider.dart': `class BlocProvider<T> {
const BlocProvider({required T Function() create}) : _create = create, _value = null;
const BlocProvider.value({required T value}) : this._(value: value);
const BlocProvider._({T? value}) : _create = null, _value = value;
final T Function()? _create;
final T? _value;
void handle(Object value) => accept(value);
}
void accept(Object o) {}
`,
'lib/repository_provider.dart': `class RepositoryProvider<T> {
RepositoryProvider({required T Function() create});
RepositoryProvider.value({required T value});
static T of<T>(Object context) => throw 0;
}
`,
'lib/new_car_state.dart': `class NewCarState {
const NewCarState._({this.brands = const []});
const NewCarState.initial() : this._();
const factory NewCarState.loading() = _Loading;
const factory NewCarState() = _Loading.named;
const factory NewCarState.fromJson(Map<String, Object?> json) = _Loading.fromJson;
final List<String> brands;
}
class _Loading extends NewCarState {
const _Loading() : super._();
const _Loading.named() : super._();
const _Loading.fromJson(Map<String, Object?> json) : super._();
}
`,
'lib/profile_wizard_state.dart': `class ProfileWizardState {
ProfileWizardState.initial() : step = 0;
final int step;
Object fromJson(Object json) => json;
}
`,
// A look-alike declared first in the same file.
'lib/family.dart': `class EmptyFamily extends Family {
EmptyFamily._() : super._();
}
class Family {
const Family._();
static Family make() => Family._();
}
`,
'lib/sub.dart': `abstract class Base {
const Base();
factory Base.named() => const _BaseImpl();
}
class _BaseImpl extends Base {
const _BaseImpl();
}
class Sub extends Base {
const Sub();
const factory Sub.named() = _SubImpl;
}
class _SubImpl extends Sub {
const _SubImpl();
}
`,
'lib/analysis_options.dart': `class AnalysisOptions {
const AnalysisOptions(this.path);
factory AnalysisOptions.parse(String path) => AnalysisOptions(path);
final String path;
}
`,
'lib/linter.dart': `extension on Uri {
String get canonicalizedPath => path;
}
`,
// An instance getter declared before a constructor of the same name.
'lib/async_value.dart': `sealed class AsyncValue<T> {
const AsyncValue._();
Object? get error => null;
const factory AsyncValue.error(Object error) = AsyncError<T>;
}
class AsyncError<T> extends AsyncValue<T> {
const AsyncError(this.error) : super._();
@override
final Object error;
}
Object fail(Object e) => AsyncValue.error(e);
`,
'lib/change_notifier_provider.dart': `class ChangeNotifierProvider {
const ChangeNotifierProvider.internal();
}
`,
// package:meta's `internal` annotation is a constant outside the project.
'lib/scope.dart': `import 'package:meta/meta.dart';
@internal
Object guarded() => 0;
`,
'lib/app.dart': `import 'bloc_provider.dart';
import 'repository_provider.dart';
import 'new_car_state.dart';
import 'sub.dart';
Object build(Object counter) => BlocProvider.value(value: counter);
Object typed(Object counter) => BlocProvider<Object>.value(value: counter);
Object typedRepository(Object repository) => RepositoryProvider<Object>.value(value: repository);
NewCarState start() => NewCarState.initial();
NewCarState load() => NewCarState.loading();
Sub sub() => Sub.named();
Object find(Object context) => BlocProvider.of(context);
Uri link() => Uri.parse('https://example.com');
Object tail(dynamic box) => box.inner.value(1);
`,
};
describe('Dart const constructors and redirecting factories', () => {
let root = '';
let cg: CodeGraph | undefined;
let kernel: string | undefined;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-const-ctors-'));
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);
}
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'])('are indexed and called through their own type (%s)', async (backend) => {
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
else delete process.env.CODEGRAPH_KERNEL;
cg = await CodeGraph.init(root, { index: true });
const graph = cg;
const find = (file: string, qualifiedName: string): Node | undefined =>
graph.getNodesInFile(file).find((n) => n.qualifiedName === qualifiedName);
const node = (file: string, qualifiedName: string): Node => {
const found = find(file, qualifiedName);
expect(found, `${file} ${qualifiedName}`).toBeDefined();
return found!;
};
const targets = (from: Node, kind: string): string[] =>
[...new Set(graph
.getOutgoingEdgesFrom([from.id])
.filter((e) => e.kind === kind)
.map((e) => graph.getNode(e.target)!.qualifiedName))]
.sort();
const sources = (to: Node): string[] =>
[...new Set(graph
.getIncomingEdgesTo([to.id])
.filter((e) => e.kind !== 'contains')
.map((e) => `${e.kind} ${graph.getNode(e.source)!.qualifiedName}`))]
.sort();
// Named `const` constructors and redirecting factories are methods named
// by the constructor, returning their class; the unnamed ones stay
// unindexed, as the unnamed generative constructor is, and a redirecting
// factory's target (`= _Loading.named`) names nothing.
const value = node('lib/bloc_provider.dart', 'BlocProvider::value');
expect(value.kind).toBe('method');
expect(value.returnType).toBe('BlocProvider');
expect(value.signature).toBe('({required T value})');
node('lib/bloc_provider.dart', 'BlocProvider::_');
expect(find('lib/bloc_provider.dart', 'BlocProvider::BlocProvider')).toBeUndefined();
node('lib/new_car_state.dart', 'NewCarState::initial');
const loading = node('lib/new_car_state.dart', 'NewCarState::loading');
expect(loading.signature).toBe('()');
expect(find('lib/new_car_state.dart', 'NewCarState::NewCarState')).toBeUndefined();
expect(find('lib/new_car_state.dart', 'NewCarState::named')).toBeUndefined();
node('lib/new_car_state.dart', '_Loading::named');
node('lib/sub.dart', 'Sub::named');
// A redirecting factory refers to the class it constructs, and not to
// that class's constructor as if it were a type (`= _Loading.fromJson`).
expect(targets(loading, 'references')).toEqual(['_Loading']);
expect(targets(node('lib/new_car_state.dart', 'NewCarState::fromJson'), 'references')).toEqual(['_Loading']);
expect(sources(node('lib/profile_wizard_state.dart', 'ProfileWizardState::fromJson'))).toEqual([]);
// Each call reaches the constructor of the type it names, type arguments
// or not...
expect(targets(node('lib/app.dart', 'build'), 'calls')).toEqual(['BlocProvider::value']);
expect(targets(node('lib/app.dart', 'typed'), 'calls')).toEqual(['BlocProvider::value']);
expect(targets(node('lib/app.dart', 'typedRepository'), 'calls')).toEqual(['RepositoryProvider::value']);
expect(targets(node('lib/app.dart', 'start'), 'calls')).toEqual(['NewCarState::initial']);
expect(targets(node('lib/app.dart', 'load'), 'calls')).toEqual(['NewCarState::loading']);
expect(targets(node('lib/app.dart', 'sub'), 'calls')).toEqual(['Sub::named']);
expect(targets(node('lib/family.dart', 'Family::make'), 'calls')).toEqual(['Family::_']);
// A constructor, not the instance getter that shares its name.
const errorFactory = graph.getNodesInFile('lib/async_value.dart')
.find((n) => n.qualifiedName === 'AsyncValue::error' && n.signature === '(Object error)');
expect(errorFactory).toBeDefined();
expect(graph.getOutgoingEdgesFrom([node('lib/async_value.dart', 'fail').id])
.filter((e) => e.kind === 'calls').map((e) => e.target)).toEqual([errorFactory!.id]);
// ...and never another type's member of that name.
expect(sources(node('lib/repository_provider.dart', 'RepositoryProvider::value'))).toEqual(['calls typedRepository']);
expect(sources(node('lib/profile_wizard_state.dart', 'ProfileWizardState::initial'))).toEqual([]);
expect(sources(node('lib/family.dart', 'EmptyFamily::_'))).toEqual([]);
expect(sources(node('lib/sub.dart', 'Base::named'))).toEqual([]);
// A member the named type does not declare is outside the project.
expect(targets(node('lib/app.dart', 'find'), 'calls')).toEqual([]);
expect(sources(node('lib/repository_provider.dart', 'RepositoryProvider::of'))).toEqual([]);
expect(targets(node('lib/app.dart', 'link'), 'calls')).toEqual([]);
expect(sources(node('lib/analysis_options.dart', 'AnalysisOptions::parse'))).toEqual([]);
// A constructor is only ever reached through its class: a parameter that
// shares its name, the last link of a chain, or an annotation is not it.
expect(targets(node('lib/app.dart', 'tail'), 'calls')).toEqual([]);
expect(sources(value)).toEqual(['calls build', 'calls typed']);
expect(sources(node('lib/change_notifier_provider.dart', 'ChangeNotifierProvider::internal'))).toEqual([]);
});
});
@@ -0,0 +1,86 @@
import 'package:meta/meta.dart';
import 'package:other/other.dart' as p;
const kDepth = 3;
/// A provider with const constructors and redirecting factories.
class ProviderT<T> {
/// The unnamed const constructor: no node.
const ProviderT({required this.create, this.lazy = true}) : _value = null;
/// A named const constructor that redirects.
const ProviderT.value({required T value, int depth = kDepth})
: this._(value: value);
@visibleForTesting
const ProviderT._({T? value})
: create = null,
lazy = false,
_value = value;
const factory ProviderT.redirect({int a}) = _ImplT<T>;
factory ProviderT.plain() = _ImplT<T>.named;
const factory ProviderT.json(Map<String, Object?> json) = _ImplT.fromJson;
const factory ProviderT.prefixed() = p.OtherT;
const factory ProviderT.prefixedNamed() = p.OtherT.named;
const factory ProviderT.unnamedTarget() = _ImplT;
const factory ProviderT() = _ImplT.named;
final Object? create;
final bool lazy;
final T? _value;
void handle(Object value) => accept(value);
static ProviderT<int> make() => ProviderT<int>.value(value: 1);
static Object typed(dynamic box) {
ProviderT<int>.value(value: 2);
final made = ProviderT<int>.value(value: 3);
accept(ProviderT<int>.value(value: 4));
wrap(child: ProviderT<int>.value(value: 5));
box.inner.value(6);
return p.OtherT<int>.named(made);
}
}
class _ImplT<T> extends ProviderT<T> {
const _ImplT({int a = 0}) : super._();
const _ImplT.named() : super._();
const _ImplT.fromJson(Map<String, Object?> json) : super._();
}
class _$GeneratedT {
const _$GeneratedT.create();
}
enum StatusT {
ok(200),
gone.named(410);
const StatusT(this.code);
const StatusT.named(this.code);
final int code;
}
extension type const MetersT(int value) {
const MetersT.zero() : this(0);
}
final topProvider = ProviderT<int>.value(value: 7);
Object build() => ProviderT.value(value: 8);
void accept(Object o) {}
void wrap({required Object child}) {}
+9 -2
View File
@@ -19,12 +19,15 @@
* and the exact duplicated-ref interleave; TortureFnrefDart.dart: fn-ref
* capture channels incl. named-argument non-capture and the file/class
* twins; TortureMini/TortureSigs/TortureCtors/TortureVrefDart: signatures
* verbatim, prefixed-return-type prefix bug, const factories invisible,
* verbatim, prefixed-return-type prefix bug, a const factory as a method,
* value-ref matrix with `$X` vs `${X}` asymmetry; TortureReadsTypes.dart:
* getter-read refs and the type positions outside signatures — #2338/#2327;
* TortureInitializers.dart: initializers walked as the code their constant,
* class or file runs, every member-body kind, and a local declaration's
* second variable left to its function) and their CRLF variants
* 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
* (derived in-memory — #1329), plus defer and generated-file pins.
*
* The full-repo sweeps live in scripts/kernel-parity.mjs (shelf/bloc/flutter
@@ -122,6 +125,10 @@ describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => {
// instantiations, reads, closures with block bodies and local functions,
// and function values captured once, for the declaration.
['TortureInitializers.dart', 30],
// `const` constructors and redirecting factories as methods (the unnamed
// ones skipped), every redirect-target form, and constructors called
// with type arguments in statements and expressions.
['TortureConstCtors.dart', 20],
] as const;
for (const [file, minNodes] of FIXTURES) {
+100 -19
View File
@@ -18,8 +18,9 @@
//! `.`); `ConfigT.load()` double-emits (calls + a static-member references
//! ref — no callee-of-call skip in the dart branch); operator methods mint
//! `method "<anonymous>"`; the unnamed constructor is skipped
//! (isMisparsedFunction) while named ctors/factories are named by the CTOR
//! name with the class as returnType; instance fields mint NO nodes (only
//! (isMisparsedFunction) while named ctors/factories — `const` ones and
//! redirecting factories included — are named by the CTOR name with the
//! class as returnType; instance fields mint NO nodes (only
//! static_final_declaration → constant, via the hook); every initializer is
//! body-walked once — a constant's from the constant, a field's or a
//! top-level variable's from the class or the file; prefixed return
@@ -90,6 +91,23 @@ fn angle_args_re() -> &'static Regex {
RE.get_or_init(|| Regex::new(r"<[^>]*>").unwrap())
}
/// The UpperCamel test of isDartTypeName (`/^[_$]*[A-Z]/`) — how Dart writes
/// a type, `_Private` and `$Generated` ones included.
fn is_upper_camel(text: &str) -> bool {
text.trim_start_matches(&['_', '$'][..])
.chars()
.next()
.map(|c| c.is_ascii_uppercase())
.unwrap_or(false)
}
/// DART_BODILESS_CTORS (dart.ts) — `const Foo.bar();` and a redirecting
/// factory `const factory Foo.bar() = _Bar;`, each a signature kind of its
/// own inside a `declaration`.
fn is_bodiless_ctor(kind: &str) -> bool {
matches!(kind, "constant_constructor_signature" | "redirecting_factory_constructor_signature")
}
struct Scope {
row: u32,
kind: &'static str,
@@ -385,7 +403,9 @@ impl<'t> Walker<'t> {
/// dartConstructorSignature (dart.ts:25-35).
fn constructor_signature(&self, node: Node<'t>) -> Option<Node<'t>> {
if matches!(node.kind(), "factory_constructor_signature" | "constructor_signature") {
if matches!(node.kind(), "factory_constructor_signature" | "constructor_signature")
|| is_bodiless_ctor(node.kind())
{
return Some(node);
}
if node.kind() == "method_signature" {
@@ -415,9 +435,12 @@ impl<'t> Walker<'t> {
/// dartCtorInfo (dart.ts:61-70).
fn ctor_info(&self, node: Node<'t>) -> Option<(String, String)> {
let ctor = self.constructor_signature(node)?;
// The names before the parameters: a redirecting factory names its
// target after them (`factory Foo() = _Impl.named;`).
let mut cursor = ctor.walk();
let ids: Vec<Node<'t>> = ctor
.named_children(&mut cursor)
.take_while(|c| c.kind() != "formal_parameter_list")
.filter(|c| c.kind() == "identifier")
.collect();
let class_name = self.enclosing_type_name(node)?;
@@ -448,11 +471,13 @@ impl<'t> Walker<'t> {
Some(last.to_string())
}
/// isMisparsedFunction (dart.ts:177-188) — skip the UNNAMED constructor.
fn is_unnamed_ctor(&self, node: Node<'t>) -> bool {
/// isMisparsedFunction (dart.ts:177-188) — skip the UNNAMED constructor,
/// and a `const` ctor / redirecting factory that names no enclosing type
/// (error recovery's).
fn is_misparsed_function(&self, node: Node<'t>) -> bool {
match self.ctor_info(node) {
Some((class_name, ctor_name)) => ctor_name == class_name,
None => false,
None => is_bodiless_ctor(node.kind()),
}
}
@@ -463,10 +488,15 @@ impl<'t> Walker<'t> {
let params = sig
.named_children(&mut c1)
.find(|c| c.kind() == "formal_parameter_list");
// A constructor has no return type: the type a redirecting factory
// names is its target (`= _Impl`).
let mut c2 = sig.walk();
let ret = sig
.named_children(&mut c2)
.find(|c| matches!(c.kind(), "type_identifier" | "void_type"));
let ret = if is_bodiless_ctor(sig.kind()) {
None
} else {
sig.named_children(&mut c2)
.find(|c| matches!(c.kind(), "type_identifier" | "void_type"))
};
if params.is_none() && ret.is_none() {
return None;
}
@@ -704,7 +734,10 @@ impl<'t> Walker<'t> {
self.extract_class(node);
return;
}
"method_signature" | "constructor_signature" => {
"method_signature"
| "constructor_signature"
| "constant_constructor_signature"
| "redirecting_factory_constructor_signature" => {
self.extract_method(node);
return;
}
@@ -753,7 +786,7 @@ impl<'t> Walker<'t> {
}
// isMisparsedFunction: the unnamed constructor is skipped — node
// suppressed, body still walked (attributed to the current stack top).
if self.is_unnamed_ctor(node) {
if self.is_misparsed_function(node) {
if let Some(body) = self.resolve_body(node) {
self.visit_body(body);
}
@@ -800,7 +833,7 @@ impl<'t> Walker<'t> {
}
let name = self.extract_name(node);
// isMisparsedFunction — the unnamed ctor: body-only walk.
if self.is_unnamed_ctor(node) {
if self.is_misparsed_function(node) {
if let Some(body) = self.resolve_body(node) {
self.visit_body(body);
}
@@ -1035,6 +1068,11 @@ impl<'t> Walker<'t> {
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).
@@ -1091,6 +1129,21 @@ impl<'t> Walker<'t> {
};
}
// `=> BlocProvider<CounterCubit>.value(…)` — a constructor called
// with type arguments, as most expressions parse it. The type is the
// LAST type_identifier: an import prefix (`p.X<T>.named`) is one too.
if node.kind() == "constructor_invocation" {
let mut c1 = node.walk();
let type_id = node.named_children(&mut c1).filter(|c| c.kind() == "type_identifier").last();
let mut c2 = node.walk();
let name_id = node.named_children(&mut c2).find(|c| c.kind() == "identifier");
return match (type_id, name_id) {
(Some(t), Some(n)) => Some(format!("{}.{}", self.text(t), self.text(n))),
(Some(t), None) => Some(self.text(t).to_string()),
_ => None,
};
}
None
}
@@ -1119,6 +1172,9 @@ impl<'t> Walker<'t> {
if ap.kind() == "identifier" {
return Some(format!("{}.{}", self.text(ap), self.text(method_id)));
}
if let Some(type_name) = self.type_arguments_receiver(ap) {
return Some(format!("{}.{}", type_name, self.text(method_id)));
}
}
return Some(self.text(method_id).to_string());
}
@@ -1126,6 +1182,23 @@ impl<'t> Walker<'t> {
None
}
/// dartTypeArgumentsReceiver (dart.ts) — `BlocProvider` in
/// `BlocProvider<CounterCubit>.value(…)`, when `selector` is the `<…>`
/// between the type and `.value`. Only a constructor is called that way.
fn type_arguments_receiver(&self, selector: Node<'t>) -> Option<&'t str> {
if selector.kind() != "selector" || selector.named_child_count() != 1 {
return None;
}
if selector.named_child(0)?.kind() != "type_arguments" {
return None;
}
let type_node = selector.prev_named_sibling()?;
if type_node.kind() != "identifier" {
return None;
}
Some(self.text(type_node))
}
// --- extractStaticMemberRef — the dart branch (:4759-4767) ------------
fn extract_static_member_ref(&mut self, node: Node<'t>) {
@@ -1326,6 +1399,20 @@ impl<'t> Walker<'t> {
} else {
node
};
// A redirecting factory names the class it constructs after `=`, then
// perhaps that class's constructor (`= _$QuestionImpl.fromJson`, two
// type_identifiers). Neither the constructor nor an import prefix is
// a type, and Dart writes types UpperCamel.
if sig.kind() == "redirecting_factory_constructor_signature" {
let mut cursor = sig.walk();
let kids: Vec<Node<'t>> = sig.named_children(&mut cursor).collect();
for child in kids {
if child.kind() != "type_identifier" || is_upper_camel(self.text(child)) {
self.type_refs_from_subtree(child, row);
}
}
return;
}
self.type_refs_from_subtree(sig, row);
}
@@ -1354,13 +1441,7 @@ impl<'t> Walker<'t> {
/// constructor call names.
fn is_type_name_dart(&self, node: Node<'t>) -> bool {
let text = self.text(node);
let upper = text
.trim_start_matches(&['_', '$'][..])
.chars()
.next()
.map(|c| c.is_ascii_uppercase())
.unwrap_or(false);
if !upper || is_builtin_type(text) {
if !is_upper_camel(text) || is_builtin_type(text) {
return false;
}
if node.next_sibling().map(|s| s.kind() == ".").unwrap_or(false) {
+36 -14
View File
@@ -190,7 +190,9 @@ change dart's grammar, which is exactly the hazard this vendor kills.
Types: functionTypes=[`function_signature`] (:119); classTypes=
[`class_definition`] (:120); methodTypes=[`method_signature`,
`constructor_signature`] (:126); interfaceTypes=[] ; structTypes=[];
`constructor_signature`, `constant_constructor_signature`,
`redirecting_factory_constructor_signature`] (:126 — the last two since
2026-10-06); interfaceTypes=[] ; structTypes=[];
enumTypes=[`enum_declaration`] (:129); enumMemberTypes=[`enum_constant`]
(:130); typeAliasTypes=[`type_alias`] (:131); importTypes=[`import_or_export`]
(:132); **callTypes=[] (:133 — extractCall NEVER runs for dart; all call refs
@@ -237,9 +239,14 @@ Grammar-shape facts the config leans on (all probed, `mini-cst.txt` /
initializers]`) — reached by the walker via plain recursion into
`declaration`. `const` constructors are `declaration >
constant_constructor_signature` and redirecting factories are
`declaration > redirecting_factory_constructor_signature` — **NEITHER is
in methodTypes → const ctors and `const factory X.r() = Impl;` are
INVISIBLE (no node, no refs)**. Fields are `declaration >
`declaration > redirecting_factory_constructor_signature` — both carry
NO fields (children: `const_builtin`? `"factory"`? identifier [`.`
identifier] formal_parameter_list, then for a redirect `=` and the
target: `type_identifier` [type_arguments] [`.` name], where the target
constructor's name is a `type_identifier` without type arguments and an
`identifier` after them). Until 2026-10-06 neither was in methodTypes
(no node, no refs); now both mint methods exactly like the bare
`constructor_signature` (§isMisparsedFunction, §resolveName). Fields are `declaration >
(static)? (final|const|type|var) > initialized_identifier_list |
static_final_declaration_list` (§Constants).
- **Statements/expressions:** calls have NO call node — a postfix chain is
@@ -349,7 +356,12 @@ Hooks PRESENT (port each exactly — anchors into languages/dart.ts):
extension_declaration/enum_declaration and reads ITS name field) and
`reduce` ≠ `Action` → treated as the method it is (pinned:
`extract-probe4.txt` — method `reduce`, position starting AT `reduce`,
sig/ret undefined, decorates override intact).
sig/ret undefined, decorates override intact). dartCtorInfo reads only the
identifiers BEFORE the formal_parameter_list, so a redirecting factory's
target (`factory Foo() = _Impl.named;`) never names it. A
`constant_constructor_signature` / `redirecting_factory_constructor_signature`
that dartCtorInfo rejects (names no enclosing type — error recovery) is
skipped too: it can be nothing else.
- **getSignature (:189-208)** — method_signature unwraps to
function/getter/setter signature (NOT ctor/factory/operator); params =
find namedChild `formal_parameter_list`; retType = find namedChild
@@ -365,7 +377,9 @@ Hooks PRESENT (port each exactly — anchors into languages/dart.ts):
formal_parameter_list on method_signature); **bodiless named ctor
(declaration > constructor_signature) → `"()"`-style params-only sig**
(node IS the ctor signature; its formal_parameter_list is found by type) —
pin BOTH ctor sig shapes; operators → undefined.
pin BOTH ctor sig shapes; operators → undefined. `const` ctors and
redirecting factories → params only too: no retType is looked up for them
(a redirect's `= _Impl` type_identifier is its target, not a return type).
- **getVisibility (:209-222)** — method_signature → unwrap to
function/getter/setter signature → its first `identifier` child; other
nodes → childForFieldName('name'). Name starts `_` → 'private', else
@@ -386,9 +400,9 @@ Hooks PRESENT (port each exactly — anchors into languages/dart.ts):
children for type `'static'` → true; else false. `static WidgetT make()`
→ true ✓ (the `static` keyword is an anon child of method_signature);
bare constructor_signature → false. Top-level functions → false.
- **resolveName (:244-260)** — dartCtorInfo: named ctor/factory → the ctor
name (`named`, `create`, `_`); unnamed ctor or non-ctor → undefined (falls
to extractName §below).
- **resolveName (:244-260)** — dartCtorInfo: named ctor/factory (`const`
and redirecting ones included) → the ctor name (`named`, `create`, `_`);
unnamed ctor or non-ctor → undefined (falls to extractName §below).
- **extractImport (:261-304)** — importText = trimmed full node slice;
moduleName = the URI string content: `library_import >
import_specification > configurable_uri > uri > string_literal` (else the
@@ -442,7 +456,7 @@ Registration: `EXTRACTORS.dart` (languages/index.ts:56), `FN_REF_SPECS.dart`
| `import_or_export` | importTypes:1209 → extractImport:3170 | §Extractor config |
| `new_expression` | INSTANTIATION_KINDS:1255 (`new_expression` ∈ :354-361) | extractInstantiation:4610 → ctor field lookups null → namedChild(0) = type_identifier → `instantiates` ref from stack top; `<`-strip + last-`.`-segment apply (`new p.Foo<T>()` → `Foo`). findAnonymousClassBody → always null for dart. Children still recursed |
| `function_body` (sibling of a consumed signature) | **no branch** | recursed → THE DOUBLE-WALK (§below) |
| `declaration` (fields, bodiless ctors) | no branch | recursed → constructor_signature hits methodTypes; each field `initialized_identifier` hits the hook (body-walked for the class); constant_constructor_signature, redirecting_factory_constructor_signature, initializers, annotations-in-place: nothing |
| `declaration` (fields, bodiless ctors) | no branch | recursed → constructor_signature, constant_constructor_signature and redirecting_factory_constructor_signature hit methodTypes; each field `initialized_identifier` hits the hook (body-walked for the class); initializers, annotations-in-place: nothing |
| `getter_signature` / `setter_signature` BARE (top level) | no branch | **top-level getters/setters are INVISIBLE** (no node; their sibling function_body is visitNode-recursed where calls don't extract) — in classes they're method_signature-wrapped → methods |
| `const_object_expression`, `selector`, `cascade_section`, `assignment_expression`, `local_variable_declaration`, patterns, `extension_type_declaration`, `part_directive`, `library_name`, lambdas | no branch | recursed; calls only extract in the BODY walker (`extractBareCall` is not consulted by visitNode!) — §Calls for the consequences |
| `property_signature`/`method_signature` TS branch (:1282) | **shadowed** | method_signature is consumed at :1027 first; property_signature isn't a dart kind — branch unreachable |
@@ -643,8 +657,9 @@ chain → FIRST value child only); `static final sharedInst = WidgetT(0)` →
constant under the class, sig `= WidgetT`; multi-declarations → one node
each with own columns. **NO nodes ever**: instance fields (typed/untyped/
late/var), `static var`, top-level var/typed vars, top-level getters/
setters, const constructors, redirecting factories, extension_type
containers, `part`/`part of`/`library`/deferred imports. **Initializers
setters, extension_type containers, `part`/`part of`/`library`/deferred
imports. (Named `const` constructors and redirecting factories mint
methods since 2026-10-06; the unnamed ones still mint nothing.) **Initializers
are code:** every initializer is walked with the body walker — a
constant's for the constant (riverpod's `final repoProvider =
Provider((ref) => Repository(ref.watch(dioProvider)));` → calls
@@ -682,7 +697,9 @@ extract-mini.txt):
| `w?.render()` | conditional_assignable_selector | `w.render` — **`?.` is encoded exactly like `.`** |
| `y2..add(1)..add(2)` (cascades) | cascade_section (argument_part NOT inside a selector) | **NOTHING — cascade calls are completely invisible** |
| `new WidgetT(2)` | new_expression → INSTANTIATION branch :5145 FIRST | `instantiates WidgetT` (extractBareCall's new_expression arm :363-367 is DEAD — the else-if never reaches it); args still recursed |
| `pad(const EdgeInsetsT.all(8.0))` | const_object_expression :369-376 | `EdgeInsetsT.all` at the CONST node position (typeId + '.' + nameId; type-only form → `EdgeInsetsT`); children recursed after |
| `pad(const EdgeInsetsT.all(8.0))` | const_object_expression :369-376 | `EdgeInsetsT.all` at the CONST node position (typeId + '.' + nameId; type-only form → `EdgeInsetsT`); children recursed after. The FIRST type_identifier: `const p.X.named()` → `p.named` (prefix bug, preserved) |
| `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` |
| `await fetch()` | recursion through unary/await_expression | `fetch` at the selector |
| `throw StateError('bad')` | recursion | `StateError` |
@@ -746,7 +763,12 @@ BUILTIN_TYPES (:5768-5782), at each leaf's position. Consequences (pinned):
= constructor_signature → param types emit (`Widget.named(WidgetT w)` →
references WidgetT); `this.`-params (constructor_param) hold no
type_identifier → nothing. Bodiless declaration-wrapped ctors: extractMethod
runs on the bare constructor_signature → sig = node → same.
runs on the bare constructor_signature → sig = node → same; so do `const`
ctors. A redirecting factory walks its named children one by one and
skips a DIRECT type_identifier that is not UpperCamel (`/^[_$]*[A-Z]/`):
after `=` those are an import prefix (`= p.Impl`) or the target
constructor's name (`= _$QuestionImpl.fromJson`, where `fromJson` is a
type_identifier) — `references _$QuestionImpl` only.
- Getters: references from the getter's TYPE (suppressed if builtin —
`int get area` → nothing; `WidgetT get w` → references WidgetT).
- extractVariableTypeAnnotation (:6074) needs a `type_annotation` child —
+63 -5
View File
@@ -100,6 +100,16 @@ function dartInnerSignature(node: SyntaxNode): SyntaxNode {
return node;
}
/**
* The constructors that never have a body — `const Foo.bar();` and a
* redirecting factory `const factory Foo.bar() = _Bar;` — each parse as a
* signature kind of its own, inside a `declaration`.
*/
const DART_BODILESS_CTORS: ReadonlySet<string> = new Set([
'constant_constructor_signature',
'redirecting_factory_constructor_signature',
]);
/**
* The factory/named-constructor signature inside a node, if any. A constructor
* parses as `method_signature > {factory_,}constructor_signature` (e.g.
@@ -107,7 +117,10 @@ function dartInnerSignature(node: SyntaxNode): SyntaxNode {
* and — for a named ctor — the constructor-name identifier.
*/
function dartConstructorSignature(node: SyntaxNode): SyntaxNode | undefined {
if (node.type === 'factory_constructor_signature' || node.type === 'constructor_signature') {
if (
node.type === 'factory_constructor_signature' || node.type === 'constructor_signature' ||
DART_BODILESS_CTORS.has(node.type)
) {
return node;
}
if (node.type === 'method_signature') {
@@ -146,7 +159,13 @@ function dartEnclosingTypeName(node: SyntaxNode): string | undefined {
function dartCtorInfo(node: SyntaxNode): { className: string; ctorName: string } | undefined {
const ctor = dartConstructorSignature(node);
if (!ctor) return undefined;
const ids = ctor.namedChildren.filter((c: SyntaxNode) => c.type === 'identifier');
// The names before the parameters: a redirecting factory names its target
// after them (`factory Foo() = _Impl.named;`).
const ids: SyntaxNode[] = [];
for (const c of ctor.namedChildren) {
if (c.type === 'formal_parameter_list') break;
if (c.type === 'identifier') ids.push(c);
}
const className = dartEnclosingTypeName(node);
if (!className || !ids[0]) return undefined;
if (ids[0].text !== className) return undefined; // misparsed method, not a ctor
@@ -176,6 +195,19 @@ function extractDartReturnType(node: SyntaxNode, source: string): string | undef
return last;
}
/**
* The type a call written with type arguments goes through — `BlocProvider`
* in `BlocProvider<CounterCubit>.value(…)`, when `selector` is the `<…>`
* between the type and `.value`. Only a constructor is called that way.
*/
function dartTypeArgumentsReceiver(selector: SyntaxNode | null): string | undefined {
if (selector?.type !== 'selector' || selector.namedChildCount !== 1 || selector.namedChild(0)?.type !== 'type_arguments') {
return undefined;
}
const type = selector.previousNamedSibling;
return type?.type === 'identifier' ? type.text : undefined;
}
/**
* The callee name of the Dart call whose `argument_part` selector is `argPart`
* — mirrors the main extractBareCall accessor logic so a chained receiver
@@ -194,6 +226,8 @@ function dartCalleeOfArgPart(argPart: SyntaxNode): string | undefined {
if (methodId) {
const accessorPrev = prev.previousNamedSibling;
if (accessorPrev?.type === 'identifier') return accessorPrev.text + '.' + methodId.text;
const typeName = dartTypeArgumentsReceiver(accessorPrev);
if (typeName) return typeName + '.' + methodId.text;
return methodId.text;
}
}
@@ -208,7 +242,11 @@ export const dartExtractor: LanguageExtractor = {
// constructor `Foo._()` parses as a bare `constructor_signature`, so include
// it too — resolveName names it by the ctor name and getReturnType gives it
// the class as its return type, so `Foo._().bar()` chains resolve (#750).
methodTypes: ['method_signature', 'constructor_signature'],
// `const` constructors and redirecting factories have signature kinds of
// their own: flutter_bloc's `const BlocProvider.value(…)` is one, and
// without its node a `BlocProvider.value(…)` call went to another class's
// `value`.
methodTypes: ['method_signature', 'constructor_signature', ...DART_BODILESS_CTORS],
interfaceTypes: [],
structTypes: [],
enumTypes: ['enum_declaration'],
@@ -314,7 +352,10 @@ export const dartExtractor: LanguageExtractor = {
// tree-sitter misparsed as a ctor (`@override (T) m()`) is NOT skipped here.
// (isMisparsedFunction skips node creation but still visits the body.)
const ctor = dartCtorInfo(node);
return ctor != null && ctor.ctorName === ctor.className;
// A `const` constructor or redirecting factory is nothing else, so one
// that names no enclosing type is error recovery's, and skipped.
if (!ctor) return DART_BODILESS_CTORS.has(node.type);
return ctor.ctorName === ctor.className;
},
getSignature: (node, source) => {
// For function_signature: extract params + return type
@@ -327,7 +368,9 @@ export const dartExtractor: LanguageExtractor = {
if (inner) sig = inner;
}
const params = sig.namedChildren.find((c: SyntaxNode) => c.type === 'formal_parameter_list');
const retType = sig.namedChildren.find((c: SyntaxNode) =>
// A constructor has no return type: the type a redirecting factory names
// is its target (`= _Impl`).
const retType = DART_BODILESS_CTORS.has(sig.type) ? undefined : sig.namedChildren.find((c: SyntaxNode) =>
c.type === 'type_identifier' || c.type === 'void_type'
);
if (!params && !retType) return undefined;
@@ -467,6 +510,10 @@ export const dartExtractor: LanguageExtractor = {
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
@@ -512,6 +559,17 @@ export const dartExtractor: LanguageExtractor = {
return undefined;
}
// `=> BlocProvider<CounterCubit>.value(…)` — a constructor called with type
// arguments, as most expressions parse it. The type is the last
// `type_identifier`: an import prefix (`p.X<T>.named`) is one too.
if (node.type === 'constructor_invocation') {
const typeId = node.namedChildren.filter((c: SyntaxNode) => c.type === 'type_identifier').pop();
const nameId = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
if (typeId && nameId) return typeId.text + '.' + nameId.text;
if (typeId) return typeId.text;
return undefined;
}
return undefined;
},
extractMemberRead: dartMemberRead,
+12
View File
@@ -7280,6 +7280,18 @@ export class TreeSitterExtractor {
c.type === 'factory_constructor_signature'
) ?? node;
}
// A redirecting factory names the class it constructs after `=`, then
// perhaps that class's constructor (`= _$QuestionImpl.fromJson`, which
// parses as two type_identifiers). Neither the constructor nor an import
// prefix is a type, and Dart writes types UpperCamel.
if (sig.type === 'redirecting_factory_constructor_signature') {
for (const child of sig.namedChildren) {
if (child.type !== 'type_identifier' || /^[_$]*[A-Z]/.test(child.text)) {
this.extractTypeRefsFromSubtree(child, nodeId);
}
}
return;
}
this.extractTypeRefsFromSubtree(sig, nodeId);
return;
}
+76 -31
View File
@@ -1106,7 +1106,9 @@ export function matchFunctionRef(
// defines inside `test_custom_auth`. Those still count against a lone
// cross-file guess below — a name several functions use for themselves is
// as likely a local's.
let candidates = named.filter((n) => isLexicallyReachable(n, ref, context));
let candidates = named.filter((n) => isLexicallyReachable(n, ref, context) &&
// A Dart constructor is torn off through its class, never by a bare name.
!isDartConstructor(n, context));
if (candidates.length === 0) return null;
// A Python name the function around it binds — a parameter, an assignment —
// is that local's value: httpx's `auth_flow(self, request)` handing `request`
@@ -2866,6 +2868,32 @@ function isDartMethodInScope(method: Node, ref: UnresolvedRef, context: Resoluti
return dartMemberDepth(method, ref, context) < Infinity;
}
const DART_CONSTRUCTORS = new WeakMap<ResolutionContext, Map<string, boolean>>();
/**
* Whether a Dart method is a named constructor — `Foo.named(…)`, `factory
* Foo.of(…)`, `const Foo.value(…)`, `const factory Foo.x() = _X;` — read from
* its declaration. Dart reaches one only through its class (`Foo.value(…)`,
* the tear-off `Foo.value`), never by a bare name: a parameter `value` passed
* on is not `const BlocProvider.value(…)`.
*/
function isDartConstructor(n: Node, context: ResolutionContext): boolean {
if (n.language !== 'dart' || n.kind !== 'method') return false;
let memo = DART_CONSTRUCTORS.get(context);
if (!memo) DART_CONSTRUCTORS.set(context, (memo = new Map()));
const hit = memo.get(n.id);
if (hit !== undefined) return hit;
const cut = n.qualifiedName.lastIndexOf('::');
const owner = cut > 0 ? n.qualifiedName.slice(0, cut).split('::').pop()! : '';
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
const head = (lines[n.startLine - 1] ?? '').slice(n.startColumn);
const escape = (name: string): string => name.replace(/\$/g, '\\$');
const constructor = owner !== '' &&
new RegExp(String.raw`^(?:(?:const|factory|external)\s+)*${escape(owner)}\s*\.\s*${escape(n.name)}(?![\w$])`).test(head);
memo.set(n.id, constructor);
return constructor;
}
/**
* How many supertype steps separate the class a Dart call is written in from
* `method`'s owner: 0 for its own member, Infinity when the owner is not in
@@ -3461,25 +3489,21 @@ function dartStaticMember(typeName: string, name: string, ref: UnresolvedRef, co
const found = context.getNodesByName(name).filter((m) => m.language === 'dart' &&
(m.kind === 'method' || m.kind === 'function' || m.kind === 'constant' || m.kind === 'enum_member') &&
owners.some((o) => o.filePath === m.filePath && m.qualifiedName === `${o.qualifiedName}::${name}`) &&
!isDartSetter(m, context) && isDartStaticMember(m, typeName, context));
!isDartSetter(m, context) && isDartStaticMember(m, context));
if (found.length <= 1) return found[0] ?? null;
return found.find((m) => m.filePath === ref.filePath) ??
found.reduce((a, b) => (computePathProximity(ref.filePath, b.filePath) > computePathProximity(ref.filePath, a.filePath) ? b : a));
}
/**
* Whether a member of the Dart type `owner` is reached through the type
* itself: a static member, constant or enum value, or a named constructor or
* factory (`factory ProviderContainer.test(…)`) — not an instance member
* sharing a constructor's name (`AsyncValue`'s `error` getter beside its
* `AsyncValue.error(…)` factory).
* Whether a member of a Dart type is reached through the type itself: a
* static member, constant or enum value, or a named constructor or factory
* (`factory ProviderContainer.test(…)`) — not an instance member sharing a
* constructor's name (`AsyncValue`'s `error` getter beside its
* `AsyncValue.error(…)` factory, even declared on the line above it).
*/
function isDartStaticMember(m: Node, owner: string, context: ResolutionContext): boolean {
if (m.isStatic || m.kind === 'constant' || m.kind === 'enum_member') return true;
const lines = context.getFileLines?.(m.filePath) ?? context.readFile(m.filePath)?.split(/\r?\n/) ?? [];
const head = `${(lines[m.startLine - 1] ?? '').slice(m.startColumn)} ${lines[m.startLine] ?? ''} ${lines[m.startLine + 1] ?? ''}`;
const esc = (s: string): string => s.replace(/\$/g, '\\$');
return new RegExp(`(?:^|[^\\w$.])${esc(owner)}\\s*\\.\\s*${esc(m.name)}\\s*[(<]`).test(head);
function isDartStaticMember(m: Node, context: ResolutionContext): boolean {
return m.isStatic === true || m.kind === 'constant' || m.kind === 'enum_member' || isDartConstructor(m, context);
}
/**
@@ -3969,6 +3993,31 @@ function dartExtensionMemberOf(typeName: string, name: string, ref: UnresolvedRe
return found?.viaExtension ? found.node : null;
}
/**
* A Dart call through a type's name — `BlocProvider.value(…)`, riverpod's
* `FutureProvider.autoDispose(…)` — is to one of that type's own constructors
* or static members, or to nothing the project declares. Dart inherits none
* of them, so a guess past the type is always wrong: flutter_bloc's
* `BlocProvider.value(…)` went to `RepositoryProvider`'s `value` 89 times,
* riverpod's `Family2Family._()` to the `EmptyFamily2Family._` declared above
* it, and the SDK's `Uri.parse(…)`, past bloc_lint's `extension on Uri`, to a
* project `parse`. A static constant's callee is the value it holds (`static
* const autoDispose = AutoDisposeFutureProviderBuilder();`). The member is
* found as a chain's static link finds it (dartStaticMember). Undefined when
* the receiver names no Dart type of the project.
*/
function matchDartTypeMemberCall(
typeName: string,
member: string,
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null | undefined {
if (!context.getNodesByName(typeName).some((n) => n.language === 'dart' && DART_TYPE_KINDS.has(n.kind))) return undefined;
const own = dartStaticMember(typeName, member, ref, context);
return own && own.kind !== 'enum_member'
? { original: ref, targetNodeId: own.id, confidence: 0.85, resolvedBy: 'qualified-name' } : null;
}
/**
* How a bare Rust or Go name is written at its call: `path` after `::` (left
* to the path strategies), `chained` after a `.` — with the receiver it is
@@ -6683,6 +6732,11 @@ export function matchByExactName(
!isKotlinNumberBitwise(n, ref) &&
!(solidityBare && !isSolidityMemberInScope(n, ref, context)) &&
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
// A Dart constructor is reached through its class (`Foo.named(…)`,
// `@Foo.named(…)`), never by its name alone: not a bare call, not the
// last link of a chain, not an annotation (riverpod's 210 `@internal`
// annotations went to its providers' `internal` constructors).
!(ref.language === 'dart' && isDartConstructor(n, context)) &&
!(phpSelf && (n.kind !== 'method' || !isPhpMethodInScope(n, ref, phpSelf, context))) &&
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
!(rustBare && !isRustNameInScope(n, ref, context)) &&
@@ -8387,6 +8441,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
DART_LINEAGES.delete(context);
DART_EXTENSION_OWNERS.delete(context);
DART_GETTERS.delete(context);
DART_CONSTRUCTORS.delete(context);
DART_SETTERS.delete(context);
DART_CODE.delete(context);
DART_RETURN_TYPES.delete(context);
@@ -9514,6 +9569,13 @@ export function matchMethodCall(
// receiver type we can try to infer from its local declaration.
const inferableReceiver = dotMatch || luaColonMatch || rDollarMatch;
// A Dart call through a type's name is to that type's own member, or to
// nothing in the project.
if (ref.language === 'dart' && dotMatch) {
const typeMember = nmTimedT('mc-darttype', ref, () => matchDartTypeMemberCall(objectOrClass!, methodName!, ref, context));
if (typeMember !== undefined) return typeMember;
}
// Infer the receiver's type from its local declaration/initializer in the
// enclosing scope, then resolve the method on that type (#1108). C++ keeps its
// dedicated inferrer (header scan + `auto`); every other language uses the
@@ -9739,24 +9801,6 @@ export function matchMethodCall(
if (classNode.language !== ref.language) continue;
const nodesInFile = context.getNodesInFile(classNode.filePath);
// Dart: a call through a type's static constant invokes the value the
// constant holds. riverpod's `FutureProvider.autoDispose(…)` calls
// `static const autoDispose = AutoDisposeFutureProviderBuilder();`,
// which the lookups below gave to another builder's `autoDispose`
// method. A Dart type can't also declare a method of that name, and its
// static members are not inherited, so its own constant is the callee.
if (ref.language === 'dart') {
const holder = nodesInFile.find((n) =>
n.kind === 'constant' && n.name === methodName && n.qualifiedName === `${classNode.qualifiedName}::${methodName}`);
if (holder) {
return {
original: ref,
targetNodeId: holder.id,
confidence: 0.85,
resolvedBy: 'qualified-name',
};
}
}
const methodNode = nodesInFile.find(
(n) =>
n.kind === 'method' &&
@@ -11480,6 +11524,7 @@ export function matchFuzzy(
!(pythonShape && !fitsPythonCallShape(n, pythonShape, ref, context)) &&
!(javaBare && n.kind === 'method' && !isJavaMethodInScope(n, ref, context)) &&
!(dartBare && isDartMember(n) && !isDartMethodInScope(n, ref, context)) &&
!(ref.language === 'dart' && isDartConstructor(n, context)) &&
!(kotlinCall && !isKotlinTopLevelVisible(n, ref, context)) &&
!(kotlinBare && !isKotlinMemberReachable(n, ref, context)) &&
!isKotlinNumberBitwise(n, ref) &&