diff --git a/packages/container/container/lib/src/container.dart b/packages/container/container/lib/src/container.dart index f7e1782..43333fd 100644 --- a/packages/container/container/lib/src/container.dart +++ b/packages/container/container/lib/src/container.dart @@ -8,6 +8,7 @@ */ import 'dart:async'; +import 'dart:mirrors' show AbstractClassInstantiationError; import 'attributes.dart'; import 'exception.dart'; import 'reflector.dart'; @@ -344,10 +345,29 @@ class Container { } } - instance = reflectedType.newInstance( - isDefault(constructor.name) ? '' : constructor.name, - positional, - named, []).reflectee; + try { + dynamic tempInstance; + try { + tempInstance = reflectedType.newInstance( + isDefault(constructor.name) ? '' : constructor.name, + positional, + named, []).reflectee; + } on AbstractClassInstantiationError { + throw BindingResolutionException( + 'Cannot instantiate abstract class ${reflectedType.name}'); + } + + tempInstance = _applyExtenders(t2, tempInstance); + _fireResolvingCallbacks(tempInstance); + _fireAfterResolvingCallbacks(tempInstance); + return tempInstance as T; + } catch (e) { + if (e is AbstractClassInstantiationError) { + throw BindingResolutionException( + 'Cannot instantiate abstract class ${reflectedType.name}'); + } + rethrow; + } } } finally { _buildStack.add(t2); // Add it back diff --git a/packages/container/container/lib/src/mirrors/reflector.dart b/packages/container/container/lib/src/mirrors/reflector.dart index 1c53546..09b1368 100644 --- a/packages/container/container/lib/src/mirrors/reflector.dart +++ b/packages/container/container/lib/src/mirrors/reflector.dart @@ -564,8 +564,13 @@ class _ReflectedClassMirror extends ReflectedClass { ReflectedInstance newInstance( String constructorName, List positionalArguments, [Map? namedArguments, List? typeArguments]) { - return _ReflectedInstanceMirror( - mirror.newInstance(Symbol(constructorName), positionalArguments)); + try { + return _ReflectedInstanceMirror( + mirror.newInstance(Symbol(constructorName), positionalArguments)); + } on dart.AbstractClassInstantiationError { + throw BindingResolutionException( + 'Cannot instantiate abstract class ${mirror.simpleName}'); + } } /// Checks if this [_ReflectedClassMirror] is equal to another object. diff --git a/packages/container/container/test/constructor_injection_test.dart b/packages/container/container/test/constructor_injection_test.dart new file mode 100644 index 0000000..2d0e6b9 --- /dev/null +++ b/packages/container/container/test/constructor_injection_test.dart @@ -0,0 +1,287 @@ +import 'package:test/test.dart'; +import 'package:platform_container/container.dart'; +import 'package:platform_container/mirrors.dart'; + +// Test interfaces +abstract class Logger { + void log(String message); +} + +abstract class Config { + String get value; +} + +abstract class Database { + void connect(); +} + +// Test implementations +class ConsoleLogger implements Logger { + @override + void log(String message) {} +} + +class FileConfig implements Config { + @override + String get value => 'test'; +} + +class SqlDatabase implements Database { + final Logger logger; + final Config config; + + SqlDatabase(this.logger, this.config); + + @override + void connect() {} +} + +// Tracking implementations for dependency order test +class TrackingLogger implements Logger { + final List order; + static int instanceCount = 0; + + TrackingLogger(this.order) { + instanceCount++; + order.add('logger'); + } + + @override + void log(String message) {} +} + +class TrackingConfig implements Config { + final List order; + static int instanceCount = 0; + + TrackingConfig(this.order) { + instanceCount++; + order.add('config'); + } + + @override + String get value => 'test'; +} + +class TrackingDatabase implements Database { + final Logger logger; + final Config config; + final List order; + static int instanceCount = 0; + + TrackingDatabase(this.logger, this.config, this.order) { + instanceCount++; + order.add('database'); + } + + @override + void connect() {} +} + +// Custom config for multiple instances test +class CustomConfig implements Config { + final String _value; + + CustomConfig(this._value); + + @override + String get value => _value; +} + +// Test service with multiple dependencies +class UserService { + final Logger logger; + final Database db; + final Config config; + + UserService(this.logger, this.db, this.config); +} + +// Test service with optional dependencies +class OptionalDepsService { + final Logger logger; + final Config? config; + + OptionalDepsService(this.logger, [this.config]); +} + +// Test service with named parameters +class NamedParamsService { + final Logger logger; + final Config? config; + + NamedParamsService(this.logger, {this.config}); +} + +// Test service with mixed parameters +class MixedParamsService { + final Logger logger; + final Database db; + final Config? config; + final String? name; + + MixedParamsService(this.logger, this.db, {this.config, this.name}); +} + +// Test service with nested dependencies +class NestedService { + final UserService userService; + final Logger logger; + + NestedService(this.userService, this.logger); +} + +void main() { + group('Constructor Injection Tests', () { + late Container container; + + setUp(() { + container = Container(MirrorsReflector()); + container.bind(Logger).to(ConsoleLogger); + container.bind(Config).to(FileConfig); + container.bind(Database).to(SqlDatabase); + + // Reset tracking counters + TrackingLogger.instanceCount = 0; + TrackingConfig.instanceCount = 0; + TrackingDatabase.instanceCount = 0; + }); + + test('injects basic dependencies', () { + var service = container.make(); + + expect(service, isNotNull); + expect(service.logger, isA()); + expect(service.config, isA()); + expect(service.db, isA()); + }); + + test('injects nested dependencies', () { + var service = container.make(); + + expect(service, isNotNull); + expect(service.logger, isA()); + expect(service.userService, isA()); + expect(service.userService.logger, isA()); + expect(service.userService.config, isA()); + expect(service.userService.db, isA()); + }); + + test('handles optional dependencies', () { + container = Container(MirrorsReflector()); + container.bind(Logger).to(ConsoleLogger); + + var service = container.make(); + expect(service, isNotNull); + expect(service.logger, isA()); + expect(service.config, isNull); + + // Now bind config and verify it gets injected + container.bind(Config).to(FileConfig); + service = container.make(); + expect(service.config, isA()); + }); + + test('handles named parameters', () { + container = Container(MirrorsReflector()); + container.bind(Logger).to(ConsoleLogger); + + var service = container.make(); + expect(service, isNotNull); + expect(service.logger, isA()); + expect(service.config, isNull); + + // Now bind config and verify it gets injected + container.bind(Config).to(FileConfig); + service = container.make(); + expect(service.config, isA()); + }); + + test('handles mixed parameters', () { + var service = container.makeWith({'name': 'test'}); + + expect(service, isNotNull); + expect(service.logger, isA()); + expect(service.db, isA()); + expect(service.config, isNull); + expect(service.name, equals('test')); + + // Now bind config and verify it gets injected + container.bind(Config).to(FileConfig); + service = container.make(); + expect(service.config, isA()); + }); + + test('handles parameter overrides', () { + var customConfig = FileConfig(); + var service = container.makeWith({'config': customConfig}); + + expect(service, isNotNull); + expect(service.logger, isA()); + expect(service.config, same(customConfig)); + }); + + test('resolves dependencies in correct order', () { + var order = []; + + container = Container(MirrorsReflector()); + container.registerSingleton(TrackingLogger(order)); + container.registerSingleton(TrackingConfig(order)); + container.registerSingleton(TrackingDatabase( + container.make(), container.make(), order)); + + // Create service and verify order + container.make(); + + expect(order, equals(['logger', 'config', 'database'])); + expect(TrackingLogger.instanceCount, equals(1), + reason: 'Logger should be instantiated once'); + expect(TrackingConfig.instanceCount, equals(1), + reason: 'Config should be instantiated once'); + expect(TrackingDatabase.instanceCount, equals(1), + reason: 'Database should be instantiated once'); + }); + + test('throws on missing required dependency', () { + container = + Container(MirrorsReflector()); // Fresh container with no bindings + + expect(() => container.make(), + throwsA(isA())); + }); + + test('handles multiple instances with different configurations', () { + var container1 = Container(MirrorsReflector()); + var container2 = Container(MirrorsReflector()); + + container1.bind(Logger).to(ConsoleLogger); + container2.bind(Logger).to(ConsoleLogger); + container1.bind(Database).to(SqlDatabase); + container2.bind(Database).to(SqlDatabase); + + container1.registerSingleton(CustomConfig('config1')); + container2.registerSingleton(CustomConfig('config2')); + + var service1 = container1.make(); + var service2 = container2.make(); + + expect(service1.config.value, equals('config1')); + expect(service2.config.value, equals('config2')); + }); + + test('throws when trying to instantiate abstract class', () { + container = Container(MirrorsReflector()); + + expect(() => container.make(), + throwsA(isA())); + }); + + test('throws when dependency is abstract', () { + container = Container(MirrorsReflector()); + container.bind(Database).to(SqlDatabase); + + expect(() => container.make(), + throwsA(isA())); + }); + }); +}