docs(generator): improve and unify English documentation and examples for all DI source files

- Added comprehensive English documentation for all DI generator and support files:
  * inject_generator.dart — full class/method doc-comments, usage samples
  * module_generator.dart — doc-comments, feature explanation, complete example
  * src/annotation_validator.dart — class and detailed static method descriptions
  * src/type_parser.dart — doc, example for ParsedType and TypeParser, specific codegen notes
  * src/bind_spec.dart — interface, static factory, and codegen docs with DI scenarios
  * src/bind_parameters_spec.dart — details and samples for code generation logic
  * src/metadata_utils.dart — full doc and examples for annotation utilities
  * src/exceptions.dart — user- and contributor-friendly errors, structured output, category explanations
  * src/generated_class.dart — usage-centric doc-comments, example of resulting generated DI class
- Removed Russian/duplicate comments for full clarity and maintainability
- Ensured that new and existing contributors can easily extend and maintain DI code generation logic

BREAKING CHANGE: All documentation now English-only; comments include usage examples for each principal structure or routine

See #docs, #generator, #cherrypick
This commit is contained in:
Sergey Penkovsky
2025-08-13 08:57:06 +03:00
parent 884df50a34
commit 7d45d00d6a
10 changed files with 557 additions and 268 deletions

View File

@@ -1,5 +1,3 @@
library;
//
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,6 +10,28 @@ library;
// See the License for the specific language governing permissions and
// limitations under the License.
//
library;
/// CherryPick code generation library: entry point for build_runner DI codegen.
///
/// This library exports generators for CherryPick dependency injection:
/// - [ModuleGenerator]: Generates DI module classes for all `@module()`-annotated classes.
/// - [InjectGenerator]: Generates field-injection mixins for classes annotated with `@injectable()`.
///
/// These generators are hooked into [build_runner] and cherrypick_generator's builder configuration.
/// Normally you do not import this directly; instead, add cherrypick_generator
/// as a dev_dependency and run `dart run build_runner build`.
///
/// Example usage in `build.yaml` or your project's workflow:
/// ```yaml
/// targets:
/// $default:
/// builders:
/// cherrypick_generator|cherrypick_generator:
/// generate_for:
/// - lib/**.dart
/// ```
///
/// For annotation details, see `package:cherrypick_annotations`.
export 'module_generator.dart';
export 'inject_generator.dart';

View File

