From 4b2da670baa390d9390b60d8ce675a5038e4c7a9 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Jul 2026 17:30:42 +0000 Subject: [PATCH] fix: validate missing dependencies and refactor SummaryReader to nullable Implement compile-time validation for missing dependency providers to fail the build runner step early with a descriptive error message. Also refactors the `SummaryReader` API to return a nullable `LibrarySummary?` instead of throwing a `FileSystemException` when a summary does not exist, allowing `resolveKey` to use a clean null-check instead of control-flow exception try/catch blocks. - Tracks all requested dependencies and throws `StateError` if any lack a provider. - Refactors `SummaryReader` and mock/fake test implementations to return nullable summaries. - Adds a unit test for missing dependency validation. Closes #7 --- package/inject_compile_generator/CHANGELOG.md | 4 +- .../src/graph/injector_graph_resolver.dart | 46 +++++++++++++-- .../lib/src/graph/summary_reader.dart | 12 ++-- .../graph/injector_graph_resolver_test.dart | 57 ++++++++++++++++--- 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/package/inject_compile_generator/CHANGELOG.md b/package/inject_compile_generator/CHANGELOG.md index 0685e94..e656d37 100644 --- a/package/inject_compile_generator/CHANGELOG.md +++ b/package/inject_compile_generator/CHANGELOG.md @@ -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 diff --git a/package/inject_compile_generator/lib/src/graph/injector_graph_resolver.dart b/package/inject_compile_generator/lib/src/graph/injector_graph_resolver.dart index 21e5db6..70c786f 100644 --- a/package/inject_compile_generator/lib/src/graph/injector_graph_resolver.dart +++ b/package/inject_compile_generator/lib/src/graph/injector_graph_resolver.dart @@ -21,6 +21,11 @@ class InjectorGraphResolver { final allModules = []; final providersByModules = {}; final injectables = {}; + final requestedByMap = >{}; + + 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) { @@ -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; @@ -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; } } } @@ -70,6 +79,27 @@ class InjectorGraphResolver { ); } + // Check for missing dependency providers. + final missingDependencies = >{}; + 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 = {}; injectables.forEach((key, summary) { @@ -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, ); diff --git a/package/inject_compile_generator/lib/src/graph/summary_reader.dart b/package/inject_compile_generator/lib/src/graph/summary_reader.dart index 44ff857..4c59f24 100644 --- a/package/inject_compile_generator/lib/src/graph/summary_reader.dart +++ b/package/inject_compile_generator/lib/src/graph/summary_reader.dart @@ -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 read(AssetId assetId); + /// The [LibrarySummary] read from [assetId], or null if the summary does not exist. + Future read(AssetId assetId); } /// An implementation of [SummaryReader] that uses an [AssetReader] to load summaries. @@ -17,7 +16,7 @@ class AssetSummaryReader implements SummaryReader { AssetSummaryReader(this._reader); @override - Future read(AssetId assetId) async { + Future read(AssetId assetId) async { if (_cache.containsKey(assetId)) { return _cache[assetId]!; } @@ -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); diff --git a/package/inject_compile_generator/test/graph/injector_graph_resolver_test.dart b/package/inject_compile_generator/test/graph/injector_graph_resolver_test.dart index d6586e2..887e4e6 100644 --- a/package/inject_compile_generator/test/graph/injector_graph_resolver_test.dart +++ b/package/inject_compile_generator/test/graph/injector_graph_resolver_test.dart @@ -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'; @@ -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); + }); }); } @@ -152,11 +199,7 @@ class FakeSummaryReader implements SummaryReader { } @override - Future read(AssetId assetId) async { - final summary = _summaries[assetId]; - if (summary == null) { - throw FileSystemException(assetId.uri.toString(), 'Summary not found'); - } - return summary; + Future read(AssetId assetId) async { + return _summaries[assetId]; } }