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) // Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
// Licensed under the Apache License, Version 2.0 (the "License"); // 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 // See the License for the specific language governing permissions and
// limitations under the License. // 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 'module_generator.dart';
export 'inject_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:analyzer/dart/element/element.dart';
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann; import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
/// InjectGenerator generates a mixin for a class marked with @injectable() /// CherryPick DI field injector generator for codegen.
/// and injects all fields annotated with @inject(), using CherryPick DI.
/// ///
/// For Future<T> fields it calls .resolveAsync<T>(), /// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`)
/// otherwise .resolve<T>() is used. Scope and named qualifiers are supported. /// 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() /// ### Example usage in a project:
/// и внедряет все поля, помеченные @inject(), используя DI-фреймворк CherryPick.
/// ///
/// Для Future<T> полей вызывается .resolveAsync<T>(), /// ```dart
/// для остальных — .resolve<T>(). Поддерживаются scope и named qualifier. /// 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> { class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
const InjectGenerator(); 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 @override
FutureOr<String> generateForAnnotatedElement( FutureOr<String> generateForAnnotatedElement(
Element element, Element element,
@@ -63,8 +120,7 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
..writeln('mixin $mixinName {') ..writeln('mixin $mixinName {')
..writeln(' void _inject($className instance) {'); ..writeln(' void _inject($className instance) {');
// Collect and process all @inject fields. // Collect and process all @inject fields
// Собираем и обрабатываем все поля с @inject.
final injectFields = final injectFields =
classElement.fields.where(_isInjectField).map(_parseInjectField); classElement.fields.where(_isInjectField).map(_parseInjectField);
@@ -79,20 +135,20 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
return buffer.toString(); 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) { static bool _isInjectField(FieldElement field) {
return field.metadata.any( return field.metadata.any(
(m) => m.computeConstantValue()?.type?.getDisplayString() == 'inject', (m) => m.computeConstantValue()?.type?.getDisplayString() == 'inject',
); );
} }
/// Parses the field for scope/named qualifiers and determines its type. /// Parses `@inject()` field and extracts all injection metadata
/// Returns a [_ParsedInjectField] describing injection information. /// (core type, qualifiers, scope, nullability, etc).
/// ///
/// Разбирает поле на наличие модификаторов scope/named и выясняет его тип. /// Converts Dart field declaration and all parameterizing injection-related
/// Возвращает [_ParsedInjectField] с информацией о внедрении. /// annotations into a [_ParsedInjectField] which is used for codegen.
static _ParsedInjectField _parseInjectField(FieldElement field) { static _ParsedInjectField _parseInjectField(FieldElement field) {
String? scopeName; String? scopeName;
String? namedValue; String? namedValue;
@@ -120,8 +176,7 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
isFuture = false; isFuture = false;
} }
// *** // Determine nullability for field types like T? or Future<T?>
// Добавим определение nullable для типа (например PostRepository? или Future<PostRepository?>)
bool isNullable = dartType.nullabilitySuffix == bool isNullable = dartType.nullabilitySuffix ==
NullabilitySuffix.question || NullabilitySuffix.question ||
(dartType is ParameterizedType && (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. /// Generates Dart code for a single dependency-injected field based on its metadata.
/// Handles resolve/resolveAsync, scoping, and named qualifiers.
/// ///
/// Генерирует строку кода, которая внедряет зависимость для поля. /// This code will resolve the field from the CherryPick DI container and assign it to the class instance.
/// Учитывает resolve/resolveAsync, scoping и named qualifier. /// 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) { String _generateInjectionLine(_ParsedInjectField field) {
// Используем tryResolve для nullable, иначе resolve
final resolveMethod = field.isFuture final resolveMethod = field.isFuture
? (field.isNullable ? (field.isNullable
? 'tryResolveAsync<${field.coreType}>' ? 'tryResolveAsync<${field.coreType}>'
@@ -166,29 +225,29 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
} }
} }
/// Data structure representing all information required to generate /// Internal structure: describes all required information for generating the injection
/// injection code for a field. /// assignment for a given field.
/// ///
/// Структура данных, содержащая всю информацию, /// Not exported. Used as a DTO in the generator for each field.
/// необходимую для генерации кода внедрения для поля.
class _ParsedInjectField { class _ParsedInjectField {
/// The name of the field / Имя поля. /// The name of the field to be injected.
final String fieldName; 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; final String coreType;
/// True if the field type is Future<T>; false otherwise /// True if the field is an async dependency (Future<...>), otherwise false.
/// Истина, если поле — Future<T>, иначе — ложь.
final bool isFuture; 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; final String? scopeName;
/// Optional named annotation argument / Опциональное имя named. /// Name qualifier for named resolution, or null if not set.
final String? namedValue; final String? namedValue;
final bool isNullable;
_ParsedInjectField({ _ParsedInjectField({
required this.fieldName, 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) => Builder injectBuilder(BuilderOptions options) =>
PartBuilder([InjectGenerator()], '.inject.cherrypick.g.dart'); 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'; import 'src/generated_class.dart';
/// --------------------------------------------------------------------------- /// ---------------------------------------------------------------------------
/// ModuleGenerator for code generation of dependency-injected modules. /// CherryPick Module Generator — Codegen for DI modules
/// ///
/// ENGLISH /// This generator scans Dart classes annotated with `@module()` and generates
/// This generator scans for Dart classes annotated with `@module()` and /// boilerplate for dependency injection registration automatically. Each public
/// automatically generates boilerplate code for dependency injection /// method in such classes can be annotated to describe how an object should be
/// (DI) based on the public methods in those classes. Each method can be /// bound to the DI container (singleton, factory, named, with parameters, etc).
/// 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 /// The generated code collects all such bind methods and produces a Dart
/// Генератор зависимостей для DI-контейнера на основе аннотаций. /// companion *module registration class* with a `.bindAll()` method, which you
/// Данный генератор автоматически создаёт код для внедрения зависимостей (DI) /// can use from your DI root to automatically register those dependencies.
/// на основе аннотаций в вашем исходном коде. Когда вы отмечаете класс ///
/// аннотацией `@module()`, этот генератор обработает все его публичные методы /// ## Example
/// и автоматически сгенерирует класс с биндингами (регистрациями зависимостей) /// ```dart
/// для DI-контейнера. Это избавляет от написания однообразного шаблонного кода. /// 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> { class ModuleGenerator extends GeneratorForAnnotation<ann.module> {
/// ------------------------------------------------------------------------- /// Generates Dart source for a class marked with the `@module()` annotation.
/// 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 /// Throws [InvalidGenerationSourceError] if used on anything except a class.
/// Генерирует исходный код для класса-модуля с аннотацией `@module()`. ///
/// [element] — исходный класс, помеченный аннотацией. /// See file-level docs for usage and generated output example.
/// [annotation] — значения параметров аннотации.
/// [buildStep] — информация о текущем шаге генерации.
/// -------------------------------------------------------------------------
@override @override
String generateForAnnotatedElement( String generateForAnnotatedElement(
Element element, Element element,
ConstantReader annotation, ConstantReader annotation,
BuildStep buildStep, BuildStep buildStep,
) { ) {
// Only classes are supported for @module() annotation
// Обрабатываются только классы (другие элементы — ошибка)
if (element is! ClassElement) { if (element is! ClassElement) {
throw InvalidGenerationSourceError( throw InvalidGenerationSourceError(
'@module() can only be applied to classes. / @module() может быть применён только к классам.', '@module() can only be applied to classes.',
element: element, element: element,
); );
} }
final classElement = element; final classElement = element;
// Build a representation of the generated bindings based on class methods /
// Создаёт объект, описывающий, какие биндинги нужно сгенерировать на основании методов класса
final generatedClass = GeneratedClass.fromClassElement(classElement); final generatedClass = GeneratedClass.fromClassElement(classElement);
// Generate the resulting Dart code / Генерирует итоговый Dart-код
return generatedClass.generate(); return generatedClass.generate();
} }
} }
/// --------------------------------------------------------------------------- /// ---------------------------------------------------------------------------
/// ENGLISH /// Codegen builder entry point: register this builder in build.yaml or your package.
/// Entry point for build_runner. Returns a Builder used to generate code for
/// every file with a @module() annotation.
/// ///
/// RUSSIAN /// Used by build_runner. Generates .module.cherrypick.g.dart files for each
/// Точка входа для генератора build_runner. /// source file with an annotated @module() class.
/// Возвращает Builder, используемый build_runner для генерации кода для всех
/// файлов, где встречается @module().
/// --------------------------------------------------------------------------- /// ---------------------------------------------------------------------------
Builder moduleBuilder(BuilderOptions options) => Builder moduleBuilder(BuilderOptions options) =>
PartBuilder([ModuleGenerator()], '.module.cherrypick.g.dart'); PartBuilder([ModuleGenerator()], '.module.cherrypick.g.dart');

View File

@@ -15,9 +15,43 @@ import 'package:analyzer/dart/element/element.dart';
import 'exceptions.dart'; import 'exceptions.dart';
import 'metadata_utils.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 { 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) { static void validateMethodAnnotations(MethodElement method) {
final annotations = _getAnnotationNames(method.metadata); final annotations = _getAnnotationNames(method.metadata);
@@ -26,14 +60,28 @@ class AnnotationValidator {
_validateAnnotationParameters(method); _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) { static void validateFieldAnnotations(FieldElement field) {
final annotations = _getAnnotationNames(field.metadata); final annotations = _getAnnotationNames(field.metadata);
_validateInjectFieldAnnotations(field, annotations); _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) { static void validateClassAnnotations(ClassElement classElement) {
final annotations = _getAnnotationNames(classElement.metadata); final annotations = _getAnnotationNames(classElement.metadata);
@@ -41,6 +89,9 @@ class AnnotationValidator {
_validateInjectableClassAnnotations(classElement, annotations); _validateInjectableClassAnnotations(classElement, annotations);
} }
// --- Internal helpers follow (private) ---
/// Helper: Returns the names of all annotation types on `metadata`.
static List<String> _getAnnotationNames(List<ElementAnnotation> metadata) { static List<String> _getAnnotationNames(List<ElementAnnotation> metadata) {
return metadata return metadata
.map((m) => m.computeConstantValue()?.type?.getDisplayString()) .map((m) => m.computeConstantValue()?.type?.getDisplayString())
@@ -49,6 +100,9 @@ class AnnotationValidator {
.toList(); .toList();
} }
/// Validates that mutually exclusive method annotations are not used together.
///
/// For example, `@instance` and `@provide` cannot both be present.
static void _validateMutuallyExclusiveAnnotations( static void _validateMutuallyExclusiveAnnotations(
MethodElement method, MethodElement method,
List<String> annotations, 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( static void _validateAnnotationCombinations(
MethodElement method, MethodElement method,
List<String> annotations, List<String> annotations,
@@ -105,6 +163,7 @@ class AnnotationValidator {
} }
} }
/// Singleton-specific method annotation checks.
static void _validateSingletonUsage( static void _validateSingletonUsage(
MethodElement method, MethodElement method,
List<String> annotations, 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) { static void _validateAnnotationParameters(MethodElement method) {
// Validate @named annotation parameters // Validate @named annotation parameters
final namedValue = MetadataUtils.getNamedValue(method.metadata); final namedValue = MetadataUtils.getNamedValue(method.metadata);
@@ -170,11 +230,11 @@ class AnnotationValidator {
} }
} }
/// Checks that @params is used with compatible parameter type.
static void _validateParamsParameter( static void _validateParamsParameter(
ParameterElement param, MethodElement method) { ParameterElement param, MethodElement method) {
// @params parameter should typically be dynamic or Map<String, dynamic> // @params parameter should typically be dynamic or Map<String, dynamic>
final paramType = param.type.getDisplayString(); final paramType = param.type.getDisplayString();
if (paramType != 'dynamic' && if (paramType != 'dynamic' &&
paramType != 'Map<String, dynamic>' && paramType != 'Map<String, 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( static void _validateInjectFieldAnnotations(
FieldElement field, FieldElement field,
List<String> annotations, 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( static void _validateModuleClassAnnotations(
ClassElement classElement, ClassElement classElement,
List<String> annotations, List<String> annotations,
@@ -268,6 +330,7 @@ class AnnotationValidator {
} }
} }
/// Checks @injectable usage on classes and their fields.
static void _validateInjectableClassAnnotations( static void _validateInjectableClassAnnotations(
ClassElement classElement, ClassElement classElement,
List<String> annotations, 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 DI provider/factory/binding method,
/// Describes a single parameter for a provider/binding method in the DI system. /// specifying how that parameter is to be resolved in generated code.
/// 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
/// ///
/// RUSSIAN /// Stores the parameter's type name, optional `@named` identifier (for named DI resolution),
/// Описывает один параметр метода в DI, и его способ разрешения из контейнера. /// and a flag for runtime (@params) arguments. Used in CherryPick generator
/// Сохраняет имя типа, дополнительное имя (если параметр аннотирован через @named), /// for creating argument lists when invoking factories or provider methods.
/// и признак runtime-параметра (@params). ///
/// Для обычной зависимости типа (например, SomeDep) генерирует строку вида: /// ## Example usage
/// currentScope.resolve<SomeDep>() /// ```dart
/// Для зависимости с именем: /// // Binding method: @provide() Logger provideLogger(@named('debug') Config config, @params Map<String, dynamic> args)
/// currentScope.resolve<SomeDep>(named: 'имя') /// final namedParam = BindParameterSpec('Config', 'debug');
/// Для runtime-параметра: /// final runtimeParam = BindParameterSpec('Map<String, dynamic>', null, isParams: true);
/// args /// 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 { class BindParameterSpec {
/// Type name of the parameter (e.g. SomeService) /// The type name of the parameter (e.g., 'UserRepository')
/// Имя типа параметра (например, SomeService)
final String typeName; final String typeName;
/// Optional name for named resolution (from @named) /// If non-null, this is the named-key for DI resolution (from @named).
/// Необязательное имя для разрешения по имени (если аннотировано через @named)
final String? named; final String? named;
/// True if this parameter uses @params and should be provided from runtime args /// True if this parameter is a runtime param (annotated with @params and
/// Признак, что параметр — runtime (через @params) /// filled by a runtime argument map).
final bool isParams; final bool isParams;
BindParameterSpec(this.typeName, this.named, {this.isParams = false}); BindParameterSpec(this.typeName, this.named, {this.isParams = false});
/// -------------------------------------------------------------------------- /// Generates Dart code to resolve this parameter in the DI container.
/// generateArg
/// ///
/// ENGLISH /// - For normal dependencies: resolves by type
/// Generates Dart code for resolving the dependency from the DI scope, /// - For named dependencies: resolves by type and name
/// considering type, named, and param-argument. /// - For @params: uses the supplied params variable (default 'args')
/// ///
/// RUSSIAN /// ## Example
/// Генерирует строку для получения зависимости из DI scope (с учётом имени /// ```dart
/// и типа параметра или runtime-режима @params). /// 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']) { String generateArg([String paramsVar = 'args']) {
if (isParams) { if (isParams) {
return paramsVar; return paramsVar;

View File

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

View File

@@ -14,10 +14,36 @@
import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/element.dart';
import 'package:source_gen/source_gen.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 { class CherryPickGeneratorException extends InvalidGenerationSourceError {
/// A string describing the error category (for grouping).
final String category; final String category;
/// An optional suggestion string for resolving the error.
final String? suggestion; final String? suggestion;
/// Arbitrary key-value map for additional debugging information.
final Map<String, dynamic>? context; final Map<String, dynamic>? context;
CherryPickGeneratorException( CherryPickGeneratorException(
@@ -50,7 +76,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
buffer.writeln(' Type: ${element.runtimeType}'); buffer.writeln(' Type: ${element.runtimeType}');
buffer.writeln(' Location: ${element.source?.fullName ?? 'unknown'}'); 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 { try {
final enclosing = (element as dynamic).enclosingElement; final enclosing = (element as dynamic).enclosingElement;
if (enclosing != null) { if (enclosing != null) {
@@ -60,7 +86,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
// Ignore if enclosingElement is not available // Ignore if enclosingElement is not available
} }
// Additional context // Arbitrary user context
if (context != null && context.isNotEmpty) { if (context != null && context.isNotEmpty) {
buffer.writeln(''); buffer.writeln('');
buffer.writeln('Additional Context:'); buffer.writeln('Additional Context:');
@@ -69,7 +95,7 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
}); });
} }
// Suggestion // Hint/suggestion if present
if (suggestion != null) { if (suggestion != null) {
buffer.writeln(''); buffer.writeln('');
buffer.writeln('💡 Suggestion: $suggestion'); 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 { class AnnotationValidationException extends CherryPickGeneratorException {
AnnotationValidationException( AnnotationValidationException(
super.message, { super.message, {
@@ -89,6 +132,24 @@ class AnnotationValidationException extends CherryPickGeneratorException {
}) : super(category: 'ANNOTATION_VALIDATION'); }) : 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 { class TypeParsingException extends CherryPickGeneratorException {
TypeParsingException( TypeParsingException(
super.message, { super.message, {
@@ -98,6 +159,23 @@ class TypeParsingException extends CherryPickGeneratorException {
}) : super(category: 'TYPE_PARSING'); }) : 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 { class CodeGenerationException extends CherryPickGeneratorException {
CodeGenerationException( CodeGenerationException(
super.message, { super.message, {
@@ -107,6 +185,23 @@ class CodeGenerationException extends CherryPickGeneratorException {
}) : super(category: 'CODE_GENERATION'); }) : 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 { class DependencyResolutionException extends CherryPickGeneratorException {
DependencyResolutionException( DependencyResolutionException(
super.message, { super.message, {

View File

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

View File

@@ -14,30 +14,32 @@
import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/element.dart';
/// --------------------------------------------------------------------------- /// ---------------------------------------------------------------------------
/// MetadataUtils -- utilities for analyzing method and parameter annotations. /// MetadataUtils
/// ///
/// ENGLISH /// Static utilities for querying and extracting information from
/// Provides static utility methods to analyze Dart annotations on methods or /// Dart annotations ([ElementAnnotation]) in the context of code generation,
/// parameters. For instance, helps to find if an element is annotated with /// such as checking for the presence of specific DI-related annotations.
/// `@named()`, `@singleton()`, or other meta-annotations used in this DI framework. /// Designed to be used internally by code generation and validation routines.
/// ///
/// RUSSIAN /// # Example usage
/// Утилиты для разбора аннотаций методов и параметров. /// ```dart
/// Позволяют находить наличие аннотаций, например, @named() и @singleton(), /// 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 { class MetadataUtils {
/// ------------------------------------------------------------------------- /// Checks whether any annotation in [meta] matches the [typeName]
/// anyMeta /// (type name is compared in a case-insensitive manner and can be partial).
/// ///
/// ENGLISH /// Returns true if an annotation (such as @singleton, @provide, @named) is found.
/// Checks if any annotation in the list has a type name that contains
/// [typeName] (case insensitive).
/// ///
/// RUSSIAN /// Example:
/// Проверяет: есть ли среди аннотаций метка, имя которой содержит [typeName] /// ```dart
/// (регистр не учитывается). /// bool isSingleton = MetadataUtils.anyMeta(myMethod.metadata, 'singleton');
/// ------------------------------------------------------------------------- /// ```
static bool anyMeta(List<ElementAnnotation> meta, String typeName) { static bool anyMeta(List<ElementAnnotation> meta, String typeName) {
return meta.any((m) => return meta.any((m) =>
m m
@@ -49,17 +51,15 @@ class MetadataUtils {
false); false);
} }
/// ------------------------------------------------------------------------- /// Extracts the string value from a `@named('value')` annotation if present in [meta].
/// getNamedValue
/// ///
/// ENGLISH /// Returns the named value or `null` if not annotated.
/// Retrieves the value from a `@named('value')` annotation if present.
/// Returns the string value or null if not found.
/// ///
/// RUSSIAN /// Example:
/// Находит значение из аннотации @named('значение'). /// ```dart
/// Возвращает строку значения, если аннотация присутствует; иначе null. /// // For: @named('dev') ApiClient provideApi() ...
/// ------------------------------------------------------------------------- /// final named = MetadataUtils.getNamedValue(method.metadata); // 'dev'
/// ```
static String? getNamedValue(List<ElementAnnotation> meta) { static String? getNamedValue(List<ElementAnnotation> meta) {
for (final m in meta) { for (final m in meta) {
final cv = m.computeConstantValue(); 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 'package:analyzer/dart/element/type.dart';
import 'exceptions.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 { 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) { static ParsedType parseType(DartType dartType, Element context) {
try { try {
return _parseTypeInternal(dartType, context); return _parseTypeInternal(dartType, context);
@@ -49,7 +75,7 @@ class TypeParser {
return _parseGenericType(dartType, context, isNullable); return _parseGenericType(dartType, context, isNullable);
} }
// Simple type // Simple type (non-generic, non-Future)
return ParsedType( return ParsedType(
displayString: displayString, displayString: displayString,
coreType: displayString.replaceAll('?', ''), 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) { static void validateInjectableType(ParsedType parsedType, Element context) {
// Check for void type // Check for void type
if (parsedType.coreType == 'void') { 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 { class ParsedType {
/// The full display string of the type (e.g., "Future<List<String>?>") /// The full display string of the type (e.g., "Future<List<String>?>")
final String displayString; final String displayString;
@@ -139,19 +173,19 @@ class ParsedType {
/// The core type name without nullability and Future wrapper (e.g., "List<String>") /// The core type name without nullability and Future wrapper (e.g., "List<String>")
final String coreType; final String coreType;
/// Whether the type is nullable /// True if nullable (has `?`)
final bool isNullable; final bool isNullable;
/// Whether the type is wrapped in Future /// True if this type is a `Future<T>`
final bool isFuture; final bool isFuture;
/// Whether the type has generic parameters /// True if the type is a generic type (`List<T>`)
final bool isGeneric; final bool isGeneric;
/// Parsed type arguments for generic types /// List of parsed type arguments in generics, if any.
final List<ParsedType> typeArguments; final List<ParsedType> typeArguments;
/// For Future types, the inner type /// For `Future<T>`, this is the type inside the `Future`.
final ParsedType? innerType; final ParsedType? innerType;
const ParsedType({ const ParsedType({
@@ -164,7 +198,11 @@ class ParsedType {
this.innerType, 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 { String get codeGenType {
if (isFuture && innerType != null) { if (isFuture && innerType != null) {
return innerType!.codeGenType; return innerType!.codeGenType;
@@ -179,10 +217,10 @@ class ParsedType {
return coreType; 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; bool get shouldUseTryResolve => isNullable;
/// Returns the appropriate resolve method name /// Returns the method name for DI, e.g. "resolve", "tryResolveAsync", etc.
String get resolveMethodName { String get resolveMethodName {
if (isFuture) { if (isFuture) {
return shouldUseTryResolve ? 'tryResolveAsync' : 'resolveAsync'; return shouldUseTryResolve ? 'tryResolveAsync' : 'resolveAsync';