@@ -20,28 +20,85 @@ import 'package:source_gen/source_gen.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
/// InjectGenerator generates a mixin for a class marked with @injectable()
/// and injects all fields annotated with @inject(), using CherryPick DI.
/// CherryPick DI field injector generator for codegen.
///
/// For Future<T> fields it calls .resolveAsync<T>(),
/// otherwise .resolve<T>() is used. Scope and named qualifiers are supported.
/// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`)
/// which contains the `_inject` method. This method will assign all fields annotated with `@inject()`
/// using the CherryPick DI container. Extra annotation qualifiers such as `@named` and `@scope` are respected
/// for each field. Nullable fields and Future/injectable async dependencies are also supported automatically.
///
/// ---
///
/// InjectGenerator генерирует миксин для класса с аннотацией @injectable()
/// и внедряет все поля, помеченные @inject(), используя DI-фреймворк CherryPick.
/// ### Example usage in a project:
///
/// Для Future<T> полей вызывается .resolveAsync<T>(),
/// для остальных — .resolve<T>(). Поддерживаются scope и named qualifier.
/// ```dart
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
///
/// @injectable()
/// class MyScreen with _$MyScreen {
/// @inject()
/// late final Logger logger;
///
/// @inject()
/// @named('test')
/// late final HttpClient client;
///
/// @inject()
/// Future<Analytics>? analytics;
/// }
/// ```
///
/// After running build_runner, this mixin will be auto-generated:
///
/// ```dart
/// mixin _$MyScreen {
/// void _inject(MyScreen instance) {
/// instance.logger = CherryPick.openRootScope().resolve<Logger>();
/// instance.client = CherryPick.openRootScope().resolve<HttpClient>(named: 'test');
/// instance.analytics = CherryPick.openRootScope().tryResolveAsync<Analytics>(); // nullable async inject
/// }
/// }
/// ```
///
/// You may use the mixin (e.g., `myScreen._inject(myScreen)`) or expose your own public helper for instance field injection.
///
/// **Supported scenarios:**
/// - Ordinary injectable fields: `resolve<T>()`.
/// - Named qualifiers: `resolve<T>(named: ...)`.
/// - Scoping: `CherryPick.openScope(scopeName: ...).resolve<T>()`.
/// - Nullable/incomplete fields: `tryResolve`/`tryResolveAsync`.
/// - Async dependencies: `Future<T>`/`resolveAsync<T>()`.
///
/// See also:
/// * @inject
/// * @injectable
class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
const InjectGenerator();
/// The main entry point for code generation.
/// Main entry point for CherryPick field injection code generation.
///
/// Checks class validity, collects injectable fields, and produces injection code.
/// - Only triggers for classes marked with `@injectable()`.
/// - Throws an error if used on non-class elements.
/// - Scans all fields marked with `@inject()` and gathers qualifiers (if any).
/// - Generates Dart code for a mixin that injects all dependencies into the target class instance.
///
/// Основная точка входа генератора. Проверяет класс, собирает инъектируемые поля и создает код внедрения зависимостей.
/// Returns the Dart code as a String defining the new mixin.
///
/// Example input (user code):
/// ```dart
/// @injectable()
/// class UserBloc with _$UserBloc {
/// @inject() late final AuthRepository authRepository;
/// }
/// ```
/// Example output (generated):
/// ```dart
/// mixin _$UserBloc {
/// void _inject(UserBloc instance) {
/// instance.authRepository = CherryPick.openRootScope().resolve<AuthRepository>();
/// }
/// }
/// ```
@override
FutureOr<String> generateForAnnotatedElement(
Element element,
@@ -63,8 +120,7 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
..writeln('mixin $mixinName {')
..writeln(' void _inject($className instance) {');
// Collect and process all @inject fields.
// Собираем и обрабатываем все поля с @inject.
// Collect and process all @inject fields
final injectFields =
classElement.fields.where(_isInjectField).map(_parseInjectField);
@@ -79,20 +135,20 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
return buffer.toString();
}
/// Checks if a field has the @inject annotation.
/// Returns true if a field is annotated with `@inject`.
///
/// Проверяет, отмечено ли поле аннотацией @inject.
/// Used to detect which fields should be processed for injection.
static bool _isInjectField(FieldElement field) {
return field.metadata.any(
(m) => m.computeConstantValue()?.type?.getDisplayString() == 'inject',
);
}
/// Parses the field for scope/named qualifiers and determines its type.
/// Returns a [_ParsedInjectField] describing injection information.
/// Parses `@inject()` field and extracts all injection metadata
/// (core type, qualifiers, scope, nullability, etc).
///
/// Разбирает поле на наличие модификаторов scope/named и выясняет его тип.
/// Возвращает [_ParsedInjectField] с информацией о внедрении.
/// Converts Dart field declaration and all parameterizing injection-related
/// annotations into a [_ParsedInjectField] which is used for codegen.
static _ParsedInjectField _parseInjectField(FieldElement field) {
String? scopeName;
String? namedValue;
@@ -120,8 +176,7 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
isFuture = false;
}
// ***
// Добавим определение nullable для типа (например PostRepository? или Future<PostRepository?>)
// Determine nullability for field types like T? or Future<T?>
bool isNullable = dartType.nullabilitySuffix ==
NullabilitySuffix.question ||
(dartType is ParameterizedType &&
@@ -139,13 +194,17 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
);
}
/// Generates a line of code that performs the dependency injection for a field.
/// Handles resolve/resolveAsync, scoping, and named qualifiers.
/// Generates Dart code for a single dependency-injected field based on its metadata.
///
/// Генерирует строку кода, которая внедряет зависимость для поля.
/// Учитывает resolve/resolveAsync, scoping и named qualifier.
/// This code will resolve the field from the CherryPick DI container and assign it to the class instance.
/// Correctly dispatches to resolve, tryResolve, resolveAsync, or tryResolveAsync methods,
/// and applies container scoping or named resolution where required.
///
/// Returns literal Dart code as string (1 line).
///
/// Example output:
/// `instance.logger = CherryPick.openRootScope().resolve<Logger>();`
String _generateInjectionLine(_ParsedInjectField field) {
// Используем tryResolve для nullable, иначе resolve
final resolveMethod = field.isFuture
? (field.isNullable
? 'tryResolveAsync<${field.coreType}>'
@@ -166,29 +225,29 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
}
}
/// Data structure representing all information required to generate
/// injection code for a field.
/// Internal structure: describes all required information for generating the injection
/// assignment for a given field.
///
/// Структура данных, содержащая всю информацию,
/// необходимую для генерации кода внедрения для поля.
/// Not exported. Used as a DTO in the generator for each field.
class _ParsedInjectField {
/// The name of the field / Имя поля.
/// The name of the field to be injected.
final String fieldName;
/// The base type name (T or Future<T>) / Базовый тип (T или тип из Future<T>).
/// The Dart type to resolve (e.g. `Logger` from `Logger` or `Future<Logger>`).
final String coreType;
/// True if the field type is Future<T>; false otherwise
/// Истина, если поле — Future<T>, иначе — ложь.
/// True if the field is an async dependency (Future<...>), otherwise false.
final bool isFuture;
/// Optional scope annotation argument / Опциональное имя scope.
/// True if the field accepts null (T?), otherwise false.
final bool isNullable;
/// The scoping for DI resolution, or null to use root scope.
final String? scopeName;
/// Optional named annotation argument / Опциональное имя named.
/// Name qualifier for named resolution, or null if not set.
final String? namedValue;
final bool isNullable;
_ParsedInjectField({
required this.fieldName,
@@ -200,8 +259,8 @@ class _ParsedInjectField {
});
}
/// Builder factory. Used by build_runner.
/// Factory for creating the build_runner builder for DI field injection.
///
/// Фабрика билдера. Используется build_runner.
/// Add this builder in your build.yaml if you're invoking CherryPick generators manually.
Builder injectBuilder(BuilderOptions options) =>
PartBuilder([InjectGenerator()], '.inject.cherrypick.g.dart');

View File

@@ -19,75 +19,89 @@ import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
import 'src/generated_class.dart';
/// ---------------------------------------------------------------------------
/// ModuleGenerator for code generation of dependency-injected modules.
/// CherryPick Module Generator — Codegen for DI 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.
/// This generator scans Dart classes annotated with `@module()` and generates
/// boilerplate for dependency injection registration automatically. Each public
/// method in such classes can be annotated to describe how an object should be
/// bound to the DI container (singleton, factory, named, with parameters, etc).
///
/// RUSSIAN
/// Генератор зависимостей для DI-контейнера на основе аннотаций.
/// Данный генератор автоматически создаёт код для внедрения зависимостей (DI)
/// на основе аннотаций в вашем исходном коде. Когда вы отмечаете класс
/// аннотацией `@module()`, этот генератор обработает все его публичные методы
/// и автоматически сгенерирует класс с биндингами (регистрациями зависимостей)
/// для DI-контейнера. Это избавляет от написания однообразного шаблонного кода.
/// The generated code collects all such bind methods and produces a Dart
/// companion *module registration class* with a `.bindAll()` method, which you
/// can use from your DI root to automatically register those dependencies.
///
/// ## Example
/// ```dart
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
///
/// @module()
/// abstract class AppModule {
/// @singleton()
/// Logger logger() => Logger();
///
/// @provide()
/// ApiService api(Logger logger) => ApiService(logger);
///
/// @named('dev')
/// FakeService fake() => FakeService();
/// }
/// ```
///
/// After codegen, you will get (simplified):
/// ```dart
/// class _\$AppModuleCherrypickModule extend AppModule {
/// static void bindAll(CherryPickScope scope, AppModule module) {
/// scope.addSingleton<Logger>(() => module.logger());
/// scope.addFactory<ApiService>(() => module.api(scope.resolve<Logger>()));
/// scope.addFactory<FakeService>(() => module.fake(), named: 'dev');
/// }
/// }
/// ```
///
/// Use it e.g. in your bootstrap:
/// ```dart
/// final scope = CherryPick.openRootScope()..intallModules([_\$AppModuleCherrypickModule()]);
/// ```
///
/// Features supported:
/// - Singleton, factory, named, parametric, and async providers
/// - Eliminates all boilerplate for DI registration
/// - Works with abstract classes and real classes
/// - Error if @module() is applied to a non-class
///
/// See also: [@singleton], [@provide], [@named], [@module]
/// ---------------------------------------------------------------------------
class ModuleGenerator extends GeneratorForAnnotation<ann.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.
/// Generates Dart source for a class marked with the `@module()` annotation.
///
/// RUSSIAN
/// Генерирует исходный код для класса-модуля с аннотацией `@module()`.
/// [element] — исходный класс, помеченный аннотацией.
/// [annotation] — значения параметров аннотации.
/// [buildStep] — информация о текущем шаге генерации.
/// -------------------------------------------------------------------------
/// Throws [InvalidGenerationSourceError] if used on anything except a class.
///
/// See file-level docs for usage and generated output example.
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
// Only classes are supported for @module() annotation
// Обрабатываются только классы (другие элементы — ошибка)
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@module() can only be applied to classes. / @module() может быть применён только к классам.',
'@module() can only be applied to classes.',
element: element,
);
}
final classElement = element;
// Build a representation of the generated bindings based on class methods /
// Создаёт объект, описывающий, какие биндинги нужно сгенерировать на основании методов класса
final generatedClass = GeneratedClass.fromClassElement(classElement);
// 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.
/// Codegen builder entry point: register this builder in build.yaml or your package.
///
/// RUSSIAN
/// Точка входа для генератора build_runner.
/// Возвращает Builder, используемый build_runner для генерации кода для всех
/// файлов, где встречается @module().
/// Used by build_runner. Generates .module.cherrypick.g.dart files for each
/// source file with an annotated @module() class.
/// ---------------------------------------------------------------------------
Builder moduleBuilder(BuilderOptions options) =>
PartBuilder([ModuleGenerator()], '.module.cherrypick.g.dart');

