diff --git a/cherrypick_generator/lib/module_generator.dart b/cherrypick_generator/lib/module_generator.dart index 7fc1957..0f766e5 100644 --- a/cherrypick_generator/lib/module_generator.dart +++ b/cherrypick_generator/lib/module_generator.dart @@ -18,49 +18,76 @@ import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann; import 'src/generated_class.dart'; +/// --------------------------------------------------------------------------- +/// ModuleGenerator for code generation of dependency-injected modules. /// +/// ENGLISH +/// This generator scans for Dart classes annotated with `@module()` and +/// automatically generates boilerplate code for dependency injection +/// (DI) based on the public methods in those classes. Each method can be +/// annotated to describe how an object should be provided to the DI container. +/// The generated code registers those methods as bindings. This automates the +/// creation of factories, singletons, and named instances, reducing repetitive +/// manual code. +/// +/// RUSSIAN /// Генератор зависимостей для DI-контейнера на основе аннотаций. -/// /// Данный генератор автоматически создаёт код для внедрения зависимостей (DI) /// на основе аннотаций в вашем исходном коде. Когда вы отмечаете класс /// аннотацией `@module()`, этот генератор обработает все его публичные методы /// и автоматически сгенерирует класс с биндингами (регистрациями зависимостей) /// для DI-контейнера. Это избавляет от написания однообразного шаблонного кода. -/// +/// --------------------------------------------------------------------------- + class ModuleGenerator extends GeneratorForAnnotation { - /// Генерирует исходный код для класса-модуля с аннотацией `@module()`. + /// ------------------------------------------------------------------------- + /// ENGLISH + /// Generates the Dart source for a class marked with the `@module()` annotation. + /// - [element]: the original Dart class element. + /// - [annotation]: the annotation parameters (not usually used here). + /// - [buildStep]: the current build step info. /// + /// RUSSIAN + /// Генерирует исходный код для класса-модуля с аннотацией `@module()`. /// [element] — исходный класс, помеченный аннотацией. /// [annotation] — значения параметров аннотации. /// [buildStep] — информация о текущем шаге генерации. + /// ------------------------------------------------------------------------- @override String generateForAnnotatedElement( Element element, ConstantReader annotation, BuildStep buildStep, ) { - // Генератор обрабатывает только классы (остальное — ошибка) + // Only classes are supported for @module() annotation + // Обрабатываются только классы (другие элементы — ошибка) if (element is! ClassElement) { throw InvalidGenerationSourceError( - '@module() может быть применён только к классам.', + '@module() can only be applied to classes. / @module() может быть применён только к классам.', element: element, ); } final classElement = element; + // Build a representation of the generated bindings based on class methods / // Создаёт объект, описывающий, какие биндинги нужно сгенерировать на основании методов класса final generatedClass = GeneratedClass.fromClassElement(classElement); - // Генерирует итоговый Dart-код + // Generate the resulting Dart code / Генерирует итоговый Dart-код return generatedClass.generate(); } } +/// --------------------------------------------------------------------------- +/// ENGLISH +/// Entry point for build_runner. Returns a Builder used to generate code for +/// every file with a @module() annotation. /// +/// RUSSIAN /// Точка входа для генератора build_runner. /// Возвращает Builder, используемый build_runner для генерации кода для всех /// файлов, где встречается @module(). -/// +/// --------------------------------------------------------------------------- Builder moduleBuilder(BuilderOptions options) => PartBuilder([ModuleGenerator()], '.cherrypick.g.dart'); diff --git a/cherrypick_generator/lib/src/bind_parameters_spec.dart b/cherrypick_generator/lib/src/bind_parameters_spec.dart index fa80e02..a936db9 100644 --- a/cherrypick_generator/lib/src/bind_parameters_spec.dart +++ b/cherrypick_generator/lib/src/bind_parameters_spec.dart @@ -11,25 +11,58 @@ // limitations under the License. // +/// ---------------------------------------------------------------------------- +/// BindParameterSpec - describes a single method parameter and how to resolve it. /// -/// Описывает один параметр метода и возможность его разрешения из контейнера. +/// ENGLISH +/// Describes a single parameter for a provider/binding method in the DI system. +/// Stores the parameter type, its optional `@named` key for named resolution, +/// and whether it is a runtime "params" argument. Used to generate code that +/// resolves dependencies from the DI scope: +/// - If the parameter is a dependency type (e.g. SomeDep), emits: +/// currentScope.resolve() +/// - If the parameter is named, emits: +/// currentScope.resolve(named: 'yourName') +/// - If it's a runtime parameter (e.g. via @params()), emits: +/// args /// -/// Например, если метод принимает SomeDep dep, то -/// BindParameterSpec хранит тип SomeDep, а generateArg отдаст строку +/// RUSSIAN +/// Описывает один параметр метода в DI, и его способ разрешения из контейнера. +/// Сохраняет имя типа, дополнительное имя (если параметр аннотирован через @named), +/// и признак runtime-параметра (@params). +/// Для обычной зависимости типа (например, SomeDep) генерирует строку вида: /// currentScope.resolve() -/// +/// Для зависимости с именем: +/// currentScope.resolve(named: 'имя') +/// Для runtime-параметра: +/// args +/// ---------------------------------------------------------------------------- class BindParameterSpec { + /// Type name of the parameter (e.g. SomeService) /// Имя типа параметра (например, SomeService) final String typeName; + /// Optional name for named resolution (from @named) /// Необязательное имя для разрешения по имени (если аннотировано через @named) final String? named; + /// True if this parameter uses @params and should be provided from runtime args + /// Признак, что параметр — runtime (через @params) final bool isParams; BindParameterSpec(this.typeName, this.named, {this.isParams = false}); - /// Генерирует строку для получения зависимости из DI scope (с учётом имени) + /// -------------------------------------------------------------------------- + /// generateArg + /// + /// ENGLISH + /// Generates Dart code for resolving the dependency from the DI scope, + /// considering type, named, and param-argument. + /// + /// RUSSIAN + /// Генерирует строку для получения зависимости из DI scope (с учётом имени + /// и типа параметра или runtime-режима @params). + /// -------------------------------------------------------------------------- String generateArg([String paramsVar = 'args']) { if (isParams) { return paramsVar; diff --git a/cherrypick_generator/lib/src/bind_spec.dart b/cherrypick_generator/lib/src/bind_spec.dart index 2d49848..c3e66bc 100644 --- a/cherrypick_generator/lib/src/bind_spec.dart +++ b/cherrypick_generator/lib/src/bind_spec.dart @@ -17,33 +17,57 @@ import 'package:source_gen/source_gen.dart'; import 'bind_parameters_spec.dart'; import 'metadata_utils.dart'; +/// --------------------------------------------------------------------------- +/// 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 String 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({ @@ -58,10 +82,20 @@ class BindSpec { 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); @@ -72,13 +106,15 @@ class BindSpec { '$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.isParams ? paramVar : p.generateArg(paramVar)) @@ -103,6 +139,7 @@ class BindSpec { } } + /// 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'); @@ -125,24 +162,37 @@ class BindSpec { } } + /// EN / RU: Adds .withName and .singleton if needed. String _generatePostfix() { final namePart = named != null ? ".withName('$named')" : ''; final singletonPart = isSingleton ? '.singleton()' : ''; return '$namePart$singletonPart'; } - /// Создаёт спецификацию биндинга (BindSpec) из метода класса-модуля + /// ------------------------------------------------------------------------- + /// 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; - // Проверим, помечен ли метод аннотацией @singleton + // Check for @singleton annotation. final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton'); - // Получаем имя из @named(), если есть + // 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) { @@ -153,18 +203,18 @@ class BindSpec { params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams)); } - // определяем bindingType + // Determine bindingType: @instance or @provide. final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance'); final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide'); if (!hasInstance && !hasProvide) { throw InvalidGenerationSourceError( - 'Метод $methodName класса-модуля должен быть помечен либо @instance(), либо @provide().', + 'Method $methodName must be marked with @instance() or @provide().', element: method, ); } final bindingType = hasInstance ? 'instance' : 'provide'; - // --- Новый участок: извлекаем внутренний тип из Future<> и выставляем флаги + // -- Extract inner type for Future and set async flags. bool isAsyncInstance = false; bool isAsyncProvide = false; final futureInnerType = _extractFutureInnerType(returnType); @@ -187,6 +237,7 @@ class BindSpec { ); } + /// 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(); diff --git a/cherrypick_generator/lib/src/generated_class.dart b/cherrypick_generator/lib/src/generated_class.dart index 782483c..8d1e68c 100644 --- a/cherrypick_generator/lib/src/generated_class.dart +++ b/cherrypick_generator/lib/src/generated_class.dart @@ -15,17 +15,37 @@ import 'package:analyzer/dart/element/element.dart'; import 'bind_spec.dart'; +/// --------------------------------------------------------------------------- +/// GeneratedClass -- represents the result of processing a single module class. /// -/// Результат обработки одного класса-модуля: имя класса, его биндинги, -/// имя генерируемого класса и т.д. +/// ENGLISH +/// Encapsulates all the information produced from analyzing a DI module class: +/// - The original class name, +/// - Its generated class name (e.g., `$SomeModule`), +/// - The collection of bindings (BindSpec) for all implemented provider methods. /// +/// Also provides code generation functionality, allowing to generate the source +/// code for the derived DI module class, including all binding registrations. +/// +/// RUSSIAN +/// Описывает результат обработки одного класса-модуля DI: +/// - Имя оригинального класса, +/// - Имя генерируемого класса (например, `$SomeModule`), +/// - Список всех бидингов (BindSpec) — по публичным методам модуля. +/// +/// Также содержит функцию генерации исходного кода для этого класса и +/// регистрации всех зависимостей через bind(...). +/// --------------------------------------------------------------------------- class GeneratedClass { + /// The name of the original module class. /// Имя исходного класса-модуля final String className; + /// The name of the generated class (e.g., $SomeModule). /// Имя генерируемого класса (например, $SomeModule) final String generatedClassName; + /// List of all discovered bindings for the class. /// Список всех обнаруженных биндингов final List binds; @@ -35,13 +55,24 @@ class GeneratedClass { this.binds, ); - /// Обрабатывает объект ClassElement (отображение класса в AST) - /// и строит структуру _GeneratedClass для генерации кода. + /// ------------------------------------------------------------------------- + /// fromClassElement + /// + /// ENGLISH + /// Static factory: creates a GeneratedClass from a Dart ClassElement (AST representation). + /// Discovers all non-abstract methods, builds BindSpec for each, and computes the + /// generated class name by prefixing `$`. + /// + /// RUSSIAN + /// Строит объект класса по элементу AST (ClassElement): имя класса, + /// сгенерированное имя, список BindSpec по всем не абстрактным методам. + /// Имя ген-класса строится с префиксом `$`. + /// ------------------------------------------------------------------------- static GeneratedClass fromClassElement(ClassElement element) { final className = element.displayName; - // Имя с префиксом $ (стандартная практика для ген-кода) + // Generated class name with '$' prefix (standard for generated Dart code). final generatedClassName = r'$' + className; - // Собираем биндинги по всем методам класса, игнорируем абстрактные (без реализации) + // Collect bindings for all non-abstract methods. final binds = element.methods .where((m) => !m.isAbstract) .map(BindSpec.fromMethod) @@ -50,9 +81,19 @@ class GeneratedClass { return GeneratedClass(className, generatedClassName, binds); } - /// Генерирует исходный Dart-код для созданного класса DI-модуля. + /// ------------------------------------------------------------------------- + /// generate /// - /// Внутри builder(Scope currentScope) регистрируются все bind-методы. + /// ENGLISH + /// Generates Dart source code for the DI module class. The generated class + /// inherits from the original, overrides the 'builder' method, and registers + /// all bindings in the DI scope. + /// + /// RUSSIAN + /// Генерирует исходный Dart-код для класса-модуля DI. + /// Новая версия класса наследует оригинальный, переопределяет builder(Scope), + /// и регистрирует все зависимости через методы bind()... + /// ------------------------------------------------------------------------- String generate() { final buffer = StringBuffer(); @@ -60,6 +101,7 @@ class GeneratedClass { buffer.writeln(' @override'); buffer.writeln(' void builder(Scope currentScope) {'); + // For each binding, generate bind() code string. // Для каждого биндинга — генерируем строку bind()... for (final bind in binds) { buffer.writeln(bind.generateBind(4)); diff --git a/cherrypick_generator/lib/src/metadata_utils.dart b/cherrypick_generator/lib/src/metadata_utils.dart index 56b6f72..4f2dfea 100644 --- a/cherrypick_generator/lib/src/metadata_utils.dart +++ b/cherrypick_generator/lib/src/metadata_utils.dart @@ -13,13 +13,31 @@ import 'package:analyzer/dart/element/element.dart'; +/// --------------------------------------------------------------------------- +/// MetadataUtils -- utilities for analyzing method and parameter annotations. /// +/// ENGLISH +/// Provides static utility methods to analyze Dart annotations on methods or +/// parameters. For instance, helps to find if an element is annotated with +/// `@named()`, `@singleton()`, or other meta-annotations used in this DI framework. +/// +/// RUSSIAN /// Утилиты для разбора аннотаций методов и параметров. -/// Позволяют найти @named() и @singleton() у метода/параметра. -/// +/// Позволяют находить наличие аннотаций, например, @named() и @singleton(), +/// у методов и параметров. Используется для анализа исходного кода при генерации. +/// --------------------------------------------------------------------------- class MetadataUtils { + /// ------------------------------------------------------------------------- + /// anyMeta + /// + /// ENGLISH + /// Checks if any annotation in the list has a type name that contains + /// [typeName] (case insensitive). + /// + /// RUSSIAN /// Проверяет: есть ли среди аннотаций метка, имя которой содержит [typeName] - /// (регистр не учитывается) + /// (регистр не учитывается). + /// ------------------------------------------------------------------------- static bool anyMeta(List meta, String typeName) { return meta.any((m) => m @@ -31,8 +49,17 @@ class MetadataUtils { false); } + /// ------------------------------------------------------------------------- + /// getNamedValue + /// + /// ENGLISH + /// Retrieves the value from a `@named('value')` annotation if present. + /// Returns the string value or null if not found. + /// + /// RUSSIAN /// Находит значение из аннотации @named('значение'). /// Возвращает строку значения, если аннотация присутствует; иначе null. + /// ------------------------------------------------------------------------- static String? getNamedValue(List meta) { for (final m in meta) { final cv = m.computeConstantValue();