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
|
|
|
|
/// Генератор 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();
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// ```
|
2025-05-17 00:34:56 +03:00
|
|
|
|
class ModuleGenerator extends GeneratorForAnnotation<ann.module> {
|
2025-05-17 22:29:37 +03:00
|
|
|
|
/// Основной метод генерации кода для аннотированного класса-модуля.
|
|
|
|
|
|
/// [element] - класс с аннотацией @module.
|
|
|
|
|
|
/// [annotation], [buildStep] - служебные параметры build_runner.
|
|
|
|
|
|
/// Возвращает сгенерированный Dart-код класса-расширения.
|
2025-05-17 00:34:56 +03:00
|
|
|
|
@override
|
|
|
|
|
|
String generateForAnnotatedElement(
|
2025-05-17 11:27:30 +03:00
|
|
|
|
Element element,
|
|
|
|
|
|
ConstantReader annotation,
|
|
|
|
|
|
BuildStep buildStep,
|
|
|
|
|
|
) {
|
2025-05-17 22:29:37 +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;
|
|
|
|
|
|
final className = classElement.displayName;
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
|
|
|
|
|
// Имя сгенерированного класса (например: $AppModule)
|
2025-05-17 00:34:56 +03:00
|
|
|
|
final generatedClassName = r'$' + className;
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
2025-05-17 00:34:56 +03:00
|
|
|
|
final buffer = StringBuffer();
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
|
|
|
|
|
// Объявление генерируемого класса как final, который наследуется от исходного модуля
|
2025-05-17 00:34:56 +03:00
|
|
|
|
buffer.writeln('final class $generatedClassName extends $className {');
|
2025-05-17 14:31:52 +03:00
|
|
|
|
buffer.writeln(' @override');
|
|
|
|
|
|
buffer.writeln(' void builder(Scope currentScope) {');
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
|
|
|
|
|
// Обрабатываем все НЕ-абстрактные методы модуля
|
2025-05-17 00:34:56 +03:00
|
|
|
|
for (final method in classElement.methods.where((m) => !m.isAbstract)) {
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// Проверка на наличие аннотации @singleton() у метода
|
2025-05-17 00:34:56 +03:00
|
|
|
|
final hasSingleton = method.metadata.any(
|
|
|
|
|
|
(m) =>
|
|
|
|
|
|
m
|
|
|
|
|
|
.computeConstantValue()
|
|
|
|
|
|
?.type
|
2025-05-18 13:50:14 +03:00
|
|
|
|
?.getDisplayString()
|
2025-05-17 00:34:56 +03:00
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
.contains('singleton') ??
|
|
|
|
|
|
false,
|
|
|
|
|
|
);
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
|
|
|
|
|
// Проверяем, есть ли у метода @named('...')
|
2025-05-17 14:31:52 +03:00
|
|
|
|
ElementAnnotation? namedMeta;
|
|
|
|
|
|
try {
|
|
|
|
|
|
namedMeta = method.metadata.firstWhere(
|
|
|
|
|
|
(m) =>
|
|
|
|
|
|
m
|
|
|
|
|
|
.computeConstantValue()
|
|
|
|
|
|
?.type
|
2025-05-18 13:50:14 +03:00
|
|
|
|
?.getDisplayString()
|
2025-05-17 14:31:52 +03:00
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
.contains('named') ??
|
|
|
|
|
|
false,
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
namedMeta = null;
|
|
|
|
|
|
}
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
|
|
|
|
|
// Извлекаем значение name из @named('value')
|
2025-05-17 14:31:52 +03:00
|
|
|
|
String? nameArg;
|
|
|
|
|
|
if (namedMeta != null) {
|
|
|
|
|
|
final cv = namedMeta.computeConstantValue();
|
|
|
|
|
|
if (cv != null) {
|
|
|
|
|
|
nameArg = cv.getField('value')?.toStringValue();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-17 21:38:14 +03:00
|
|
|
|
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// Формируем список аргументов для вызова метода.
|
|
|
|
|
|
// Каждый параметр может быть с аннотацией @named, либо без.
|
2025-05-17 21:38:14 +03:00
|
|
|
|
final args = method.parameters.map((p) {
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// Проверяем наличие @named('value') на параметре
|
2025-05-17 21:38:14 +03:00
|
|
|
|
ElementAnnotation? paramNamed;
|
|
|
|
|
|
try {
|
|
|
|
|
|
paramNamed = p.metadata.firstWhere(
|
|
|
|
|
|
(m) =>
|
|
|
|
|
|
m
|
|
|
|
|
|
.computeConstantValue()
|
|
|
|
|
|
?.type
|
2025-05-18 13:50:14 +03:00
|
|
|
|
?.getDisplayString()
|
2025-05-17 21:38:14 +03:00
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
.contains('named') ??
|
|
|
|
|
|
false,
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
paramNamed = null;
|
|
|
|
|
|
}
|
2025-05-17 21:54:03 +03:00
|
|
|
|
|
|
|
|
|
|
String argExpr;
|
2025-05-17 21:38:14 +03:00
|
|
|
|
if (paramNamed != null) {
|
|
|
|
|
|
final cv = paramNamed.computeConstantValue();
|
2025-05-17 21:54:03 +03:00
|
|
|
|
final namedValue = cv?.getField('value')?.toStringValue();
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// Если указано имя для параметра (@named), пробрасываем его в resolve
|
2025-05-17 21:54:03 +03:00
|
|
|
|
if (namedValue != null) {
|
|
|
|
|
|
argExpr =
|
2025-05-18 13:50:14 +03:00
|
|
|
|
"currentScope.resolve<${p.type.getDisplayString()}>(named: '$namedValue')";
|
2025-05-17 21:54:03 +03:00
|
|
|
|
} else {
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// fallback — скобки все равно нужны
|
2025-05-18 13:50:14 +03:00
|
|
|
|
argExpr = "currentScope.resolve<${p.type.getDisplayString()}>()";
|
2025-05-17 21:38:14 +03:00
|
|
|
|
}
|
2025-05-17 21:54:03 +03:00
|
|
|
|
} else {
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// Если параметр не @named - просто resolve по типу
|
2025-05-18 13:50:14 +03:00
|
|
|
|
argExpr = "currentScope.resolve<${p.type.getDisplayString()}>()";
|
2025-05-17 21:38:14 +03:00
|
|
|
|
}
|
2025-05-17 21:54:03 +03:00
|
|
|
|
return argExpr;
|
2025-05-17 21:38:14 +03:00
|
|
|
|
}).join(', ');
|
|
|
|
|
|
|
2025-05-18 13:50:14 +03:00
|
|
|
|
final returnType = method.returnType.getDisplayString();
|
2025-05-17 00:34:56 +03:00
|
|
|
|
final methodName = method.displayName;
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
|
|
|
|
|
// Если список параметров длинный — переносим вызов на новую строку для читаемости.
|
2025-05-17 21:38:14 +03:00
|
|
|
|
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))');
|
|
|
|
|
|
}
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// Применяем имя биндера если есть @named
|
2025-05-17 14:31:52 +03:00
|
|
|
|
if (nameArg != null) {
|
|
|
|
|
|
buffer.write(".withName('$nameArg')");
|
|
|
|
|
|
}
|
2025-05-17 22:29:37 +03:00
|
|
|
|
// Применяем singleton если был @singleton
|
2025-05-17 11:27:30 +03:00
|
|
|
|
if (hasSingleton) {
|
|
|
|
|
|
buffer.write('.singleton()');
|
|
|
|
|
|
}
|
|
|
|
|
|
buffer.write(';\n');
|
2025-05-17 00:34:56 +03:00
|
|
|
|
}
|
2025-05-17 22:29:37 +03:00
|
|
|
|
|
2025-05-17 14:31:52 +03:00
|
|
|
|
buffer.writeln(' }');
|
2025-05-17 11:27:30 +03:00
|
|
|
|
buffer.writeln('}');
|
2025-05-17 21:38:14 +03:00
|
|
|
|
|
2025-05-17 00:34:56 +03:00
|
|
|
|
return buffer.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-17 22:29:37 +03:00
|
|
|
|
/// Фабрика Builder-го класса для build_runner
|
2025-05-19 10:08:20 +03:00
|
|
|
|
/// Генерирует .cherrypick.g.dart файл для каждого модуля.
|
2025-05-17 00:34:56 +03:00
|
|
|
|
Builder moduleBuilder(BuilderOptions options) =>
|
|
|
|
|
|
PartBuilder([ModuleGenerator()], '.cherrypick.g.dart');
|