Files
cherrypick/cherrypick_generator/lib/inject_generator.dart

187 lines
6.9 KiB
Dart
Raw Normal View History

2025-05-23 16:10:09 +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-23 12:21:23 +03:00
import 'dart:async';
2025-05-23 14:08:08 +03:00
import 'package:analyzer/dart/constant/value.dart';
2025-05-23 15:26:09 +03:00
import 'package:analyzer/dart/element/type.dart';
2025-05-23 12:21:23 +03:00
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
2025-05-23 16:10:09 +03:00
/// InjectGenerator generates a mixin for a class marked with @injectable()
/// and injects all fields annotated with @inject(), using CherryPick DI.
///
/// For Future<T> fields it calls .resolveAsync<T>(),
/// otherwise .resolve<T>() is used. Scope and named qualifiers are supported.
///
/// ---
///
/// InjectGenerator генерирует миксин для класса с аннотацией @injectable()
/// и внедряет все поля, помеченные @inject(), используя DI-фреймворк CherryPick.
///
/// Для Future<T> полей вызывается .resolveAsync<T>(),
/// для остальных — .resolve<T>(). Поддерживаются scope и named qualifier.
///
2025-05-23 12:21:23 +03:00
class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
const InjectGenerator();
2025-05-23 16:10:09 +03:00
/// The main entry point for code generation.
///
/// Checks class validity, collects injectable fields, and produces injection code.
///
/// Основная точка входа генератора. Проверяет класс, собирает инъектируемые поля и создает код внедрения зависимостей.
2025-05-23 12:21:23 +03:00
@override
FutureOr<String> generateForAnnotatedElement(
2025-05-23 16:03:29 +03:00
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
2025-05-23 12:21:23 +03:00
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@injectable() can only be applied to classes.',
element: element,
);
}
final classElement = element;
final className = classElement.name;
final mixinName = '_\$$className';
2025-05-23 16:03:29 +03:00
final buffer = StringBuffer()
..writeln('mixin $mixinName {')
..writeln(' void _inject($className instance) {');
2025-05-23 14:08:08 +03:00
2025-05-23 16:10:09 +03:00
// Collect and process all @inject fields.
// Собираем и обрабатываем все поля с @inject.
2025-05-23 16:03:29 +03:00
final injectFields =
classElement.fields.where(_isInjectField).map(_parseInjectField);
for (final parsedField in injectFields) {
buffer.writeln(_generateInjectionLine(parsedField));
}
buffer
..writeln(' }')
..writeln('}');
2025-05-23 15:26:09 +03:00
2025-05-23 16:03:29 +03:00
return buffer.toString();
}
2025-05-23 14:08:08 +03:00
2025-05-23 16:10:09 +03:00
/// Checks if a field has the @inject annotation.
///
/// Проверяет, отмечено ли поле аннотацией @inject.
2025-05-23 16:03:29 +03:00
static bool _isInjectField(FieldElement field) {
return field.metadata.any(
(m) => m.computeConstantValue()?.type?.getDisplayString() == 'inject',
);
}
2025-05-23 14:08:08 +03:00
2025-05-23 16:10:09 +03:00
/// Parses the field for scope/named qualifiers and determines its type.
/// Returns a [_ParsedInjectField] describing injection information.
///
/// Разбирает поле на наличие модификаторов scope/named и выясняет его тип.
/// Возвращает [_ParsedInjectField] с информацией о внедрении.
2025-05-23 16:03:29 +03:00
static _ParsedInjectField _parseInjectField(FieldElement field) {
String? scopeName;
String? namedValue;
for (final meta in field.metadata) {
final DartObject? obj = meta.computeConstantValue();
final type = obj?.type?.getDisplayString();
if (type == 'scope') {
scopeName = obj?.getField('name')?.toStringValue();
} else if (type == 'named') {
namedValue = obj?.getField('value')?.toStringValue();
}
2025-05-23 12:21:23 +03:00
}
2025-05-23 16:03:29 +03:00
final DartType dartType = field.type;
String coreTypeName;
bool isFuture;
2025-05-23 12:21:23 +03:00
2025-05-23 16:03:29 +03:00
if (dartType.isDartAsyncFuture) {
final ParameterizedType paramType = dartType as ParameterizedType;
coreTypeName = paramType.typeArguments.first.getDisplayString();
isFuture = true;
} else {
coreTypeName = dartType.getDisplayString();
isFuture = false;
}
return _ParsedInjectField(
fieldName: field.name,
coreType: coreTypeName,
isFuture: isFuture,
scopeName: scopeName,
namedValue: namedValue,
);
2025-05-23 12:21:23 +03:00
}
2025-05-23 16:03:29 +03:00
2025-05-23 16:10:09 +03:00
/// Generates a line of code that performs the dependency injection for a field.
/// Handles resolve/resolveAsync, scoping, and named qualifiers.
///
/// Генерирует строку кода, которая внедряет зависимость для поля.
/// Учитывает resolve/resolveAsync, scoping и named qualifier.
2025-05-23 16:03:29 +03:00
String _generateInjectionLine(_ParsedInjectField field) {
final methodName = field.isFuture
? 'resolveAsync<${field.coreType}>'
: 'resolve<${field.coreType}>';
final openCall = (field.scopeName != null && field.scopeName!.isNotEmpty)
? "CherryPick.openScope(scopeName: '${field.scopeName}')"
: "CherryPick.openRootScope()";
final params = (field.namedValue != null && field.namedValue!.isNotEmpty)
? "(named: '${field.namedValue}')"
: '()';
return " instance.${field.fieldName} = $openCall.$methodName$params;";
}
}
2025-05-23 16:10:09 +03:00
/// Data structure representing all information required to generate
/// injection code for a field.
///
/// Структура данных, содержащая всю информацию,
/// необходимую для генерации кода внедрения для поля.
2025-05-23 16:03:29 +03:00
class _ParsedInjectField {
2025-05-23 16:10:09 +03:00
/// The name of the field / Имя поля.
2025-05-23 16:03:29 +03:00
final String fieldName;
2025-05-23 16:10:09 +03:00
/// The base type name (T or Future<T>) / Базовый тип (T или тип из Future<T>).
2025-05-23 16:03:29 +03:00
final String coreType;
2025-05-23 16:10:09 +03:00
/// True if the field type is Future<T>; false otherwise
/// Истина, если поле — Future<T>, иначе — ложь.
2025-05-23 16:03:29 +03:00
final bool isFuture;
2025-05-23 16:10:09 +03:00
/// Optional scope annotation argument / Опциональное имя scope.
2025-05-23 16:03:29 +03:00
final String? scopeName;
2025-05-23 16:10:09 +03:00
/// Optional named annotation argument / Опциональное имя named.
2025-05-23 16:03:29 +03:00
final String? namedValue;
_ParsedInjectField({
required this.fieldName,
required this.coreType,
required this.isFuture,
this.scopeName,
this.namedValue,
});
2025-05-23 12:21:23 +03:00
}
2025-05-23 16:10:09 +03:00
/// Builder factory. Used by build_runner.
///
/// Фабрика билдера. Используется build_runner.
2025-05-23 12:21:23 +03:00
Builder injectBuilder(BuilderOptions options) =>
PartBuilder([InjectGenerator()], '.inject.cherrypick.g.dart');