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
4 changes: 3 additions & 1 deletion package/inject_compile_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## 0.9.2
- Throw StateError on circular dependency detection instead of logging a warning to a detached logger.
- Throw StateError on circular dependency cycles instead of logging a warning.
- Add compile-time validation for missing dependency providers to fail the build runner step early with a descriptive error message.
- Refactor `SummaryReader` API to return nullable summaries instead of throwing `FileSystemException`, avoiding control-flow exceptions.

## 0.9.1
- Add example/example.md file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ class InjectorGraphResolver {
final allModules = <ModuleSummary>[];
final providersByModules = <LookupKey, DependencyProvidedByModule>{};
final injectables = <LookupKey, InjectableSummary>{};
final requestedByMap = <LookupKey, Set<SymbolPath>>{};

void recordRequest(LookupKey key, SymbolPath requestedBy) {
requestedByMap.putIfAbsent(key, () => {}).add(requestedBy);
}

// 1. Collect all modules starting from the injector.
for (final modulePath in _injectorSummary.modules) {
Expand All @@ -32,6 +37,8 @@ class InjectorGraphResolver {
LookupKey key, {
required SymbolPath requestedBy,
}) async {
recordRequest(key, requestedBy);

// Modules take precedence.
if (providersByModules.containsKey(key) || injectables.containsKey(key)) {
return;
Expand All @@ -41,13 +48,15 @@ class InjectorGraphResolver {
final summary = await _reader.read(
AssetId(key.root.package!, key.root.path!),
);
for (final injectable in summary.injectables) {
if (injectable.clazz == key.root) {
injectables[key] = injectable;
for (final dep in injectable.constructor.dependencies) {
await resolveKey(dep.lookupKey, requestedBy: injectable.clazz);
if (summary != null) {
for (final injectable in summary.injectables) {
if (injectable.clazz == key.root) {
injectables[key] = injectable;
for (final dep in injectable.constructor.dependencies) {
await resolveKey(dep.lookupKey, requestedBy: injectable.clazz);
}
return;
}
return;
}
}
}
Expand All @@ -70,6 +79,27 @@ class InjectorGraphResolver {
);
}

// Check for missing dependency providers.
final missingDependencies = <LookupKey, Set<SymbolPath>>{};
for (final key in requestedByMap.keys) {
if (!providersByModules.containsKey(key) &&
!injectables.containsKey(key)) {
missingDependencies[key] = requestedByMap[key]!;
}
}

if (missingDependencies.isNotEmpty) {
final message = StringBuffer('Missing dependency providers:\n');
missingDependencies.forEach((key, requesters) {
message.writeln(' - ${key.toPrettyString()}');
message.writeln(' requested by:');
for (final req in requesters) {
message.writeln(' * ${req.symbol} (${req.toAbsoluteUri()})');
}
});
throw StateError(message.toString());
}

// 3. Construct the merged dependencies map.
final mergedDependencies = <LookupKey, ResolvedDependency>{};
injectables.forEach((key, summary) {
Expand Down Expand Up @@ -106,6 +136,10 @@ class InjectorGraphResolver {
final summary = await _reader.read(
AssetId(modulePath.package!, modulePath.path!),
);
if (summary == null) {
_logger.severe('Module summary not found for $modulePath.');
return;
}
final module = summary.modules.firstWhereOrNull(
(m) => m.clazz == modulePath,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import 'dart:convert';
import 'dart:io';
import 'package:build/build.dart';
import '../models/summary.dart';

/// A reader that retrieves and caches [LibrarySummary] instances.
abstract class SummaryReader {
/// The [LibrarySummary] read from [assetId].
Future<LibrarySummary> read(AssetId assetId);
/// The [LibrarySummary] read from [assetId], or null if the summary does not exist.
Future<LibrarySummary?> read(AssetId assetId);
}

/// An implementation of [SummaryReader] that uses an [AssetReader] to load summaries.
Expand All @@ -17,7 +16,7 @@ class AssetSummaryReader implements SummaryReader {
AssetSummaryReader(this._reader);

@override
Future<LibrarySummary> read(AssetId assetId) async {
Future<LibrarySummary?> read(AssetId assetId) async {
if (_cache.containsKey(assetId)) {
return _cache[assetId]!;
}
Expand All @@ -27,10 +26,7 @@ class AssetSummaryReader implements SummaryReader {
final summaryId = assetId.changeExtension('.inject.summary');

if (!await _reader.canRead(summaryId)) {
throw FileSystemException(
summaryId.uri.toString(),
'Could not read summary file',
);
return null;
}

final json = await _reader.readAsString(summaryId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:build/build.dart';
import 'package:inject_compile_generator/src/graph/injector_graph_resolver.dart';
import 'package:inject_compile_generator/src/graph/summary_reader.dart';
Expand Down Expand Up @@ -141,6 +140,54 @@ void main() {
final resolver = InjectorGraphResolver(reader, injectorSummary);
expect(resolver.resolve(), throwsStateError);
});

test('fails on missing dependencies', () async {
final aPath = SymbolPath(package: 'pkg', path: 'lib/a.dart', symbol: 'A');
final bPath = SymbolPath(package: 'pkg', path: 'lib/b.dart', symbol: 'B');
final injectorPath = SymbolPath(
package: 'pkg',
path: 'lib/main.dart',
symbol: 'MainInjector',
);

reader.addSummary(
AssetId('pkg', 'lib/a.dart'),
LibrarySummary(
assetUri: 'package:pkg/a.dart',
injectables: [
InjectableSummary(
clazz: aPath,
constructor: ProviderSummary(
name: '',
kind: ProviderKind.constructor,
resultType: InjectedType(lookupKey: LookupKey(root: aPath)),
isSingleton: false,
isAsynchronous: false,
dependencies: [InjectedType(lookupKey: LookupKey(root: bPath))],
),
),
],
),
);

final injectorSummary = InjectorSummary(
clazz: injectorPath,
modules: [],
providers: [
ProviderSummary(
name: 'getA',
kind: ProviderKind.method,
resultType: InjectedType(lookupKey: LookupKey(root: aPath)),
isSingleton: false,
isAsynchronous: false,
dependencies: [],
),
],
);

final resolver = InjectorGraphResolver(reader, injectorSummary);
expect(resolver.resolve(), throwsStateError);
});
});
}

Expand All @@ -152,11 +199,7 @@ class FakeSummaryReader implements SummaryReader {
}

@override
Future<LibrarySummary> read(AssetId assetId) async {
final summary = _summaries[assetId];
if (summary == null) {
throw FileSystemException(assetId.uri.toString(), 'Summary not found');
}
return summary;
Future<LibrarySummary?> read(AssetId assetId) async {
return _summaries[assetId];
}
}
Loading