Files
cherrypick/cherrypick_generator/lib/inject_generator.dart

243 lines
8.0 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
2025-08-08 23:24:05 +03:00
// https://www.apache.org/licenses/LICENSE-2.0
2025-05-23 16:10:09 +03:00
// 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.
//
import 'package:analyzer/dart/element/element2.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:code_builder/code_builder.dart';
2025-05-23 12:21:23 +03:00
import 'package:source_gen/source_gen.dart';
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann;
import 'src/annotation_validator.dart';
import 'src/code_builder_emitters.dart';
import 'src/type_parser.dart';
/// CherryPick DI field injector generator for codegen.
2025-05-23 16:10:09 +03:00
///
/// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`)
/// which contains the `_inject` method. This method will assign all fields annotated with `@inject()`
/// using the CherryPick DI container. Extra annotation qualifiers such as `@named` and `@scope` are respected
/// for each field. Nullable fields and Future/injectable async dependencies are also supported automatically.
2025-05-23 16:10:09 +03:00
///
/// ---
///
/// ### Example usage in a project:
2025-05-23 16:10:09 +03:00
///
/// ```dart
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
2025-05-23 16:10:09 +03:00
///
/// @injectable()
/// class MyScreen with _$MyScreen {
/// @inject()
/// late final Logger logger;
///
/// @inject()
/// @named('test')
/// late final HttpClient client;
///
/// @inject()
/// Future<Analytics>? analytics;
/// }
/// ```
///
/// After running build_runner, this mixin will be auto-generated:
///
/// ```dart
/// mixin _$MyScreen {
/// void _inject(MyScreen instance) {
/// instance.logger = CherryPick.openRootScope().resolve<Logger>();
/// instance.client = CherryPick.openRootScope().resolve<HttpClient>(named: 'test');
/// instance.analytics = CherryPick.openRootScope().tryResolveAsync<Analytics>(); // nullable async inject
/// }
/// }
/// ```
///
/// You may use the mixin (e.g., `myScreen._inject(myScreen)`) or expose your own public helper for instance field injection.
///
/// **Supported scenarios:**
/// - Ordinary injectable fields: `resolve<T>()`.
/// - Named qualifiers: `resolve<T>(named: ...)`.
/// - Scoping: `CherryPick.openScope(scopeName: ...).resolve<T>()`.
/// - Nullable/incomplete fields: `tryResolve`/`tryResolveAsync`.
/// - Async dependencies: `Future<T>`/`resolveAsync<T>()`.
///
/// See also:
/// * @inject
/// * @injectable
2025-05-23 12:21:23 +03:00
class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
const InjectGenerator();
/// Main entry point for CherryPick field injection code generation.
2025-05-23 16:10:09 +03:00
///
/// - Only triggers for classes marked with `@injectable()`.
/// - Throws an error if used on non-class elements.
/// - Scans all fields marked with `@inject()` and gathers qualifiers (if any).
/// - Generates Dart code for a mixin that injects all dependencies into the target class instance.
2025-05-23 16:10:09 +03:00
///
/// Returns the Dart code as a String defining the new mixin.
///
/// Example input (user code):
/// ```dart
/// @injectable()
/// class UserBloc with _$UserBloc {
/// @inject() late final AuthRepository authRepository;
/// }
/// ```
/// Example output (generated):
/// ```dart
/// mixin _$UserBloc {
/// void _inject(UserBloc instance) {
/// instance.authRepository = CherryPick.openRootScope().resolve<AuthRepository>();
/// }
/// }
/// ```
2025-05-23 12:21:23 +03:00
@override
dynamic generateForAnnotatedElement(
Element2 element,
2025-05-23 16:03:29 +03:00
ConstantReader annotation,
BuildStep buildStep,
) {
if (element is! ClassElement2) {
2025-05-23 12:21:23 +03:00
throw InvalidGenerationSourceError(
'@injectable() can only be applied to classes.',
element: element,
);
}
final classElement = element;
final className = classElement.firstFragment.name2;
2025-05-23 12:21:23 +03:00
final mixinName = '_\$$className';
AnnotationValidator.validateClassAnnotations(classElement);
final classType = TypeParser.parseType(classElement.thisType, classElement);
2025-05-23 14:08:08 +03:00
final injectFields = classElement.fields2
.where((f) => _isInjectField(f))
.map(_parseInjectField)
.toList();
final injectMethod = Method((b) {
b
..name = '_inject'
..returns = refer('void')
..requiredParameters.add(
Parameter((p) {
p
..name = 'instance'
..type = CodeBuilderEmitters.resolveTypeRef(classType);
}),
)
..body = Block((body) {
for (final field in injectFields) {
final scopeExpr = CodeBuilderEmitters.openScope(
scopeName: field.scopeName,
);
final resolveExpr = CodeBuilderEmitters.resolveCall(
scopeExpr: scopeExpr,
parsedType: field.parsedType,
named: field.namedValue,
);
body.statements.add(
refer(
'instance',
).property(field.fieldName).assign(resolveExpr).statement,
);
}
});
});
final mixin = Mixin((b) {
b
..name = mixinName
..methods.add(injectMethod);
});
final library = Library((b) => b..body.add(mixin));
final emitter = DartEmitter(useNullSafetySyntax: true);
return '${library.accept(emitter)}';
2025-05-23 16:03:29 +03:00
}
2025-05-23 14:08:08 +03:00
/// Returns true if a field is annotated with `@inject`.
2025-05-23 16:10:09 +03:00
///
/// Used to detect which fields should be processed for injection.
static bool _isInjectField(FieldElement2 field) {
return field.firstFragment.metadata2.annotations.any(
2025-05-23 16:03:29 +03:00
(m) => m.computeConstantValue()?.type?.getDisplayString() == 'inject',
);
}
2025-05-23 14:08:08 +03:00
/// Parses `@inject()` field and extracts all injection metadata
/// (core type, qualifiers, scope, nullability, etc).
2025-05-23 16:10:09 +03:00
///
/// Converts Dart field declaration and all parameterizing injection-related
/// annotations into a [_ParsedInjectField] which is used for codegen.
static _ParsedInjectField _parseInjectField(FieldElement2 field) {
AnnotationValidator.validateFieldAnnotations(field);
2025-05-23 16:03:29 +03:00
String? scopeName;
String? namedValue;
for (final meta in field.firstFragment.metadata2.annotations) {
final obj = meta.computeConstantValue();
2025-05-23 16:03:29 +03:00
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;
final parsedType = TypeParser.parseType(dartType, field);
2025-05-23 16:03:29 +03:00
return _ParsedInjectField(
fieldName: field.firstFragment.name2 ?? '',
parsedType: parsedType,
2025-05-23 16:03:29 +03:00
scopeName: scopeName,
namedValue: namedValue,
);
2025-05-23 12:21:23 +03:00
}
2025-05-23 16:03:29 +03:00
}
/// Internal structure: describes all required information for generating the injection
/// assignment for a given field.
2025-05-23 16:10:09 +03:00
///
/// Not exported. Used as a DTO in the generator for each field.
2025-05-23 16:03:29 +03:00
class _ParsedInjectField {
/// The name of the field to be injected.
2025-05-23 16:03:29 +03:00
final String fieldName;
2025-05-23 16:10:09 +03:00
/// Parsed type info for the field.
final ParsedType parsedType;
/// The scoping for DI resolution, or null to use root scope.
2025-05-23 16:03:29 +03:00
final String? scopeName;
2025-05-23 16:10:09 +03:00
/// Name qualifier for named resolution, or null if not set.
2025-05-23 16:03:29 +03:00
final String? namedValue;
_ParsedInjectField({
required this.fieldName,
required this.parsedType,
2025-05-23 16:03:29 +03:00
this.scopeName,
this.namedValue,
});
2025-05-23 12:21:23 +03:00
}
/// Factory for creating the build_runner builder for DI field injection.
2025-05-23 16:10:09 +03:00
///
/// Add this builder in your build.yaml if you're invoking CherryPick generators manually.
2025-05-23 12:21:23 +03:00
Builder injectBuilder(BuilderOptions options) =>
PartBuilder([InjectGenerator()], '.inject.cherrypick.g.dart');