View File

@@ -15,9 +15,43 @@ import 'package:analyzer/dart/element/element.dart';
import 'exceptions.dart';
import 'metadata_utils.dart';
/// Validates annotation combinations and usage patterns
/// Provides static utility methods for validating annotation usage in CherryPick
/// dependency injection code generation.
///
/// This validator helps ensure that `@provide`, `@instance`, `@singleton`, `@params`,
/// `@inject`, `@named`, `@module`, and `@injectable` annotations are correctly and safely
/// combined in your codebase, preventing common configuration and codegen errors before
/// code is generated.
///
/// #### Example Usage
/// ```dart
/// void processMethod(MethodElement method) {
/// AnnotationValidator.validateMethodAnnotations(method);
/// }
/// ```
///
/// All exceptions are thrown as [AnnotationValidationException] and will include
/// a helpful message and context.
///
/// ---
/// Typical checks performed by this utility:
/// - Mutual exclusivity (`@instance` vs `@provide`)
/// - Required presence for fields and methods
/// - Proper parameters for `@named` and `@params`
/// - Correct usage of injectable fields, module class methods, etc.
///
class AnnotationValidator {
/// Validates annotations on a method element
/// Validates annotations for a given [MethodElement].
///
/// Checks:
/// - Mutual exclusivity of `@instance` and `@provide`.
/// - That a method is annotated with exactly one DI-producing annotation.
/// - If `@params` is present, that it is used together with `@provide`.
/// - Appropriate usage of `@singleton`.
/// - [@named] syntax and conventions.
/// - Parameter validation for method arguments.
///
/// Throws [AnnotationValidationException] on any violation.
static void validateMethodAnnotations(MethodElement method) {
final annotations = _getAnnotationNames(method.metadata);
@@ -26,14 +60,28 @@ class AnnotationValidator {
_validateAnnotationParameters(method);
}
/// Validates annotations on a field element
/// Validates that a [FieldElement] has correct injection annotations.
///
/// Specifically, ensures:
/// - Injectable fields are of valid type.
/// - No `void` injection.
/// - Correct scope naming if present.
///
/// Throws [AnnotationValidationException] if checks fail.
static void validateFieldAnnotations(FieldElement field) {
final annotations = _getAnnotationNames(field.metadata);
_validateInjectFieldAnnotations(field, annotations);
}
/// Validates annotations on a class element
/// Validates all class-level DI annotations.
///
/// Executes checks for:
/// - Module class validity (e.g. must have public DI methods if `@module`).
/// - Injectable class: at least one @inject field, field finalness, etc.
/// - Provides helpful context for error/warning reporting.
///
/// Throws [AnnotationValidationException] if checks fail.
static void validateClassAnnotations(ClassElement classElement) {
final annotations = _getAnnotationNames(classElement.metadata);
@@ -41,6 +89,9 @@ class AnnotationValidator {
_validateInjectableClassAnnotations(classElement, annotations);
}
// --- Internal helpers follow (private) ---
/// Helper: Returns the names of all annotation types on `metadata`.
static List<String> _getAnnotationNames(List<ElementAnnotation> metadata) {
return metadata
.map((m) => m.computeConstantValue()?.type?.getDisplayString())
@@ -49,6 +100,9 @@ class AnnotationValidator {
.toList();
}
/// Validates that mutually exclusive method annotations are not used together.
///
/// For example, `@instance` and `@provide` cannot both be present.
static void _validateMutuallyExclusiveAnnotations(
MethodElement method,
List<String> annotations,
@@ -68,6 +122,10 @@ class AnnotationValidator {
}
}
/// Validates correct annotation combinations, e.g.
/// - `@params` must be with `@provide`
/// - One of `@instance` or `@provide` must be present for a registration method
/// - Validates singleton usage
static void _validateAnnotationCombinations(
MethodElement method,
List<String> annotations,
@@ -105,6 +163,7 @@ class AnnotationValidator {
}
}
/// Singleton-specific method annotation checks.
static void _validateSingletonUsage(
MethodElement method,
List<String> annotations,
@@ -130,6 +189,7 @@ class AnnotationValidator {
}
}
/// Validates extra requirements or syntactic rules for annotation arguments, like @named.
static void _validateAnnotationParameters(MethodElement method) {
// Validate @named annotation parameters
final namedValue = MetadataUtils.getNamedValue(method.metadata);
@@ -170,11 +230,11 @@ class AnnotationValidator {
}
}
/// Checks that @params is used with compatible parameter type.
static void _validateParamsParameter(
ParameterElement param, MethodElement method) {
// @params parameter should typically be dynamic or Map<String, dynamic>
final paramType = param.type.getDisplayString();
if (paramType != 'dynamic' &&
paramType != 'Map<String, dynamic>' &&
paramType != 'Map<String, dynamic>?') {
@@ -194,6 +254,7 @@ class AnnotationValidator {
}
}
/// Checks field-level annotation for valid injectable fields.
static void _validateInjectFieldAnnotations(
FieldElement field,
List<String> annotations,
@@ -227,6 +288,7 @@ class AnnotationValidator {
}
}
/// Checks @module usage: must have at least one DI method, each with DI-annotation.
static void _validateModuleClassAnnotations(
ClassElement classElement,
List<String> annotations,
@@ -268,6 +330,7 @@ class AnnotationValidator {
}
}
/// Checks @injectable usage on classes and their fields.
static void _validateInjectableClassAnnotations(
ClassElement classElement,
List<String> annotations,

View File

@@ -12,57 +12,59 @@
//
/// ----------------------------------------------------------------------------
/// BindParameterSpec - describes a single method parameter and how to resolve it.
/// BindParameterSpec
///
/// 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<SomeDep>()
/// - If the parameter is named, emits:
/// currentScope.resolve<SomeDep>(named: 'yourName')
/// - If it's a runtime parameter (e.g. via @params()), emits:
/// args
/// Describes a single parameter for a DI provider/factory/binding method,
/// specifying how that parameter is to be resolved in generated code.
///
/// RUSSIAN
/// Описывает один параметр метода в DI, и его способ разрешения из контейнера.
/// Сохраняет имя типа, дополнительное имя (если параметр аннотирован через @named),
/// и признак runtime-параметра (@params).
/// Для обычной зависимости типа (например, SomeDep) генерирует строку вида:
/// currentScope.resolve<SomeDep>()
/// Для зависимости с именем:
/// currentScope.resolve<SomeDep>(named: 'имя')
/// Для runtime-параметра:
/// args
/// Stores the parameter's type name, optional `@named` identifier (for named DI resolution),
/// and a flag for runtime (@params) arguments. Used in CherryPick generator
/// for creating argument lists when invoking factories or provider methods.
///
/// ## Example usage
/// ```dart
/// // Binding method: @provide() Logger provideLogger(@named('debug') Config config, @params Map<String, dynamic> args)
/// final namedParam = BindParameterSpec('Config', 'debug');
/// final runtimeParam = BindParameterSpec('Map<String, dynamic>', null, isParams: true);
/// print(namedParam.generateArg()); // prints: currentScope.resolve<Config>(named: 'debug')
/// print(runtimeParam.generateArg()); // prints: args
/// ```
///
/// ## Code generation logic
/// - Injected: currentScope.resolve<Service>()
/// - Named: currentScope.resolve<Service>(named: 'name')
/// - @params: args
/// ----------------------------------------------------------------------------
class BindParameterSpec {
/// Type name of the parameter (e.g. SomeService)
/// Имя типа параметра (например, SomeService)
/// The type name of the parameter (e.g., 'UserRepository')
final String typeName;
/// Optional name for named resolution (from @named)
/// Необязательное имя для разрешения по имени (если аннотировано через @named)
/// If non-null, this is the named-key for DI resolution (from @named).
final String? named;
/// True if this parameter uses @params and should be provided from runtime args
/// Признак, что параметр — runtime (через @params)
/// True if this parameter is a runtime param (annotated with @params and
/// filled by a runtime argument map).
final bool isParams;
BindParameterSpec(this.typeName, this.named, {this.isParams = false});
/// --------------------------------------------------------------------------
/// generateArg
/// Generates Dart code to resolve this parameter in the DI container.
///
/// ENGLISH
/// Generates Dart code for resolving the dependency from the DI scope,
/// considering type, named, and param-argument.
/// - For normal dependencies: resolves by type
/// - For named dependencies: resolves by type and name
/// - For @params: uses the supplied params variable (default 'args')
///
/// RUSSIAN
/// Генерирует строку для получения зависимости из DI scope (с учётом имени
/// и типа параметра или runtime-режима @params).
/// --------------------------------------------------------------------------
/// ## Example
/// ```dart
/// final a = BindParameterSpec('Api', null); // normal
/// print(a.generateArg()); // currentScope.resolve<Api>()
///
/// final b = BindParameterSpec('Api', 'prod'); // named
/// print(b.generateArg()); // currentScope.resolve<Api>(named: 'prod')
///
/// final c = BindParameterSpec('Map<String,dynamic>', null, isParams: true); // params
/// print(c.generateArg()); // args
/// ```
String generateArg([String paramsVar = 'args']) {
if (isParams) {
return paramsVar;

View File

@@ -19,62 +19,63 @@ import 'exceptions.dart';
import 'type_parser.dart';
import 'annotation_validator.dart';
/// Enum representing the binding annotation applied to a module method.
enum BindingType {
/// Direct instance returned from the method (@instance).
instance,
/// Provider/factory function (@provide).
provide;
}
/// ---------------------------------------------------------------------------
/// BindSpec -- describes a binding specification generated for a dependency.
/// BindSpec
///
/// 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.
/// Describes a DI container binding as generated from a single public factory,
/// instance, or provider method of a module (annotated with @instance or @provide).
///
/// RUSSIAN
/// Описывает параметры для создания одного биндинга зависимости (binding spec).
/// Каждый биндинг соответствует одному публичному методу класса-модуля и
/// содержит всю информацию для генерации кода регистрации этого биндинга в
/// DI-контейнере: тип возвращаемой зависимости, имя метода, параметры, аннотации
/// (@singleton, @named, @instance, @provide), асинхронность, признак runtime
/// аргументов и др. Генерирует правильный Dart-код для регистрации биндера.
/// Includes all annotation-driven parameters required to generate valid DI
/// registration Dart code in CherryPick:
/// - Return type
/// - Provider method name
/// - Singleton flag
/// - Named identifier (from @named)
/// - List of resolved or runtime (@params) parameters
/// - Binding mode (instance/provide)
/// - Async and parametric variants
///
/// ## Example usage
/// ```dart
/// // Suppose @provide() Api api(@named('test') Client client)
/// final bindSpec = BindSpec.fromMethod(methodElement);
/// print(bindSpec.generateBind(2)); // bind<Api>().toProvide(() => api(currentScope.resolve<Client>(named: 'test')));
/// ```
/// ---------------------------------------------------------------------------
class BindSpec {
/// The type this binding provides (e.g. SomeService)
/// Тип, который предоставляет биндинг (например, SomeService)
final String returnType;
/// Method name that implements the binding
/// Имя метода, который реализует биндинг
/// Binding provider/factory method name
final String methodName;
/// Optional name for named dependency (from @named)
/// Необязательное имя, для именованной зависимости (используется с @named)
/// Named identifier for DI resolution (null if unnamed)
final String? named;
/// Whether the dependency is a singleton (@singleton annotation)
/// Является ли зависимость синглтоном (имеется ли аннотация @singleton)
/// If true, binding is registered as singleton in DI
final bool isSingleton;
/// List of method parameters to inject dependencies with
/// Список параметров, которые требуются методу для внедрения зависимостей
/// Provider/factory method parameters (in order)
final List<BindParameterSpec> parameters;
/// Binding type: 'instance' or 'provide' (@instance or @provide)
final BindingType bindingType; // 'instance' | 'provide'
/// Instance vs provider mode, from annotation choice
final BindingType bindingType;
/// True if the method is asynchronous and uses instance binding (Future)
/// Async flag for .toInstanceAsync()
final bool isAsyncInstance;
/// True if the method is asynchronous and uses provide binding (Future)
/// Async flag for .toProvideAsync()
final bool isAsyncProvide;
/// True if the binding method accepts runtime "params" argument (@params)
/// True if a @params runtime parameter is present
final bool hasParams;
BindSpec({
@@ -89,20 +90,12 @@ class BindSpec {
required this.hasParams,
});
/// -------------------------------------------------------------------------
/// generateBind
/// Generates a Dart code line for binding registration.
///
/// ENGLISH
/// Generates a line of Dart code registering the binding with the DI framework.
/// Produces something like:
/// bind<Type>().toProvide(() => method(args)).withName('name').singleton();
/// Indent parameter allows formatted multiline output.
/// Example (single-line):
/// bind<Api>().toProvide(() => provideApi(currentScope.resolve<Client>(named: 'test'))).withName('prod').singleton();
///
/// RUSSIAN
/// Формирует dart-код для биндинга, например:
/// bind<Type>().toProvide(() => method(args)).withName('name').singleton();
/// Параметр [indent] задаёт отступ для красивого форматирования кода.
/// -------------------------------------------------------------------------
/// The [indent] argument sets the space indentation for pretty-printing.
String generateBind(int indent) {
final indentStr = ' ' * indent;
final provide = _generateProvideClause(indent);
@@ -151,7 +144,7 @@ class BindSpec {
return _generatePlainProvideClause(indent);
}
/// EN / RU: Supports runtime parameters (@params).
/// Generates code when using runtime parameters (@params).
String _generateWithParamsProvideClause(int indent) {
// Safe variable name for parameters.
const paramVar = 'args';
@@ -178,7 +171,7 @@ class BindSpec {
}
}
/// EN / RU: Supports only injected dependencies, not runtime (@params).
/// Generates code when only resolved (not runtime) arguments used.
String _generatePlainProvideClause(int indent) {
final argsStr = parameters.map((p) => p.generateArg()).join(', ');
@@ -241,16 +234,17 @@ class 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().
/// Constructs a [BindSpec] from an analyzer [MethodElement].
///
/// RUSSIAN
/// Создаёт спецификацию биндинга (BindSpec) из метода класса-модуля, анализируя
/// возвращаемый тип, аннотации, параметры (и их аннотации), а также факт
/// асинхронности. Если нет @instance или @provide — кидает ошибку.
/// -------------------------------------------------------------------------
/// Validates and parses all type annotations, method/parameter DI hints,
/// and derives async and parametric flags as needed.
///
/// ## Example
/// ```dart
/// final bindSpec = BindSpec.fromMethod(methodElement);
/// print(bindSpec.returnType); // e.g., 'Logger'
/// ```
/// Throws [AnnotationValidationException] or [CodeGenerationException] if invalid.
static BindSpec fromMethod(MethodElement method) {
try {
// Validate method annotations

View File

@@ -14,10 +14,36 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:source_gen/source_gen.dart';
/// Enhanced exception class for CherryPick generator with detailed context information
/// ---------------------------------------------------------------------------
/// CherryPickGeneratorException
///
/// The base exception for all CherryPick code generation and annotation
/// validation errors. This exception provides enhanced diagnostics including
/// the error category, helpful suggestions, and additional debugging context.
///
/// All errors are structured to be as helpful as possible for users
/// running build_runner and for CherryPick contributors debugging generators.
///
/// ## Example usage:
/// ```dart
/// if (someErrorCondition) {
/// throw AnnotationValidationException(
/// 'Custom message about what went wrong',
/// element: methodElement,
/// suggestion: 'Add @provide() or @instance() annotation',
/// context: {'found_annotations': annotations},
/// );
/// }
/// ```
/// ---------------------------------------------------------------------------
class CherryPickGeneratorException extends InvalidGenerationSourceError {
/// A string describing the error category (for grouping).
final String category;
/// An optional suggestion string for resolving the error.
final String? suggestion;
/// Arbitrary key-value map for additional debugging information.
final Map<String, dynamic>? context;
CherryPickGeneratorException(
@@ -50,7 +76,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
buffer.writeln(' Type: ${element.runtimeType}');
buffer.writeln(' Location: ${element.source?.fullName ?? 'unknown'}');
// Note: enclosingElement may not be available in all analyzer versions
// Try to show enclosing element info for extra context
try {
final enclosing = (element as dynamic).enclosingElement;
if (enclosing != null) {
@@ -60,7 +86,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
// Ignore if enclosingElement is not available
}
// Additional context
// Arbitrary user context
if (context != null && context.isNotEmpty) {
buffer.writeln('');
buffer.writeln('Additional Context:');
@@ -69,7 +95,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
});
}
// Suggestion
// Hint/suggestion if present
if (suggestion != null) {
buffer.writeln('');
buffer.writeln('💡 Suggestion: $suggestion');
@@ -79,7 +105,24 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
}
}
/// Specific exception types for different error categories
/// ---------------------------------------------------------------------------
/// AnnotationValidationException
///
/// Thrown when annotation usage is invalid (e.g., missing required annotation,
/// mutually exclusive annotations, or incorrect @named format).
///
/// Grouped as category "ANNOTATION_VALIDATION".
///
/// ## Example:
/// ```dart
/// throw AnnotationValidationException(
/// '@instance and @provide cannot be used together',
/// element: method,
/// suggestion: 'Use only one of @instance or @provide.',
/// context: {'method_name': method.displayName},
/// );
/// ```
/// ---------------------------------------------------------------------------
class AnnotationValidationException extends CherryPickGeneratorException {
AnnotationValidationException(
super.message, {
@@ -89,6 +132,24 @@ class AnnotationValidationException extends CherryPickGeneratorException {
}) : super(category: 'ANNOTATION_VALIDATION');
}
/// ---------------------------------------------------------------------------
/// TypeParsingException
///
/// Thrown when a Dart type cannot be interpreted/parsed for DI,
/// or if it's not compatible (void, raw Future, etc).
///
/// Grouped as category "TYPE_PARSING".
///
/// ## Example:
/// ```dart
/// throw TypeParsingException(
/// 'Cannot parse injected type',
/// element: field,
/// suggestion: 'Specify a concrete type. Avoid dynamic and raw Future.',
/// context: {'type': field.type.getDisplayString()},
/// );
/// ```
/// ---------------------------------------------------------------------------
class TypeParsingException extends CherryPickGeneratorException {
TypeParsingException(
super.message, {
@@ -98,6 +159,23 @@ class TypeParsingException extends CherryPickGeneratorException {
}) : super(category: 'TYPE_PARSING');
}
/// ---------------------------------------------------------------------------
/// CodeGenerationException
///
/// Thrown on unexpected code generation or formatting failures
/// during generator execution.
///
/// Grouped as category "CODE_GENERATION".
///
/// ## Example:
/// ```dart
/// throw CodeGenerationException(
/// 'Could not generate module binding',
/// element: classElement,
/// suggestion: 'Check module class methods and signatures.',
/// );
/// ```
/// ---------------------------------------------------------------------------
class CodeGenerationException extends CherryPickGeneratorException {
CodeGenerationException(
super.message, {
@@ -107,6 +185,23 @@ class CodeGenerationException extends CherryPickGeneratorException {
}) : super(category: 'CODE_GENERATION');
}
/// ---------------------------------------------------------------------------
/// DependencyResolutionException
///
/// Thrown if dependency information (for example, types or names)
/// cannot be resolved during code generation analysis.
///
/// Grouped as category "DEPENDENCY_RESOLUTION".
///
/// ## Example:
/// ```dart
/// throw DependencyResolutionException(
/// 'Dependency type not found in scope',
/// element: someElement,
/// suggestion: 'Check CherryPick registration for this type.',
/// );
/// ```
/// ---------------------------------------------------------------------------
class DependencyResolutionException extends CherryPickGeneratorException {
DependencyResolutionException(
super.message, {

View File

@@ -12,45 +12,48 @@
//
import 'package:analyzer/dart/element/element.dart';
import 'bind_spec.dart';
/// ---------------------------------------------------------------------------
/// GeneratedClass -- represents the result of processing a single module class.
/// GeneratedClass
///
/// 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.
/// Represents a processed DI module class with all its binding methods analyzed.
/// Stores:
/// - The original class name,
/// - The generated implementation class name (with $ prefix),
/// - The list of all BindSpec for the module methods,
/// - The source file name for reference or directive generation.
///
/// Also provides code generation functionality, allowing to generate the source
/// code for the derived DI module class, including all binding registrations.
/// Provides static and instance methods to construct from a ClassElement
/// and generate Dart source code for the resulting DI registration class.
///
/// RUSSIAN
/// Описывает результат обработки одного класса-модуля DI:
/// - Имя оригинального класса,
/// - Имя генерируемого класса (например, `$SomeModule`),
/// - Список всех бидингов (BindSpec) — по публичным методам модуля.
///
/// Также содержит функцию генерации исходного кода для этого класса и
/// регистрации всех зависимостей через bind(...).
/// ## Example usage
/// ```dart
/// final gen = GeneratedClass.fromClassElement(myModuleClassElement);
/// print(gen.generate());
/// /*
/// Produces:
/// final class $MyModule extends MyModule {
/// @override
/// void builder(Scope currentScope) {
/// bind<Service>().toProvide(() => provideService(currentScope.resolve<Dep>()));
/// ...
/// }
/// }
/// */
/// ```
/// ---------------------------------------------------------------------------
class GeneratedClass {
/// The name of the original module class.
/// Имя исходного класса-модуля
/// Name of the original Dart module class.
final String className;
/// The name of the generated class (e.g., $SomeModule).
/// Имя генерируемого класса (например, $SomeModule)
/// Name of the generated class, e.g. `$MyModule`
final String generatedClassName;
/// List of all discovered bindings for the class.
/// Список всех обнаруженных биндингов
/// Binding specs for all provider/factory methods in the class.
final List<BindSpec> binds;
/// Source file name for the part directive
/// Имя исходного файла для part директивы
/// Source filename of the module class (for code references).
final String sourceFile;
GeneratedClass(
@@ -63,16 +66,15 @@ class 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 `$`.
/// Creates a [GeneratedClass] by analyzing a Dart [ClassElement].
/// Collects all public non-abstract methods, creates a [BindSpec] for each,
/// and infers the generated class name using a `$` prefix.
///
/// RUSSIAN
/// Строит объект класса по элементу AST (ClassElement): имя класса,
/// сгенерированное имя, список BindSpec по всем не абстрактным методам.
/// Имя ген-класса строится с префиксом `$`.
/// -------------------------------------------------------------------------
/// ## Example usage
/// ```dart
/// final gen = GeneratedClass.fromClassElement(classElement);
/// print(gen.generatedClassName); // e.g. $AppModule
/// ```
static GeneratedClass fromClassElement(ClassElement element) {
final className = element.displayName;
// Generated class name with '$' prefix (standard for generated Dart code).
@@ -91,16 +93,19 @@ class GeneratedClass {
/// -------------------------------------------------------------------------
/// generate
///
/// 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.
/// Generates the Dart source code for the DI registration class.
/// The generated class extends the original module, and the `builder` method
/// registers all bindings (dependencies) into the DI scope.
///
/// RUSSIAN
/// Генерирует исходный Dart-код для класса-модуля DI.
/// Новая версия класса наследует оригинальный, переопределяет builder(Scope),
/// и регистрирует все зависимости через методы bind<Type>()...
/// -------------------------------------------------------------------------
/// ## Example output
/// ```dart
/// final class $UserModule extends UserModule {
/// @override
/// void builder(Scope currentScope) {
/// bind<Service>().toProvide(() => provideService(currentScope.resolve<Dep>()));
/// }
/// }
/// ```
String generate() {
final buffer = StringBuffer()
..writeln('final class $generatedClassName extends $className {')
@@ -108,7 +113,6 @@ class GeneratedClass {
..writeln(' void builder(Scope currentScope) {');
// For each binding, generate bind<Type>() code string.
// Для каждого биндинга — генерируем строку bind<Type>()...
for (final bind in binds) {
buffer.writeln(bind.generateBind(4));
}

View File

@@ -14,30 +14,32 @@
import 'package:analyzer/dart/element/element.dart';
/// ---------------------------------------------------------------------------
/// MetadataUtils -- utilities for analyzing method and parameter annotations.
/// MetadataUtils
///
/// 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.
/// Static utilities for querying and extracting information from
/// Dart annotations ([ElementAnnotation]) in the context of code generation,
/// such as checking for the presence of specific DI-related annotations.
/// Designed to be used internally by code generation and validation routines.
///
/// RUSSIAN
/// Утилиты для разбора аннотаций методов и параметров.
/// Позволяют находить наличие аннотаций, например, @named() и @singleton(),
/// у методов и параметров. Используется для анализа исходного кода при генерации.
/// # Example usage
/// ```dart
/// if (MetadataUtils.anyMeta(method.metadata, 'singleton')) {
/// // The method is annotated with @singleton
/// }
/// final name = MetadataUtils.getNamedValue(field.metadata);
/// if (name != null) print('@named value: $name');
/// ```
/// ---------------------------------------------------------------------------
class MetadataUtils {
/// -------------------------------------------------------------------------
/// anyMeta
/// Checks whether any annotation in [meta] matches the [typeName]
/// (type name is compared in a case-insensitive manner and can be partial).
///
/// ENGLISH
/// Checks if any annotation in the list has a type name that contains
/// [typeName] (case insensitive).
/// Returns true if an annotation (such as @singleton, @provide, @named) is found.
///
/// RUSSIAN
/// Проверяет: есть ли среди аннотаций метка, имя которой содержит [typeName]
/// (регистр не учитывается).
/// -------------------------------------------------------------------------
/// Example:
/// ```dart
/// bool isSingleton = MetadataUtils.anyMeta(myMethod.metadata, 'singleton');
/// ```
static bool anyMeta(List<ElementAnnotation> meta, String typeName) {
return meta.any((m) =>
m
@@ -49,17 +51,15 @@ class MetadataUtils {
false);
}
/// -------------------------------------------------------------------------
/// getNamedValue
/// Extracts the string value from a `@named('value')` annotation if present in [meta].
///
/// ENGLISH
/// Retrieves the value from a `@named('value')` annotation if present.
/// Returns the string value or null if not found.
/// Returns the named value or `null` if not annotated.
///
/// RUSSIAN
/// Находит значение из аннотации @named('значение').
/// Возвращает строку значения, если аннотация присутствует; иначе null.
/// -------------------------------------------------------------------------
/// Example:
/// ```dart
/// // For: @named('dev') ApiClient provideApi() ...
/// final named = MetadataUtils.getNamedValue(method.metadata); // 'dev'
/// ```
static String? getNamedValue(List<ElementAnnotation> meta) {
for (final m in meta) {
final cv = m.computeConstantValue();

View File

@@ -16,9 +16,35 @@ import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'exceptions.dart';
/// Enhanced type parser that uses AST analysis instead of regular expressions
/// Utility for analyzing and parsing Dart types for CherryPick DI code generation.
///
/// This type parser leverages the Dart analyzer AST to extract nuanced information
/// from Dart types encountered in the source code, in particular for dependency
/// injection purposes. It is capable of extracting nullability, generics,
/// and Future-related metadata with strong guarantees and handles even nested generics.
///
/// # Example usage for parsing types:
/// ```dart
/// final parsed = TypeParser.parseType(method.returnType, method);
/// print(parsed);
/// print(parsed.resolveMethodName); // e.g. "resolveAsync" or "tryResolve"
/// ```
///
/// # Supported scenarios:
/// - Nullable types (e.g., `List<String>?`)
/// - Generic types (e.g., `Map<String, User>`)
/// - Async types (`Future<T>`, including nested generics)
/// - Validation for DI compatibility (throws for `void`, warns on `dynamic`)
class TypeParser {
/// Parses a DartType and extracts detailed type information
/// Parses a [DartType] and extracts detailed type information for use in code generation.
///
/// If a type is not suitable or cannot be parsed, a [TypeParsingException] is thrown.
///
/// Example:
/// ```dart
/// final parsed = TypeParser.parseType(field.type, field);
/// if (parsed.isNullable) print('Field is nullable');
/// ```
static ParsedType parseType(DartType dartType, Element context) {
try {
return _parseTypeInternal(dartType, context);
@@ -49,7 +75,7 @@ class TypeParser {
return _parseGenericType(dartType, context, isNullable);
}
// Simple type
// Simple type (non-generic, non-Future)
return ParsedType(
displayString: displayString,
coreType: displayString.replaceAll('?', ''),
@@ -103,7 +129,15 @@ class TypeParser {
);
}
/// Validates that a type is suitable for dependency injection
/// Validates that a parsed type is suitable for dependency injection.
///
/// Throws [TypeParsingException] for void and may warn for dynamic.
///
/// Example:
/// ```dart
/// final parsed = TypeParser.parseType(field.type, field);
/// TypeParser.validateInjectableType(parsed, field);
/// ```
static void validateInjectableType(ParsedType parsedType, Element context) {
// Check for void type
if (parsedType.coreType == 'void') {
@@ -131,7 +165,7 @@ class TypeParser {
}
}
/// Represents a parsed type with detailed information
/// Represents a parsed type with full metadata for code generation.
class ParsedType {
/// The full display string of the type (e.g., "Future<List<String>?>")
final String displayString;
@@ -139,19 +173,19 @@ class ParsedType {
/// The core type name without nullability and Future wrapper (e.g., "List<String>")
final String coreType;
/// Whether the type is nullable
/// True if nullable (has `?`)
final bool isNullable;
/// Whether the type is wrapped in Future
/// True if this type is a `Future<T>`
final bool isFuture;
/// Whether the type has generic parameters
/// True if the type is a generic type (`List<T>`)
final bool isGeneric;
/// Parsed type arguments for generic types
/// List of parsed type arguments in generics, if any.
final List<ParsedType> typeArguments;
/// For Future types, the inner type
/// For `Future<T>`, this is the type inside the `Future`.
final ParsedType? innerType;
const ParsedType({
@@ -164,7 +198,11 @@ class ParsedType {
this.innerType,
});
/// Returns the type string suitable for code generation
/// Generates the type string suitable for code generation.
///
/// - For futures, the codegen type of the inner type is returned
/// - For generics, returns e.g. `List<User>`
/// - For plain types, just the name
String get codeGenType {
if (isFuture && innerType != null) {
return innerType!.codeGenType;
@@ -179,10 +217,10 @@ class ParsedType {
return coreType;
}
/// Returns whether this type should use tryResolve instead of resolve
/// True if this type should use `tryResolve` instead of `resolve` for DI.
bool get shouldUseTryResolve => isNullable;
/// Returns the appropriate resolve method name
/// Returns the method name for DI, e.g. "resolve", "tryResolveAsync", etc.
String get resolveMethodName {
if (isFuture) {
return shouldUseTryResolve ? 'tryResolveAsync' : 'resolveAsync';