Repository navigation
Missing dependency providers silently pass build_runner and cause late compilation errors #7
Description
Describe the bug
When a class or provider requests a dependency for which no provider is defined (or it is a third-party class that is not annotated with @provide and not declared in any module), the build_runner build step completes with a successful exit code (0).
However, the generator (InjectorGenerator) silently fails to generate the implementation helper method _create[Type]() for the missing dependency, while still generating calls to _create[Type]() inside dependent constructors.
This shifts the error detection from a compile-time build step check to a late Dart compilation error during run or compile phases.
To Reproduce
- Define a class
Athat depends onB(which has no@provideannotation and is not provided in any module):@provide class A { final B b; A(this.b); } class B {} @Injector() abstract class MissingInjector { static final create = g.MissingInjector$Injector.create; @provide A get a; }
- Run the build runner:
dart run build_runner build
- Observe that the build completes successfully:
Built with build_runner/aot in 43s; wrote 2 outputs. - Run or compile the application:
dart run lib/main.dart
- Observe the Dart compilation error:
example/missing_demo/lib/missing_demo.inject.dart:17:29: Error: The method '_createB' isn't defined for the type 'MissingInjector$Injector'. - 'MissingInjector$Injector' is from 'package:missing_demo/missing_demo.inject.dart'. Try correcting the name to the name of an existing method, or defining a method named '_createB'. _i1.A _createA() => _i1.A(_createB()); ^^^^^^^^
Emitted Code Analysis
The generator silently generates a call to _createB() but does not emit the definition for _createB() inside missing_demo.inject.dart:
class MissingInjector$Injector extends _i1.MissingInjector {
MissingInjector$Injector();
static Future<_i1.MissingInjector> create() async {
final injector = MissingInjector$Injector();
return injector;
}
@override
_i1.A get a => _createA();
// _createB() is called here but is never defined in this class!
_i1.A _createA() => _i1.A(_createB());
}Context / Code Reference
In package/inject_compile_generator/lib/src/graph/injector_graph_resolver.dart, the resolveKey function silently returns if the dependency is not provided by a module and the target class does not contain @provide metadata:
Future<void> resolveKey(
LookupKey key, {
required SymbolPath requestedBy,
}) async {
// Modules take precedence.
if (providersByModules.containsKey(key) || injectables.containsKey(key)) {
return;
}
if (!key.root.isGlobal) {
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);
}
return;
}
}
// Exits silently if it fails to resolve
}
}Suggested Solution
- Maintain a map of all requested keys and the files/classes that requested them.
- Catch summary read exceptions (such as
FileSystemExceptionorAssetNotFoundExceptionwhen looking up third-party classes that have no.inject.summaryfiles). - Validate that every requested dependency exists in
providersByModulesorinjectablesonce resolution completes. - Throw a descriptive
StateErrorlisting all missing dependencies and their requester sources.
Future<InjectorGraph> resolve() async {
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) {
await _collectModules(modulePath, allModules, providersByModules);
}
// 2. Resolve all dependencies (both from injector and modules).
Future<void> resolveKey(
LookupKey key, {
required SymbolPath requestedBy,
}) async {
+ recordRequest(key, requestedBy);
+
// Modules take precedence.
if (providersByModules.containsKey(key) || injectables.containsKey(key)) {
return;
}
if (!key.root.isGlobal) {
- 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);
- }
- return;
- }
- }
+ try {
+ 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);
+ }
+ return;
+ }
+ }
+ } catch (_) {
+ // Treat unreadable summaries/external dependencies as non-injectable
+ }
}
}
// Resolve dependencies for all providers in all modules.
for (final module in allModules) {
for (final provider in module.providers) {
for (final dep in provider.dependencies) {
await resolveKey(dep.lookupKey, requestedBy: module.clazz);
}
}
}
// Resolve dependencies for all injector providers.
for (final provider in _injectorSummary.providers) {
await resolveKey(
provider.resultType.lookupKey,
requestedBy: _injectorSummary.clazz,
);
}
+ // 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.
Activity
fix: validate missing dependencies and refactor SummaryReader to null…