Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
277 changes: 277 additions & 0 deletions __tests__/dart-const-constructors.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
86 changes: 86 additions & 0 deletions __tests__/fixtures/kernel-parity/TortureConstCtors.dart
Original file line number Diff line number Diff line change
@@ -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}) {}
Loading