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
A that depends on B (which has no @provide annotation 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:
- 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
FileSystemException or AssetNotFoundException when looking up third-party classes that have no .inject.summary files).
- Validate that every requested dependency exists in
providersByModules or injectables once resolution completes.
- Throw a descriptive
StateError listing 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.
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
@provideand not declared in any module), thebuild_runnerbuild 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
Athat depends onB(which has no@provideannotation and is not provided in any module):Emitted Code Analysis
The generator silently generates a call to
_createB()but does not emit the definition for_createB()insidemissing_demo.inject.dart:Context / Code Reference
In
package/inject_compile_generator/lib/src/graph/injector_graph_resolver.dart, theresolveKeyfunction silently returns if the dependency is not provided by a module and the target class does not contain@providemetadata:Suggested Solution
FileSystemExceptionorAssetNotFoundExceptionwhen looking up third-party classes that have no.inject.summaryfiles).providersByModulesorinjectablesonce resolution completes.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.