mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-23 21:13:35 +00:00
- Added comprehensive English documentation for all DI generator and support files: * inject_generator.dart — full class/method doc-comments, usage samples * module_generator.dart — doc-comments, feature explanation, complete example * src/annotation_validator.dart — class and detailed static method descriptions * src/type_parser.dart — doc, example for ParsedType and TypeParser, specific codegen notes * src/bind_spec.dart — interface, static factory, and codegen docs with DI scenarios * src/bind_parameters_spec.dart — details and samples for code generation logic * src/metadata_utils.dart — full doc and examples for annotation utilities * src/exceptions.dart — user- and contributor-friendly errors, structured output, category explanations * src/generated_class.dart — usage-centric doc-comments, example of resulting generated DI class - Removed Russian/duplicate comments for full clarity and maintainability - Ensured that new and existing contributors can easily extend and maintain DI code generation logic BREAKING CHANGE: All documentation now English-only; comments include usage examples for each principal structure or routine See #docs, #generator, #cherrypick
344 lines
12 KiB
Dart
344 lines
12 KiB
Dart
//
|
|
// 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:analyzer/dart/element/element.dart';
|
|
|
|
import 'bind_parameters_spec.dart';
|
|
import 'metadata_utils.dart';
|
|
import 'exceptions.dart';
|
|
import 'type_parser.dart';
|
|
import 'annotation_validator.dart';
|
|
|
|
/// Enum representing the binding annotation applied to a module method.
|
|
enum BindingType {
|
|
/// Direct instance returned from the method (@instance).
|
|
instance,
|
|
/// Provider/factory function (@provide).
|
|
provide;
|
|
}
|
|
|
|
/// ---------------------------------------------------------------------------
|
|
/// BindSpec
|
|
///
|
|
/// Describes a DI container binding as generated from a single public factory,
|
|
/// instance, or provider method of a module (annotated with @instance or @provide).
|
|
///
|
|
/// Includes all annotation-driven parameters required to generate valid DI
|
|
/// registration Dart code in CherryPick:
|
|
/// - Return type
|
|
/// - Provider method name
|
|
/// - Singleton flag
|
|
/// - Named identifier (from @named)
|
|
/// - List of resolved or runtime (@params) parameters
|
|
/// - Binding mode (instance/provide)
|
|
/// - Async and parametric variants
|
|
///
|
|
/// ## Example usage
|
|
/// ```dart
|
|
/// // Suppose @provide() Api api(@named('test') Client client)
|
|
/// final bindSpec = BindSpec.fromMethod(methodElement);
|
|
/// print(bindSpec.generateBind(2)); // bind<Api>().toProvide(() => api(currentScope.resolve<Client>(named: 'test')));
|
|
/// ```
|
|
/// ---------------------------------------------------------------------------
|
|
class BindSpec {
|
|
/// The type this binding provides (e.g. SomeService)
|
|
final String returnType;
|
|
|
|
/// Binding provider/factory method name
|
|
final String methodName;
|
|
|
|
/// Named identifier for DI resolution (null if unnamed)
|
|
final String? named;
|
|
|
|
/// If true, binding is registered as singleton in DI
|
|
final bool isSingleton;
|
|
|
|
/// Provider/factory method parameters (in order)
|
|
final List<BindParameterSpec> parameters;
|
|
|
|
/// Instance vs provider mode, from annotation choice
|
|
final BindingType bindingType;
|
|
|
|
/// Async flag for .toInstanceAsync()
|
|
final bool isAsyncInstance;
|
|
|
|
/// Async flag for .toProvideAsync()
|
|
final bool isAsyncProvide;
|
|
|
|
/// True if a @params runtime parameter is present
|
|
final bool hasParams;
|
|
|
|
BindSpec({
|
|
required this.returnType,
|
|
required this.methodName,
|
|
required this.isSingleton,
|
|
required this.parameters,
|
|
this.named,
|
|
required this.bindingType,
|
|
required this.isAsyncInstance,
|
|
required this.isAsyncProvide,
|
|
required this.hasParams,
|
|
});
|
|
|
|
/// Generates a Dart code line for binding registration.
|
|
///
|
|
/// Example (single-line):
|
|
/// bind<Api>().toProvide(() => provideApi(currentScope.resolve<Client>(named: 'test'))).withName('prod').singleton();
|
|
///
|
|
/// The [indent] argument sets the space indentation for pretty-printing.
|
|
String generateBind(int indent) {
|
|
final indentStr = ' ' * indent;
|
|
final provide = _generateProvideClause(indent);
|
|
final postfix = _generatePostfix();
|
|
|
|
// Create the full single-line version first
|
|
final singleLine = '${indentStr}bind<$returnType>()$provide$postfix;';
|
|
|
|
// Check if we need multiline formatting
|
|
final needsMultiline = singleLine.length > 80 || provide.contains('\n');
|
|
|
|
if (!needsMultiline) {
|
|
return singleLine;
|
|
}
|
|
|
|
// For multiline formatting, check if we need to break after bind<Type>()
|
|
if (provide.contains('\n')) {
|
|
// Provider clause is already multiline
|
|
if (postfix.isNotEmpty) {
|
|
// If there's a postfix, break after bind<Type>()
|
|
final multilinePostfix = _generateMultilinePostfix(indent);
|
|
return '${indentStr}bind<$returnType>()'
|
|
'\n${' ' * (indent + 4)}$provide'
|
|
'$multilinePostfix;';
|
|
} else {
|
|
// No postfix, keep bind<Type>() with provide start
|
|
return '${indentStr}bind<$returnType>()$provide;';
|
|
}
|
|
} else {
|
|
// Simple multiline: break after bind<Type>()
|
|
if (postfix.isNotEmpty) {
|
|
final multilinePostfix = _generateMultilinePostfix(indent);
|
|
return '${indentStr}bind<$returnType>()'
|
|
'\n${' ' * (indent + 4)}$provide'
|
|
'$multilinePostfix;';
|
|
} else {
|
|
return '${indentStr}bind<$returnType>()'
|
|
'\n${' ' * (indent + 4)}$provide;';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Internal method: decides how the provide clause should be generated by param kind.
|
|
String _generateProvideClause(int indent) {
|
|
if (hasParams) return _generateWithParamsProvideClause(indent);
|
|
return _generatePlainProvideClause(indent);
|
|
}
|
|
|
|
/// Generates code when using runtime parameters (@params).
|
|
String _generateWithParamsProvideClause(int indent) {
|
|
// Safe variable name for parameters.
|
|
const paramVar = 'args';
|
|
final fnArgs = parameters.map((p) => p.generateArg(paramVar)).join(', ');
|
|
// Use multiline format only if args are long or contain newlines
|
|
final multiLine = fnArgs.length > 60 || fnArgs.contains('\n');
|
|
switch (bindingType) {
|
|
case BindingType.instance:
|
|
throw StateError(
|
|
'Internal error: _generateWithParamsProvideClause called for @instance binding with @params.');
|
|
//return isAsyncInstance
|
|
// ? '.toInstanceAsync(($fnArgs) => $methodName($fnArgs))'
|
|
// : '.toInstance(($fnArgs) => $methodName($fnArgs))';
|
|
case BindingType.provide:
|
|
if (isAsyncProvide) {
|
|
return multiLine
|
|
? '.toProvideAsyncWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))'
|
|
: '.toProvideAsyncWithParams(($paramVar) => $methodName($fnArgs))';
|
|
} else {
|
|
return multiLine
|
|
? '.toProvideWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))'
|
|
: '.toProvideWithParams(($paramVar) => $methodName($fnArgs))';
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generates code when only resolved (not runtime) arguments used.
|
|
String _generatePlainProvideClause(int indent) {
|
|
final argsStr = parameters.map((p) => p.generateArg()).join(', ');
|
|
|
|
// Check if we need multiline formatting based on total line length
|
|
final singleLineCall = '$methodName($argsStr)';
|
|
final needsMultiline =
|
|
singleLineCall.length >= 45 || argsStr.contains('\n');
|
|
|
|
switch (bindingType) {
|
|
case BindingType.instance:
|
|
return isAsyncInstance
|
|
? '.toInstanceAsync($methodName($argsStr))'
|
|
: '.toInstance($methodName($argsStr))';
|
|
case BindingType.provide:
|
|
if (isAsyncProvide) {
|
|
if (needsMultiline) {
|
|
final lambdaIndent =
|
|
(isSingleton || named != null) ? indent + 6 : indent + 2;
|
|
final closingIndent =
|
|
(isSingleton || named != null) ? indent + 4 : indent;
|
|
return '.toProvideAsync(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
|
|
} else {
|
|
return '.toProvideAsync(() => $methodName($argsStr))';
|
|
}
|
|
} else {
|
|
if (needsMultiline) {
|
|
final lambdaIndent =
|
|
(isSingleton || named != null) ? indent + 6 : indent + 2;
|
|
final closingIndent =
|
|
(isSingleton || named != null) ? indent + 4 : indent;
|
|
return '.toProvide(\n${' ' * lambdaIndent}() => $methodName($argsStr),\n${' ' * closingIndent})';
|
|
} else {
|
|
return '.toProvide(() => $methodName($argsStr))';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// EN / RU: Adds .withName and .singleton if needed.
|
|
String _generatePostfix() {
|
|
final namePart = named != null ? ".withName('$named')" : '';
|
|
final singletonPart = isSingleton ? '.singleton()' : '';
|
|
return '$namePart$singletonPart';
|
|
}
|
|
|
|
/// EN / RU: Generates multiline postfix with proper indentation.
|
|
String _generateMultilinePostfix(int indent) {
|
|
final parts = <String>[];
|
|
if (named != null) {
|
|
parts.add(".withName('$named')");
|
|
}
|
|
if (isSingleton) {
|
|
parts.add('.singleton()');
|
|
}
|
|
if (parts.isEmpty) return '';
|
|
|
|
return parts.map((part) => '\n${' ' * (indent + 4)}$part').join('');
|
|
}
|
|
|
|
/// -------------------------------------------------------------------------
|
|
/// fromMethod
|
|
///
|
|
/// Constructs a [BindSpec] from an analyzer [MethodElement].
|
|
///
|
|
/// Validates and parses all type annotations, method/parameter DI hints,
|
|
/// and derives async and parametric flags as needed.
|
|
///
|
|
/// ## Example
|
|
/// ```dart
|
|
/// final bindSpec = BindSpec.fromMethod(methodElement);
|
|
/// print(bindSpec.returnType); // e.g., 'Logger'
|
|
/// ```
|
|
/// Throws [AnnotationValidationException] or [CodeGenerationException] if invalid.
|
|
static BindSpec fromMethod(MethodElement method) {
|
|
try {
|
|
// Validate method annotations
|
|
AnnotationValidator.validateMethodAnnotations(method);
|
|
|
|
// Parse return type using improved type parser
|
|
final parsedReturnType = TypeParser.parseType(method.returnType, method);
|
|
|
|
final methodName = method.displayName;
|
|
|
|
// Check for @singleton annotation.
|
|
final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton');
|
|
|
|
// Get @named value if present.
|
|
final named = MetadataUtils.getNamedValue(method.metadata);
|
|
|
|
// Parse each method parameter.
|
|
final params = <BindParameterSpec>[];
|
|
bool hasParams = false;
|
|
for (final p in method.parameters) {
|
|
final typeStr = p.type.getDisplayString();
|
|
final paramNamed = MetadataUtils.getNamedValue(p.metadata);
|
|
final isParams = MetadataUtils.anyMeta(p.metadata, 'params');
|
|
if (isParams) hasParams = true;
|
|
params.add(BindParameterSpec(typeStr, paramNamed, isParams: isParams));
|
|
}
|
|
|
|
// Determine bindingType: @instance or @provide.
|
|
final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance');
|
|
final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide');
|
|
|
|
if (!hasInstance && !hasProvide) {
|
|
throw AnnotationValidationException(
|
|
'Method must be marked with either @instance() or @provide() annotation',
|
|
element: method,
|
|
suggestion:
|
|
'Add @instance() for direct instances or @provide() for factory methods',
|
|
context: {
|
|
'method_name': methodName,
|
|
'return_type': parsedReturnType.displayString,
|
|
},
|
|
);
|
|
}
|
|
|
|
final bindingType =
|
|
hasInstance ? BindingType.instance : BindingType.provide;
|
|
|
|
// PROHIBIT @params with @instance bindings!
|
|
if (bindingType == BindingType.instance && hasParams) {
|
|
throw AnnotationValidationException(
|
|
'@params() (runtime arguments) cannot be used together with @instance()',
|
|
element: method,
|
|
suggestion: 'Use @provide() instead if you want runtime arguments',
|
|
context: {
|
|
'method_name': methodName,
|
|
'binding_type': 'instance',
|
|
'has_params': hasParams,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Set async flags based on parsed type
|
|
final isAsyncInstance =
|
|
bindingType == BindingType.instance && parsedReturnType.isFuture;
|
|
final isAsyncProvide =
|
|
bindingType == BindingType.provide && parsedReturnType.isFuture;
|
|
|
|
return BindSpec(
|
|
returnType: parsedReturnType.codeGenType,
|
|
methodName: methodName,
|
|
isSingleton: isSingleton,
|
|
named: named,
|
|
parameters: params,
|
|
bindingType: bindingType,
|
|
isAsyncInstance: isAsyncInstance,
|
|
isAsyncProvide: isAsyncProvide,
|
|
hasParams: hasParams,
|
|
);
|
|
} catch (e) {
|
|
if (e is CherryPickGeneratorException) {
|
|
rethrow;
|
|
}
|
|
throw CodeGenerationException(
|
|
'Failed to create BindSpec from method "${method.displayName}"',
|
|
element: method,
|
|
suggestion:
|
|
'Check that the method has valid annotations and return type',
|
|
context: {
|
|
'method_name': method.displayName,
|
|
'return_type': method.returnType.getDisplayString(),
|
|
'error': e.toString(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|