Files
cherrypick/cherrypick_generator/lib/module_generator.dart
2025-05-19 16:12:45 +03:00

176 lines
6.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// Генератор DI-модулей для фреймворка cherrypick.
/// Генерирует расширение для класса с аннотацией @module,
/// автоматически создавая биндинги для зависимостей, определённых в модуле.
///
/// Пример:
/// ```dart
/// @module()
/// abstract class AppModule extends Module {
/// @singleton()
/// Dio dio() => Dio();
/// }
/// ```
///
/// Сгенерирует код:
/// ```dart
/// final class $AppModule extends AppModule {
/// @override
/// void builder(Scope currentScope) {
/// bind<Dio>().toProvide(() => dio()).singleton();
/// }
/// }
/// ```
class ModuleGenerator extends GeneratorForAnnotation<ann.module> {
/// Основной метод генерации кода для аннотированного класса-модуля.
/// [element] - класс с аннотацией @module.
/// [annotation], [buildStep] - служебные параметры build_runner.
/// Возвращает сгенерированный Dart-код класса-расширения.
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
// Убеждаемся, что аннотирован только класс (не функция, не переменная).
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@module() может быть применён только к классам.',
element: element,
);
}
final classElement = element;
final className = classElement.displayName;
// Имя сгенерированного класса (например: $AppModule)
final generatedClassName = r'$' + className;
final buffer = StringBuffer();
// Объявление генерируемого класса как final, который наследуется от исходного модуля
buffer.writeln('final class $generatedClassName extends $className {');
buffer.writeln(' @override');
buffer.writeln(' void builder(Scope currentScope) {');
// Обрабатываем все НЕ-абстрактные методы модуля
for (final method in classElement.methods.where((m) => !m.isAbstract)) {
// Проверка на наличие аннотации @singleton() у метода
final hasSingleton = method.metadata.any(
(m) =>
m
.computeConstantValue()
?.type
?.getDisplayString(withNullability: false)
.toLowerCase()
.contains('singleton') ??
false,
);
// Проверяем, есть ли у метода @named('...')
ElementAnnotation? namedMeta;
try {
namedMeta = method.metadata.firstWhere(
(m) =>
m
.computeConstantValue()
?.type
?.getDisplayString(withNullability: false)
.toLowerCase()
.contains('named') ??
false,
);
} catch (_) {
namedMeta = null;
}
// Извлекаем значение name из @named('value')
String? nameArg;
if (namedMeta != null) {
final cv = namedMeta.computeConstantValue();
if (cv != null) {
nameArg = cv.getField('value')?.toStringValue();
}
}
// Формируем список аргументов для вызова метода.
// Каждый параметр может быть с аннотацией @named, либо без.
final args = method.parameters.map((p) {
// Проверяем наличие @named('value') на параметре
ElementAnnotation? paramNamed;
try {
paramNamed = p.metadata.firstWhere(
(m) =>
m
.computeConstantValue()
?.type
?.getDisplayString(withNullability: false)
.toLowerCase()
.contains('named') ??
false,
);
} catch (_) {
paramNamed = null;
}
String argExpr;
if (paramNamed != null) {
final cv = paramNamed.computeConstantValue();
final namedValue = cv?.getField('value')?.toStringValue();
// Если указано имя для параметра (@named), пробрасываем его в resolve
if (namedValue != null) {
argExpr =
"currentScope.resolve<${p.type.getDisplayString(withNullability: false)}>(named: '$namedValue')";
} else {
// fallback — скобки все равно нужны
argExpr =
"currentScope.resolve<${p.type.getDisplayString(withNullability: false)}>()";
}
} else {
// Если параметр не @named - просто resolve по типу
argExpr =
"currentScope.resolve<${p.type.getDisplayString(withNullability: false)}>()";
}
return argExpr;
}).join(', ');
final returnType =
method.returnType.getDisplayString(withNullability: false);
final methodName = method.displayName;
// Если список параметров длинный — переносим вызов на новую строку для читаемости.
final hasLongArgs = args.length > 60 || args.contains('\n');
if (hasLongArgs) {
buffer.write(' bind<$returnType>()\n'
' .toProvide(\n () => $methodName($args))');
} else {
buffer.write(' bind<$returnType>()'
'.toProvide(() => $methodName($args))');
}
// Применяем имя биндера если есть @named
if (nameArg != null) {
buffer.write(".withName('$nameArg')");
}
// Применяем singleton если был @singleton
if (hasSingleton) {
buffer.write('.singleton()');
}
buffer.write(';\n');
}
buffer.writeln(' }');
buffer.writeln('}');
return buffer.toString();
}
}
/// Фабрика Builder-го класса для build_runner
/// Производит .cherrypick.g.dart файл для каждого модуля.
Builder moduleBuilder(BuilderOptions options) =>
PartBuilder([ModuleGenerator()], '.cherrypick.g.dart');