mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 06:22:38 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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}) {}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
Reference in New Issue
Block a user