mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-23 21:13:35 +00:00
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:
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user