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-07-15 16:28:05 +03:00
|
|
|
|
|
|
|
|
|
|
|
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-07-15 12:07:23 +03:00
|
|
|
|
import 'cherrypick_custom_builders.dart' as custom;
|
2025-07-15 16:03:10 +03:00
|
|
|
|
import 'src/exceptions.dart';
|
|
|
|
|
|
import 'src/type_parser.dart';
|
|
|
|
|
|
import 'src/annotation_validator.dart';
|
2025-05-23 12:21:23 +03:00
|
|
|
|
|
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) {
|
2025-07-15 16:03:10 +03:00
|
|
|
|
throw CherryPickGeneratorException(
|
|
|
|
|
|
'@injectable() can only be applied to classes',
|
2025-05-23 12:21:23 +03:00
|
|
|
|
element: element,
|
2025-07-15 16:03:10 +03:00
|
|
|
|
category: 'INVALID_TARGET',
|
|
|
|
|
|
suggestion: 'Apply @injectable() to a class instead of ${element.runtimeType}',
|
2025-05-23 12:21:23 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final classElement = element;
|
2025-07-15 16:03:10 +03:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Validate class annotations
|
|
|
|
|
|
AnnotationValidator.validateClassAnnotations(classElement);
|
|
|
|
|
|
|
|
|
|
|
|
return _generateInjectionCode(classElement);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e is CherryPickGeneratorException) {
|
|
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
throw CodeGenerationException(
|
|
|
|
|
|
'Failed to generate injection code: $e',
|
|
|
|
|
|
element: classElement,
|
|
|
|
|
|
suggestion: 'Check that all @inject fields have valid types and annotations',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Generates the injection code for a class
|
|
|
|
|
|
String _generateInjectionCode(ClassElement classElement) {
|
2025-05-23 12:21:23 +03:00
|
|
|
|
final className = classElement.name;
|
|
|
|
|
|
final mixinName = '_\$$className';
|
2025-07-15 16:03:10 +03:00
|
|
|
|
|
2025-05-23 16:10:09 +03:00
|
|
|
|
// Collect and process all @inject fields.
|
2025-07-15 16:03:10 +03:00
|
|
|
|
final injectFields = classElement.fields
|
|
|
|
|
|
.where(_isInjectField)
|
|
|
|
|
|
.map((field) => _parseInjectField(field, classElement))
|
|
|
|
|
|
.toList();
|
2025-05-23 16:03:29 +03:00
|
|
|
|
|
2025-07-15 16:03:10 +03:00
|
|
|
|
final buffer = StringBuffer()
|
|
|
|
|
|
..writeln('// dart format width=80')
|
|
|
|
|
|
..writeln('// GENERATED CODE - DO NOT MODIFY BY HAND')
|
|
|
|
|
|
..writeln()
|
|
|
|
|
|
..writeln('// **************************************************************************')
|
|
|
|
|
|
..writeln('// InjectGenerator')
|
|
|
|
|
|
..writeln('// **************************************************************************')
|
|
|
|
|
|
..writeln()
|
|
|
|
|
|
..writeln('mixin $mixinName {');
|
2025-05-23 16:03:29 +03:00
|
|
|
|
|
2025-07-15 16:03:10 +03:00
|
|
|
|
if (injectFields.isEmpty) {
|
|
|
|
|
|
// For empty classes, generate a method with empty body
|
|
|
|
|
|
buffer.writeln(' void _inject($className instance) {}');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
buffer.writeln(' void _inject($className instance) {');
|
|
|
|
|
|
for (final parsedField in injectFields) {
|
|
|
|
|
|
buffer.writeln(_generateInjectionLine(parsedField));
|
|
|
|
|
|
}
|
|
|
|
|
|
buffer.writeln(' }');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buffer.writeln('}');
|
2025-05-23 15:26:09 +03:00
|
|
|
|
|
2025-07-15 16:03:10 +03:00
|
|
|
|
return '${buffer.toString()}\n';
|
2025-05-23 16:03:29 +03:00
|
|
|
|
}
|
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-07-15 16:03:10 +03:00
|
|
|
|
static _ParsedInjectField _parseInjectField(FieldElement field, ClassElement classElement) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Validate field annotations
|
|
|
|
|
|
AnnotationValidator.validateFieldAnnotations(field);
|
|
|
|
|
|
|
|
|
|
|
|
// Parse type using improved type parser
|
|
|
|
|
|
final parsedType = TypeParser.parseType(field.type, field);
|
|
|
|
|
|
TypeParser.validateInjectableType(parsedType, field);
|
|
|
|
|
|
|
|
|
|
|
|
// Extract metadata
|
|
|
|
|
|
String? scopeName;
|
|
|
|
|
|
String? namedValue;
|
2025-05-23 12:21:23 +03:00
|
|
|
|
|
2025-07-15 16:03:10 +03:00
|
|
|
|
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-07-15 16:03:10 +03:00
|
|
|
|
return _ParsedInjectField(
|
|
|
|
|
|
fieldName: field.name,
|
|
|
|
|
|
parsedType: parsedType,
|
|
|
|
|
|
scopeName: scopeName,
|
|
|
|
|
|
namedValue: namedValue,
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e is CherryPickGeneratorException) {
|
|
|
|
|
|
rethrow;
|
|
|
|
|
|
}
|
|
|
|
|
|
throw DependencyResolutionException(
|
|
|
|
|
|
'Failed to parse inject field "${field.name}"',
|
|
|
|
|
|
element: field,
|
|
|
|
|
|
suggestion: 'Check that the field type is valid and properly imported',
|
|
|
|
|
|
context: {
|
|
|
|
|
|
'field_name': field.name,
|
|
|
|
|
|
'field_type': field.type.getDisplayString(),
|
|
|
|
|
|
'class_name': classElement.name,
|
|
|
|
|
|
'error': e.toString(),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-05-23 16:03:29 +03:00
|
|
|
|
}
|
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) {
|
2025-07-15 16:03:10 +03:00
|
|
|
|
final resolveMethod = '${field.parsedType.resolveMethodName}<${field.parsedType.codeGenType}>';
|
|
|
|
|
|
final fieldName = field.fieldName;
|
|
|
|
|
|
|
|
|
|
|
|
// Build the scope call
|
2025-05-23 16:03:29 +03:00
|
|
|
|
final openCall = (field.scopeName != null && field.scopeName!.isNotEmpty)
|
|
|
|
|
|
? "CherryPick.openScope(scopeName: '${field.scopeName}')"
|
|
|
|
|
|
: "CherryPick.openRootScope()";
|
2025-07-15 16:03:10 +03:00
|
|
|
|
|
|
|
|
|
|
// Build the parameters
|
|
|
|
|
|
final hasNamedParam = field.namedValue != null && field.namedValue!.isNotEmpty;
|
|
|
|
|
|
final params = hasNamedParam ? "(named: '${field.namedValue}')" : '()';
|
|
|
|
|
|
|
|
|
|
|
|
// Create the full line
|
|
|
|
|
|
final fullLine = " instance.$fieldName = $openCall.$resolveMethod$params;";
|
|
|
|
|
|
|
|
|
|
|
|
// Check if line is too long (dart format width=80, accounting for indentation)
|
|
|
|
|
|
if (fullLine.length <= 80) {
|
|
|
|
|
|
return fullLine;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Format long lines with proper line breaks
|
|
|
|
|
|
if (hasNamedParam && field.scopeName != null && field.scopeName!.isNotEmpty) {
|
|
|
|
|
|
// For scoped calls with named parameters, break after openScope
|
|
|
|
|
|
return " instance.$fieldName = CherryPick.openScope(\n"
|
|
|
|
|
|
" scopeName: '${field.scopeName}',\n"
|
|
|
|
|
|
" ).$resolveMethod(named: '${field.namedValue}');";
|
|
|
|
|
|
} else if (hasNamedParam) {
|
|
|
|
|
|
// For named parameters without scope, break after the method call
|
|
|
|
|
|
return " instance.$fieldName = $openCall.$resolveMethod(\n"
|
|
|
|
|
|
" named: '${field.namedValue}',\n"
|
|
|
|
|
|
" );";
|
|
|
|
|
|
} else if (field.scopeName != null && field.scopeName!.isNotEmpty) {
|
|
|
|
|
|
// For scoped calls without named params, break after openScope with proper parameter formatting
|
|
|
|
|
|
return " instance.$fieldName = CherryPick.openScope(\n"
|
|
|
|
|
|
" scopeName: '${field.scopeName}',\n"
|
|
|
|
|
|
" ).$resolveMethod();";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// For simple long calls, break after openRootScope
|
|
|
|
|
|
return " instance.$fieldName = $openCall\n"
|
|
|
|
|
|
" .$resolveMethod();";
|
|
|
|
|
|
}
|
2025-05-23 16:03:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
|
2025-07-15 16:03:10 +03:00
|
|
|
|
/// Parsed type information / Информация о типе поля.
|
|
|
|
|
|
final ParsedType parsedType;
|
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,
|
2025-07-15 16:03:10 +03:00
|
|
|
|
required this.parsedType,
|
2025-05-23 16:03:29 +03:00
|
|
|
|
this.scopeName,
|
|
|
|
|
|
this.namedValue,
|
|
|
|
|
|
});
|
2025-07-15 16:03:10 +03:00
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
String toString() {
|
|
|
|
|
|
return '_ParsedInjectField(fieldName: $fieldName, parsedType: $parsedType, '
|
|
|
|
|
|
'scopeName: $scopeName, namedValue: $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) =>
|
2025-07-15 12:07:23 +03:00
|
|
|
|
custom.injectCustomBuilder(options);
|