// // 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().toProvide(() => api(currentScope.resolve(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 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().toProvide(() => provideApi(currentScope.resolve(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() if (provide.contains('\n')) { // Provider clause is already multiline if (postfix.isNotEmpty) { // If there's a postfix, break after bind() final multilinePostfix = _generateMultilinePostfix(indent); return '${indentStr}bind<$returnType>()' '\n${' ' * (indent + 4)}$provide' '$multilinePostfix;'; } else { // No postfix, keep bind() with provide start return '${indentStr}bind<$returnType>()$provide;'; } } else { // Simple multiline: break after bind() 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 = []; 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 = []; 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(), }, ); } } }