Files
cherrypick/cherrypick_generator/lib/module_generator.dart

308 lines
12 KiB
Dart
Raw Normal View History

2025-05-18 15:43:02 +03:00
//
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
2025-05-17 00:34:56 +03:00
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
2025-05-17 22:29:37 +03:00
///
2025-05-20 19:50:13 +03:00
/// Генератор зависимостей для DI-контейнера на основе аннотаций.
///
/// Данный генератор автоматически создаёт код для внедрения зависимостей (DI)
/// на основе аннотаций в вашем исходном коде. Когда вы отмечаете класс
/// аннотацией `@module()`, этот генератор обработает все его публичные методы
/// и автоматически сгенерирует класс с биндингами (регистрациями зависимостей)
/// для DI-контейнера. Это избавляет от написания однообразного шаблонного кода.
2025-05-17 22:29:37 +03:00
///
2025-05-17 00:34:56 +03:00
class ModuleGenerator extends GeneratorForAnnotation<ann.module> {
2025-05-20 19:50:13 +03:00
/// Генерирует исходный код для класса-модуля с аннотацией `@module()`.
///
/// [element] — исходный класс, помеченный аннотацией.
/// [annotation] — значения параметров аннотации.
/// [buildStep] — информация о текущем шаге генерации.
2025-05-17 00:34:56 +03:00
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
2025-05-20 19:50:13 +03:00
// Генератор обрабатывает только классы (остальное — ошибка)
2025-05-17 00:34:56 +03:00
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@module() может быть применён только к классам.',
element: element,
);
}
2025-05-17 22:29:37 +03:00
2025-05-17 00:34:56 +03:00
final classElement = element;
2025-05-17 22:29:37 +03:00
2025-05-20 19:50:13 +03:00
// Создаёт объект, описывающий, какие биндинги нужно сгенерировать на основании методов класса
final generatedClass = _GeneratedClass.fromClassElement(classElement);
// Генерирует итоговый Dart-код
return generatedClass.generate();
}
}
///
/// Описывает параметры для создания одного биндинга зависимости (binding spec).
///
/// Каждый биндинг соответствует одному публичному методу класса-модуля.
///
class BindSpec {
/// Тип, который предоставляет биндинг (например, SomeService)
final String returnType;
/// Имя метода, который реализует биндинг
final String methodName;
/// Необязательное имя, для именованной зависимости (используется с @named)
final String? named;
/// Является ли зависимость синглтоном (имеется ли аннотация @singleton)
final bool isSingleton;
/// Список параметров, которые требуются методу для внедрения зависимостей
final List<BindParameterSpec> parameters;
final String bindingType; // 'instance' | 'provide'
2025-05-21 10:40:21 +03:00
final bool isAsyncInstance;
2025-05-20 19:50:13 +03:00
BindSpec({
required this.returnType,
required this.methodName,
required this.isSingleton,
required this.parameters,
this.named,
required this.bindingType,
2025-05-21 10:40:21 +03:00
required this.isAsyncInstance,
2025-05-20 19:50:13 +03:00
});
/// Формирует dart-код для биндинга, например:
/// bind<Type>().toProvide(() => method(args)).withName('name').singleton();
///
/// Параметр [indent] задаёт отступ для красивого форматирования кода.
String generateBind(int indent) {
final indentStr = ' ' * indent;
// Собираем строку аргументов для вызова метода
final argsStr = parameters.map((p) => p.generateArg()).join(', ');
// Если аргументов много или они длинные — разбиваем вызов на несколько строк
//final needMultiline = argsStr.length > 60 || argsStr.contains('\n');
2025-05-20 19:50:13 +03:00
2025-05-21 10:40:21 +03:00
String provide;
if (bindingType == 'instance') {
// Добавляем async вариант для Future<T>
if (isAsyncInstance) {
provide = '.toInstanceAsync($methodName($argsStr))';
} else {
provide = '.toInstance($methodName($argsStr))';
}
} else {
provide = (argsStr.length > 60 || argsStr.contains('\n'))
? '.toProvide(\n${' ' * (indent + 2)}() => $methodName($argsStr))'
: '.toProvide(() => $methodName($argsStr))';
}
2025-05-20 19:50:13 +03:00
final namePart = named != null ? ".withName('$named')" : '';
final singletonPart = isSingleton ? '.singleton()' : '';
// Итоговый bind: bind<Type>().toProvide(...).withName(...).singleton();
return '$indentStr'
'bind<$returnType>()'
'$provide'
'$namePart'
'$singletonPart;';
// Всегда заканчиваем точкой с запятой!
}
/// Создаёт спецификацию биндинга (BindSpec) из метода класса-модуля
static BindSpec fromMethod(MethodElement method) {
2025-05-21 10:40:21 +03:00
var returnType = method.returnType.getDisplayString();
2025-05-20 19:50:13 +03:00
final methodName = method.displayName;
// Проверим, помечен ли метод аннотацией @singleton
final isSingleton = _MetadataUtils.anyMeta(method.metadata, 'singleton');
// Получаем имя из @named(), если есть
final named = _MetadataUtils.getNamedValue(method.metadata);
// Для каждого параметра метода
final params = <BindParameterSpec>[];
for (final p in method.parameters) {
final typeStr = p.type.getDisplayString();
final paramNamed = _MetadataUtils.getNamedValue(p.metadata);
params.add(BindParameterSpec(typeStr, paramNamed));
}
// определяем bindingType
final hasInstance = _MetadataUtils.anyMeta(method.metadata, 'instance');
final hasProvide = _MetadataUtils.anyMeta(method.metadata, 'provide');
if (!hasInstance && !hasProvide) {
throw InvalidGenerationSourceError(
'Метод $methodName класса-модуля должен быть помечен либо @instance(), либо @provide().',
element: method,
);
}
final bindingType = hasInstance ? 'instance' : 'provide';
2025-05-21 10:40:21 +03:00
// Новый кусок — для async instance возвращаем базовый тип без Future<>
bool isAsyncInstance = false;
if (bindingType == 'instance' && returnType.startsWith('Future<')) {
final futureMatch = RegExp(r'^Future<(.+)>$').firstMatch(returnType);
if (futureMatch != null) {
returnType = futureMatch.group(1)!.trim();
isAsyncInstance = true;
}
}
2025-05-20 19:50:13 +03:00
return BindSpec(
returnType: returnType,
methodName: methodName,
isSingleton: isSingleton,
named: named,
parameters: params,
bindingType: bindingType,
2025-05-21 10:40:21 +03:00
isAsyncInstance: isAsyncInstance,
2025-05-20 19:50:13 +03:00
);
}
}
///
/// Описывает один параметр метода и возможность его разрешения из контейнера.
///
/// Например, если метод принимает SomeDep dep, то
/// BindParameterSpec хранит тип SomeDep, а generateArg отдаст строку
/// currentScope.resolve<SomeDep>()
///
class BindParameterSpec {
/// Имя типа параметра (например, SomeService)
final String typeName;
/// Необязательное имя для разрешения по имени (если аннотировано через @named)
final String? named;
BindParameterSpec(this.typeName, this.named);
/// Генерирует строку для получения зависимости из DI scope (с учётом имени)
String generateArg() {
if (named != null) {
return "currentScope.resolve<$typeName>(named: '$named')";
}
return "currentScope.resolve<$typeName>()";
}
}
///
/// Результат обработки одного класса-модуля: имя класса, его биндинги,
/// имя генерируемого класса и т.д.
///
class _GeneratedClass {
/// Имя исходного класса-модуля
final String className;
/// Имя генерируемого класса (например, $SomeModule)
final String generatedClassName;
/// Список всех обнаруженных биндингов
final List<BindSpec> binds;
_GeneratedClass(
this.className,
this.generatedClassName,
this.binds,
);
/// Обрабатывает объект ClassElement (отображение класса в AST)
/// и строит структуру _GeneratedClass для генерации кода.
static _GeneratedClass fromClassElement(ClassElement element) {
final className = element.displayName;
// Имя с префиксом $ (стандартная практика для ген-кода)
2025-05-17 00:34:56 +03:00
final generatedClassName = r'$' + className;
2025-05-20 19:50:13 +03:00
// Собираем биндинги по всем методам класса, игнорируем абстрактные (без реализации)
final binds = element.methods
.where((m) => !m.isAbstract)
.map(BindSpec.fromMethod)
.toList();
return _GeneratedClass(className, generatedClassName, binds);
}
2025-05-17 22:29:37 +03:00
2025-05-20 19:50:13 +03:00
/// Генерирует исходный Dart-код для созданного класса DI-модуля.
///
/// Внутри builder(Scope currentScope) регистрируются все bind-методы.
String generate() {
2025-05-17 00:34:56 +03:00
final buffer = StringBuffer();
2025-05-17 22:29:37 +03:00
2025-05-17 00:34:56 +03:00
buffer.writeln('final class $generatedClassName extends $className {');
2025-05-20 19:50:13 +03:00
buffer.writeln(' @override');
buffer.writeln(' void builder(Scope currentScope) {');
2025-05-17 21:38:14 +03:00
2025-05-20 19:50:13 +03:00
// Для каждого биндинга — генерируем строку bind<Type>()...
for (final bind in binds) {
buffer.writeln(bind.generateBind(4));
2025-05-17 00:34:56 +03:00
}
2025-05-17 22:29:37 +03:00
2025-05-20 19:50:13 +03:00
buffer.writeln(' }');
buffer.writeln('}');
2025-05-17 21:38:14 +03:00
2025-05-17 00:34:56 +03:00
return buffer.toString();
}
}
2025-05-20 19:50:13 +03:00
///
/// Утилиты для разбора аннотаций методов и параметров.
/// Позволяют найти @named() и @singleton() у метода/параметра.
///
class _MetadataUtils {
/// Проверяет: есть ли среди аннотаций метка, имя которой содержит [typeName]
/// (регистр не учитывается)
static bool anyMeta(List<ElementAnnotation> meta, String typeName) {
return meta.any((m) =>
m
.computeConstantValue()
?.type
?.getDisplayString()
.toLowerCase()
.contains(typeName.toLowerCase()) ??
false);
}
/// Находит значение из аннотации @named('значение').
/// Возвращает строку значения, если аннотация присутствует; иначе null.
static String? getNamedValue(List<ElementAnnotation> meta) {
for (final m in meta) {
final cv = m.computeConstantValue();
final typeStr = cv?.type?.getDisplayString().toLowerCase();
if (typeStr?.contains('named') ?? false) {
return cv?.getField('value')?.toStringValue();
}
}
return null;
}
}
///
/// Точка входа для генератора build_runner.
/// Возвращает Builder, используемый build_runner для генерации кода для всех
/// файлов, где встречается @module().
///
2025-05-17 00:34:56 +03:00
Builder moduleBuilder(BuilderOptions options) =>
PartBuilder([ModuleGenerator()], '.cherrypick.g.dart');