// // Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com) // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import 'package:analyzer/dart/element/element.dart'; import 'package:source_gen/source_gen.dart'; import 'bind_parameters_spec.dart'; import 'metadata_utils.dart'; enum BindingType { instance, provide; } /// --------------------------------------------------------------------------- /// BindSpec -- describes a binding specification generated for a dependency. /// /// ENGLISH /// Represents all the data necessary to generate a DI binding for a single /// method in a module class. Each BindSpec corresponds to one public method /// and contains information about its type, provider method, lifecycle (singleton), /// parameters (with their annotations), binding strategy (instance/provide), /// asynchronous mode, and named keys. It is responsible for generating the /// correct Dart code to register this binding with the DI container, in both /// sync and async cases, with and without named or runtime arguments. /// /// RUSSIAN /// Описывает параметры для создания одного биндинга зависимости (binding spec). /// Каждый биндинг соответствует одному публичному методу класса-модуля и /// содержит всю информацию для генерации кода регистрации этого биндинга в /// DI-контейнере: тип возвращаемой зависимости, имя метода, параметры, аннотации /// (@singleton, @named, @instance, @provide), асинхронность, признак runtime /// аргументов и др. Генерирует правильный Dart-код для регистрации биндера. /// --------------------------------------------------------------------------- class BindSpec { /// The type this binding provides (e.g. SomeService) /// Тип, который предоставляет биндинг (например, SomeService) final String returnType; /// Method name that implements the binding /// Имя метода, который реализует биндинг final String methodName; /// Optional name for named dependency (from @named) /// Необязательное имя, для именованной зависимости (используется с @named) final String? named; /// Whether the dependency is a singleton (@singleton annotation) /// Является ли зависимость синглтоном (имеется ли аннотация @singleton) final bool isSingleton; /// List of method parameters to inject dependencies with /// Список параметров, которые требуются методу для внедрения зависимостей final List parameters; /// Binding type: 'instance' or 'provide' (@instance or @provide) final BindingType bindingType; // 'instance' | 'provide' /// True if the method is asynchronous and uses instance binding (Future) final bool isAsyncInstance; /// True if the method is asynchronous and uses provide binding (Future) final bool isAsyncProvide; /// True if the binding method accepts runtime "params" argument (@params) final bool hasParams; BindSpec({ required this.returnType, required this.methodName, required this.isSingleton, required this.parameters, this.named, required this.bindingType, required this.isAsyncInstance, required this.isAsyncProvide, required this.hasParams, }); /// ------------------------------------------------------------------------- /// generateBind /// /// ENGLISH /// Generates a line of Dart code registering the binding with the DI framework. /// Produces something like: /// bind().toProvide(() => method(args)).withName('name').singleton(); /// Indent parameter allows formatted multiline output. /// /// RUSSIAN /// Формирует dart-код для биндинга, например: /// bind().toProvide(() => method(args)).withName('name').singleton(); /// Параметр [indent] задаёт отступ для красивого форматирования кода. /// ------------------------------------------------------------------------- String generateBind(int indent) { final indentStr = ' ' * indent; final provide = _generateProvideClause(indent); final postfix = _generatePostfix(); return '$indentStr' 'bind<$returnType>()' '$provide' '$postfix;'; } // Internal method: decides how the provide clause should be generated by param kind. String _generateProvideClause(int indent) { if (hasParams) return _generateWithParamsProvideClause(indent); return _generatePlainProvideClause(indent); } /// EN / RU: Supports runtime parameters (@params). String _generateWithParamsProvideClause(int indent) { // Safe variable name for parameters. const paramVar = 'args'; final fnArgs = parameters.map((p) => p.generateArg(paramVar)).join(', '); final multiLine = fnArgs.length > 60 || fnArgs.contains('\n'); switch (bindingType) { case BindingType.instance: throw StateError( 'Internal error: _generateWithParamsProvideClause called for @instance binding with @params.'); //return isAsyncInstance // ? '.toInstanceAsync(($fnArgs) => $methodName($fnArgs))' // : '.toInstance(($fnArgs) => $methodName($fnArgs))'; case BindingType.provide: if (isAsyncProvide) { return multiLine ? '.toProvideAsyncWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))' : '.toProvideAsyncWithParams(($paramVar) => $methodName($fnArgs))'; } else { return multiLine ? '.toProvideWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))' : '.toProvideWithParams(($paramVar) => $methodName($fnArgs))'; } } } /// EN / RU: Supports only injected dependencies, not runtime (@params). String _generatePlainProvideClause(int indent) { final argsStr = parameters.map((p) => p.generateArg()).join(', '); final multiLine = argsStr.length > 60 || argsStr.contains('\n'); switch (bindingType) { case BindingType.instance: return isAsyncInstance ? '.toInstanceAsync($methodName($argsStr))' : '.toInstance($methodName($argsStr))'; case BindingType.provide: if (isAsyncProvide) { return multiLine ? '.toProvideAsync(\n${' ' * (indent + 2)}() => $methodName($argsStr))' : '.toProvideAsync(() => $methodName($argsStr))'; } else { return multiLine ? '.toProvide(\n${' ' * (indent + 2)}() => $methodName($argsStr))' : '.toProvide(() => $methodName($argsStr))'; } } } /// EN / RU: Adds .withName and .singleton if needed. String _generatePostfix() { final namePart = named != null ? ".withName('$named')" : ''; final singletonPart = isSingleton ? '.singleton()' : ''; return '$namePart$singletonPart'; } /// ------------------------------------------------------------------------- /// fromMethod /// /// ENGLISH /// Creates a BindSpec from a module class method by analyzing its return type, /// annotations, list of parameters (with their own annotations), and async-ness. /// Throws if a method does not have the required @instance() or @provide(). /// /// RUSSIAN /// Создаёт спецификацию биндинга (BindSpec) из метода класса-модуля, анализируя /// возвращаемый тип, аннотации, параметры (и их аннотации), а также факт /// асинхронности. Если нет @instance или @provide — кидает ошибку. /// ------------------------------------------------------------------------- static BindSpec fromMethod(MethodElement method) { var returnType = method.returnType.getDisplayString(); final methodName = method.displayName; // Check for @singleton annotation. final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton'); // Get @named value if present. final named = MetadataUtils.getNamedValue(method.metadata); // Parse each method parameter. final params = []; bool hasParams = false; for (final p in method.parameters) { final typeStr = p.type.getDisplayString(); final paramNamed = MetadataUtils.getNamedValue(p.metadata); final isParams = MetadataUtils.anyMeta(p.metadata, 'params'); if (isParams) hasParams = true; params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams)); } // Determine bindingType: @instance or @provide. final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance'); final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide'); if (!hasInstance && !hasProvide) { throw InvalidGenerationSourceError( 'Method $methodName must be marked with @instance() or @provide().', element: method, ); } final bindingType = hasInstance ? BindingType.instance : BindingType.provide; // PROHIBIT @params with @instance bindings! if (bindingType == BindingType.instance && hasParams) { throw InvalidGenerationSourceError( '@params() (runtime arguments) cannot be used together with @instance() on method $methodName. ' 'Use @provide() instead if you want runtime arguments.', element: method, ); } // -- Extract inner type for Future and set async flags. bool isAsyncInstance = false; bool isAsyncProvide = false; final futureInnerType = _extractFutureInnerType(returnType); if (futureInnerType != null) { returnType = futureInnerType; if (bindingType == BindingType.instance) isAsyncInstance = true; if (bindingType == BindingType.provide) isAsyncProvide = true; } return BindSpec( returnType: returnType, methodName: methodName, isSingleton: isSingleton, named: named, parameters: params, bindingType: bindingType, isAsyncInstance: isAsyncInstance, isAsyncProvide: isAsyncProvide, hasParams: hasParams, ); } /// EN / RU: Extracts inner type from Future, returns e.g. "T" or null. static String? _extractFutureInnerType(String typeName) { final match = RegExp(r'^Future<(.+)>$').firstMatch(typeName); return match?.group(1)?.trim(); } }