mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-03-25 04:40:33 +00:00
refactor(cherrypick_generator): migrate inject codegen to code_builder
This commit is contained in:
@@ -11,14 +11,17 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element2.dart';
|
||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:build/build.dart';
|
||||
import 'package:code_builder/code_builder.dart';
|
||||
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.
|
||||
///
|
||||
/// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`)
|
||||
@@ -115,24 +118,58 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
final className = classElement.firstFragment.name2;
|
||||
final mixinName = '_\$$className';
|
||||
|
||||
final buffer = StringBuffer()
|
||||
..writeln('mixin $mixinName {')
|
||||
..writeln(' void _inject($className instance) {');
|
||||
AnnotationValidator.validateClassAnnotations(classElement);
|
||||
|
||||
final classType = TypeParser.parseType(
|
||||
classElement.thisType,
|
||||
classElement,
|
||||
);
|
||||
|
||||
// Collect and process all @inject fields
|
||||
final injectFields = classElement.fields2
|
||||
.where((f) => _isInjectField(f))
|
||||
.map((f) => _parseInjectField(f));
|
||||
.map(_parseInjectField)
|
||||
.toList();
|
||||
|
||||
for (final parsedField in injectFields) {
|
||||
buffer.writeln(_generateInjectionLine(parsedField));
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
buffer
|
||||
..writeln(' }')
|
||||
..writeln('}');
|
||||
final mixin = Mixin((b) {
|
||||
b
|
||||
..name = mixinName
|
||||
..methods.add(injectMethod);
|
||||
});
|
||||
|
||||
return buffer.toString();
|
||||
final library = Library((b) => b..body.add(mixin));
|
||||
final emitter = DartEmitter(useNullSafetySyntax: true);
|
||||
return '${library.accept(emitter)}';
|
||||
}
|
||||
|
||||
/// Returns true if a field is annotated with `@inject`.
|
||||
@@ -150,11 +187,13 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
/// 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);
|
||||
|
||||
String? scopeName;
|
||||
String? namedValue;
|
||||
|
||||
for (final meta in field.firstFragment.metadata2.annotations) {
|
||||
final DartObject? obj = meta.computeConstantValue();
|
||||
final obj = meta.computeConstantValue();
|
||||
final type = obj?.type?.getDisplayString();
|
||||
if (type == 'scope') {
|
||||
scopeName = obj?.getField('name')?.toStringValue();
|
||||
@@ -164,65 +203,15 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
|
||||
}
|
||||
|
||||
final DartType dartType = field.type;
|
||||
String coreTypeName;
|
||||
bool isFuture;
|
||||
|
||||
if (dartType.isDartAsyncFuture) {
|
||||
final ParameterizedType paramType = dartType as ParameterizedType;
|
||||
coreTypeName = paramType.typeArguments.first.getDisplayString();
|
||||
isFuture = true;
|
||||
} else {
|
||||
coreTypeName = dartType.getDisplayString();
|
||||
isFuture = false;
|
||||
}
|
||||
|
||||
// Determine nullability for field types like T? or Future<T?>
|
||||
bool isNullable =
|
||||
dartType.nullabilitySuffix == NullabilitySuffix.question ||
|
||||
(dartType is ParameterizedType &&
|
||||
(dartType).typeArguments.any(
|
||||
(t) => t.nullabilitySuffix == NullabilitySuffix.question,
|
||||
));
|
||||
final parsedType = TypeParser.parseType(dartType, field);
|
||||
|
||||
return _ParsedInjectField(
|
||||
fieldName: field.firstFragment.name2 ?? '',
|
||||
coreType: coreTypeName.replaceAll('?', ''), // удаляем "?" на всякий
|
||||
isFuture: isFuture,
|
||||
isNullable: isNullable,
|
||||
parsedType: parsedType,
|
||||
scopeName: scopeName,
|
||||
namedValue: namedValue,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates Dart code for a single dependency-injected field based on its metadata.
|
||||
///
|
||||
/// This code will resolve the field from the CherryPick DI container and assign it to the class instance.
|
||||
/// Correctly dispatches to resolve, tryResolve, resolveAsync, or tryResolveAsync methods,
|
||||
/// and applies container scoping or named resolution where required.
|
||||
///
|
||||
/// Returns literal Dart code as string (1 line).
|
||||
///
|
||||
/// Example output:
|
||||
/// `instance.logger = CherryPick.openRootScope().resolve<Logger>();`
|
||||
String _generateInjectionLine(_ParsedInjectField field) {
|
||||
final resolveMethod = field.isFuture
|
||||
? (field.isNullable
|
||||
? 'tryResolveAsync<${field.coreType}>'
|
||||
: 'resolveAsync<${field.coreType}>')
|
||||
: (field.isNullable
|
||||
? 'tryResolve<${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.$resolveMethod$params;";
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal structure: describes all required information for generating the injection
|
||||
@@ -233,14 +222,8 @@ class _ParsedInjectField {
|
||||
/// The name of the field to be injected.
|
||||
final String fieldName;
|
||||
|
||||
/// The Dart type to resolve (e.g. `Logger` from `Logger` or `Future<Logger>`).
|
||||
final String coreType;
|
||||
|
||||
/// True if the field is an async dependency (Future<...>), otherwise false.
|
||||
final bool isFuture;
|
||||
|
||||
/// True if the field accepts null (T?), otherwise false.
|
||||
final bool isNullable;
|
||||
/// Parsed type info for the field.
|
||||
final ParsedType parsedType;
|
||||
|
||||
/// The scoping for DI resolution, or null to use root scope.
|
||||
final String? scopeName;
|
||||
@@ -250,9 +233,7 @@ class _ParsedInjectField {
|
||||
|
||||
_ParsedInjectField({
|
||||
required this.fieldName,
|
||||
required this.coreType,
|
||||
required this.isFuture,
|
||||
required this.isNullable,
|
||||
required this.parsedType,
|
||||
this.scopeName,
|
||||
this.namedValue,
|
||||
});
|
||||
|
||||
83
cherrypick_generator/lib/src/code_builder_emitters.dart
Normal file
83
cherrypick_generator/lib/src/code_builder_emitters.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// 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
|
||||
// https://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.
|
||||
//
|
||||
|
||||
import 'package:code_builder/code_builder.dart';
|
||||
|
||||
import 'type_parser.dart';
|
||||
|
||||
/// Small helpers for building code_builder AST nodes used by generators.
|
||||
class CodeBuilderEmitters {
|
||||
/// Builds a CherryPick scope opener expression.
|
||||
///
|
||||
/// - If [scopeName] is empty or null, uses openRootScope().
|
||||
/// - Otherwise uses openScope(scopeName: ...).
|
||||
static Expression openScope({String? scopeName}) {
|
||||
if (scopeName == null || scopeName.isEmpty) {
|
||||
return refer('CherryPick').property('openRootScope').call([]);
|
||||
}
|
||||
return refer('CherryPick').property('openScope').call(
|
||||
[],
|
||||
{'scopeName': literalString(scopeName)},
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a TypeReference appropriate for resolving a dependency.
|
||||
///
|
||||
/// For Future<T>, it returns the inner type reference (T).
|
||||
/// Nullability and generic arguments are preserved.
|
||||
static TypeReference resolveTypeRef(ParsedType parsedType) {
|
||||
final target = parsedType.isFuture && parsedType.innerType != null
|
||||
? parsedType.innerType!
|
||||
: parsedType;
|
||||
return _typeRefFromParsedType(target, stripNullability: true);
|
||||
}
|
||||
|
||||
/// Builds a DI resolve call on [scopeExpr] using [parsedType] and [named].
|
||||
///
|
||||
/// The method name is derived from [ParsedType.resolveMethodName].
|
||||
static Expression resolveCall({
|
||||
required Expression scopeExpr,
|
||||
required ParsedType parsedType,
|
||||
String? named,
|
||||
}) {
|
||||
final typeRef = resolveTypeRef(parsedType);
|
||||
final method = parsedType.resolveMethodName;
|
||||
final args = <Expression>[];
|
||||
final namedArgs = <String, Expression>{};
|
||||
if (named != null && named.isNotEmpty) {
|
||||
namedArgs['named'] = literalString(named);
|
||||
}
|
||||
return scopeExpr.property(method).call(args, namedArgs, [typeRef]);
|
||||
}
|
||||
|
||||
static TypeReference _typeRefFromParsedType(
|
||||
ParsedType parsedType, {
|
||||
required bool stripNullability,
|
||||
}) {
|
||||
return TypeReference((b) {
|
||||
b
|
||||
..symbol = parsedType.coreType
|
||||
..isNullable = stripNullability ? false : parsedType.isNullable;
|
||||
if (parsedType.typeArguments.isNotEmpty) {
|
||||
b.types.addAll(
|
||||
parsedType.typeArguments.map(
|
||||
(arg) => _typeRefFromParsedType(
|
||||
arg,
|
||||
stripNullability: stripNullability,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,8 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
Element2 element,
|
||||
) {
|
||||
final buffer = StringBuffer();
|
||||
final location = _safeLocation(element);
|
||||
final enclosing = _safeEnclosingDisplayName(element);
|
||||
|
||||
// Header with category
|
||||
buffer.writeln('[$category] $message');
|
||||
@@ -74,18 +76,11 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
buffer.writeln('Context:');
|
||||
buffer.writeln(' Element: ${element.displayName}');
|
||||
buffer.writeln(' Type: ${element.runtimeType}');
|
||||
buffer.writeln(
|
||||
' Location: ${element.firstFragment.libraryFragment?.source.fullName ?? 'unknown'}',
|
||||
);
|
||||
buffer.writeln(' Location: $location');
|
||||
|
||||
// Try to show enclosing element info for extra context
|
||||
try {
|
||||
final enclosing = (element as dynamic).enclosingElement;
|
||||
if (enclosing != null) {
|
||||
buffer.writeln(' Enclosing: ${enclosing.displayName}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if enclosingElement is not available
|
||||
if (enclosing != null) {
|
||||
buffer.writeln(' Enclosing: $enclosing');
|
||||
}
|
||||
|
||||
// Arbitrary user context
|
||||
@@ -105,6 +100,33 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Best-effort extraction of element location for diagnostics.
|
||||
///
|
||||
/// Some tests use lightweight mocks for [Element2] that don't implement
|
||||
/// analyzer fragment APIs, so this method must never throw.
|
||||
static String _safeLocation(Element2 element) {
|
||||
try {
|
||||
return element.firstFragment.libraryFragment?.source.fullName ?? 'unknown';
|
||||
} catch (_) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort extraction of enclosing element display name.
|
||||
///
|
||||
/// Accessed via dynamic to stay compatible with analyzer API differences.
|
||||
static String? _safeEnclosingDisplayName(Element2 element) {
|
||||
try {
|
||||
final enclosing = (element as dynamic).enclosingElement;
|
||||
if (enclosing == null) return null;
|
||||
final name = enclosing.displayName;
|
||||
if (name is String && name.isNotEmpty) return name;
|
||||
return enclosing.toString();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user