diff --git a/cherrypick_generator/lib/module_generator.dart b/cherrypick_generator/lib/module_generator.dart index dd807f2..7fc1957 100644 --- a/cherrypick_generator/lib/module_generator.dart +++ b/cherrypick_generator/lib/module_generator.dart @@ -16,6 +16,8 @@ import 'package:build/build.dart'; import 'package:source_gen/source_gen.dart'; import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann; +import 'src/generated_class.dart'; + /// /// Генератор зависимостей для DI-контейнера на основе аннотаций. /// @@ -48,313 +50,13 @@ class ModuleGenerator extends GeneratorForAnnotation { final classElement = element; // Создаёт объект, описывающий, какие биндинги нужно сгенерировать на основании методов класса - final generatedClass = _GeneratedClass.fromClassElement(classElement); + final generatedClass = GeneratedClass.fromClassElement(classElement); // Генерирует итоговый Dart-код return generatedClass.generate(); } } -/// -/// Описывает параметры для создания одного биндинга зависимости (binding spec). -/// -/// Каждый биндинг соответствует одному публичному методу класса-модуля. -/// -class BindSpec { - /// Тип, который предоставляет биндинг (например, SomeService) - final String returnType; - - /// Имя метода, который реализует биндинг - final String methodName; - - /// Необязательное имя, для именованной зависимости (используется с @named) - final String? named; - - /// Является ли зависимость синглтоном (имеется ли аннотация @singleton) - final bool isSingleton; - - /// Список параметров, которые требуются методу для внедрения зависимостей - final List parameters; - - final String bindingType; // 'instance' | 'provide' - - final bool isAsyncInstance; - - final bool isAsyncProvide; - - 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, - }); - - /// Формирует dart-код для биндинга, например: - /// bind().toProvide(() => method(args)).withName('name').singleton(); - /// - /// Параметр [indent] задаёт отступ для красивого форматирования кода. - String generateBind(int indent) { - final indentStr = ' ' * indent; - - // Если есть @params() - const paramVar = 'args'; // <= новое имя для безопасности - - if (hasParams) { - final fnArgs = parameters - .map((p) => p.isParams ? paramVar : p.generateArg(paramVar)) - .join(', '); - String provide; - if (bindingType == 'instance') { - provide = isAsyncInstance - ? '.toInstanceAsync(($fnArgs) => $methodName($fnArgs))' - : '.toInstance(($fnArgs) => $methodName($fnArgs))'; - } else if (isAsyncProvide) { - provide = (fnArgs.length > 60 || fnArgs.contains('\n')) - ? '.toProvideAsyncWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))' - : '.toProvideAsyncWithParams(($paramVar) => $methodName($fnArgs))'; - } else { - provide = (fnArgs.length > 60 || fnArgs.contains('\n')) - ? '.toProvideWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))' - : '.toProvideWithParams(($paramVar) => $methodName($fnArgs))'; - } - final namePart = named != null ? ".withName('$named')" : ''; - final singletonPart = isSingleton ? '.singleton()' : ''; - return '$indentStr' - 'bind<$returnType>()' - '$provide' - '$namePart' - '$singletonPart;'; - } - - // Собираем строку аргументов для вызова метода - final argsStr = parameters.map((p) => p.generateArg()).join(', '); - - // Если аргументов много или они длинные — разбиваем вызов на несколько строк - //final needMultiline = argsStr.length > 60 || argsStr.contains('\n'); - - String provide; - if (bindingType == 'instance') { - // Добавляем async вариант для Future - if (isAsyncInstance) { - provide = '.toInstanceAsync($methodName($argsStr))'; - } else { - provide = '.toInstance($methodName($argsStr))'; - } - } else { - // provide - if (isAsyncProvide) { - // Асинхронная фабрика - provide = (argsStr.length > 60 || argsStr.contains('\n')) - ? '.toProvideAsync(\n${' ' * (indent + 2)}() => $methodName($argsStr))' - : '.toProvideAsync(() => $methodName($argsStr))'; - } else { - provide = (argsStr.length > 60 || argsStr.contains('\n')) - ? '.toProvide(\n${' ' * (indent + 2)}() => $methodName($argsStr))' - : '.toProvide(() => $methodName($argsStr))'; - } - } - - final namePart = named != null ? ".withName('$named')" : ''; - final singletonPart = isSingleton ? '.singleton()' : ''; - - // Итоговый bind: bind().toProvide(...).withName(...).singleton(); - return '$indentStr' - 'bind<$returnType>()' - '$provide' - '$namePart' - '$singletonPart;'; - // Всегда заканчиваем точкой с запятой! - } - - /// Создаёт спецификацию биндинга (BindSpec) из метода класса-модуля - static BindSpec fromMethod(MethodElement method) { - var returnType = method.returnType.getDisplayString(); - - final methodName = method.displayName; - // Проверим, помечен ли метод аннотацией @singleton - final isSingleton = _MetadataUtils.anyMeta(method.metadata, 'singleton'); - - // Получаем имя из @named(), если есть - final named = _MetadataUtils.getNamedValue(method.metadata); - - // Для каждого параметра метода - 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)); - } - - // определяем bindingType - final hasInstance = _MetadataUtils.anyMeta(method.metadata, 'instance'); - final hasProvide = _MetadataUtils.anyMeta(method.metadata, 'provide'); - if (!hasInstance && !hasProvide) { - throw InvalidGenerationSourceError( - 'Метод $methodName класса-модуля должен быть помечен либо @instance(), либо @provide().', - element: method, - ); - } - final bindingType = hasInstance ? 'instance' : 'provide'; - - // --- Новый участок: извлекаем внутренний тип из Future<> и выставляем флаги - bool isAsyncInstance = false; - bool isAsyncProvide = false; - - if (returnType.startsWith('Future<')) { - final futureMatch = RegExp(r'^Future<(.+)>$').firstMatch(returnType); - if (futureMatch != null) { - returnType = futureMatch.group(1)!.trim(); - if (bindingType == 'instance') isAsyncInstance = true; - if (bindingType == 'provide') isAsyncProvide = true; - } - } - - return BindSpec( - returnType: returnType, - methodName: methodName, - isSingleton: isSingleton, - named: named, - parameters: params, - bindingType: bindingType, - isAsyncInstance: isAsyncInstance, - isAsyncProvide: isAsyncProvide, - hasParams: hasParams, - ); - } -} - -/// -/// Описывает один параметр метода и возможность его разрешения из контейнера. -/// -/// Например, если метод принимает SomeDep dep, то -/// BindParameterSpec хранит тип SomeDep, а generateArg отдаст строку -/// currentScope.resolve() -/// -class BindParameterSpec { - /// Имя типа параметра (например, SomeService) - final String typeName; - - /// Необязательное имя для разрешения по имени (если аннотировано через @named) - final String? named; - - final bool isParams; - - BindParameterSpec(this.typeName, this.named, {this.isParams = false}); - - /// Генерирует строку для получения зависимости из DI scope (с учётом имени) - String generateArg([String paramsVar = 'args']) { - if (isParams) { - return paramsVar; - } - if (named != null) { - return "currentScope.resolve<$typeName>(named: '$named')"; - } - return "currentScope.resolve<$typeName>()"; - } -} - -/// -/// Результат обработки одного класса-модуля: имя класса, его биндинги, -/// имя генерируемого класса и т.д. -/// -class _GeneratedClass { - /// Имя исходного класса-модуля - final String className; - - /// Имя генерируемого класса (например, $SomeModule) - final String generatedClassName; - - /// Список всех обнаруженных биндингов - final List binds; - - _GeneratedClass( - this.className, - this.generatedClassName, - this.binds, - ); - - /// Обрабатывает объект ClassElement (отображение класса в AST) - /// и строит структуру _GeneratedClass для генерации кода. - static _GeneratedClass fromClassElement(ClassElement element) { - final className = element.displayName; - // Имя с префиксом $ (стандартная практика для ген-кода) - final generatedClassName = r'$' + className; - // Собираем биндинги по всем методам класса, игнорируем абстрактные (без реализации) - final binds = element.methods - .where((m) => !m.isAbstract) - .map(BindSpec.fromMethod) - .toList(); - - return _GeneratedClass(className, generatedClassName, binds); - } - - /// Генерирует исходный Dart-код для созданного класса DI-модуля. - /// - /// Внутри builder(Scope currentScope) регистрируются все bind-методы. - String generate() { - final buffer = StringBuffer(); - - buffer.writeln('final class $generatedClassName extends $className {'); - buffer.writeln(' @override'); - buffer.writeln(' void builder(Scope currentScope) {'); - - // Для каждого биндинга — генерируем строку bind()... - for (final bind in binds) { - buffer.writeln(bind.generateBind(4)); - } - - buffer.writeln(' }'); - buffer.writeln('}'); - - return buffer.toString(); - } -} - -/// -/// Утилиты для разбора аннотаций методов и параметров. -/// Позволяют найти @named() и @singleton() у метода/параметра. -/// -class _MetadataUtils { - /// Проверяет: есть ли среди аннотаций метка, имя которой содержит [typeName] - /// (регистр не учитывается) - static bool anyMeta(List meta, String typeName) { - return meta.any((m) => - m - .computeConstantValue() - ?.type - ?.getDisplayString() - .toLowerCase() - .contains(typeName.toLowerCase()) ?? - false); - } - - /// Находит значение из аннотации @named('значение'). - /// Возвращает строку значения, если аннотация присутствует; иначе null. - static String? getNamedValue(List meta) { - for (final m in meta) { - final cv = m.computeConstantValue(); - - final typeStr = cv?.type?.getDisplayString().toLowerCase(); - - if (typeStr?.contains('named') ?? false) { - return cv?.getField('value')?.toStringValue(); - } - } - - return null; - } -} - /// /// Точка входа для генератора build_runner. /// Возвращает Builder, используемый build_runner для генерации кода для всех diff --git a/cherrypick_generator/lib/src/bind_parameters_spec.dart b/cherrypick_generator/lib/src/bind_parameters_spec.dart new file mode 100644 index 0000000..fa80e02 --- /dev/null +++ b/cherrypick_generator/lib/src/bind_parameters_spec.dart @@ -0,0 +1,42 @@ +// +// 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. +// + +/// +/// Описывает один параметр метода и возможность его разрешения из контейнера. +/// +/// Например, если метод принимает SomeDep dep, то +/// BindParameterSpec хранит тип SomeDep, а generateArg отдаст строку +/// currentScope.resolve() +/// +class BindParameterSpec { + /// Имя типа параметра (например, SomeService) + final String typeName; + + /// Необязательное имя для разрешения по имени (если аннотировано через @named) + final String? named; + + final bool isParams; + + BindParameterSpec(this.typeName, this.named, {this.isParams = false}); + + /// Генерирует строку для получения зависимости из DI scope (с учётом имени) + String generateArg([String paramsVar = 'args']) { + if (isParams) { + return paramsVar; + } + if (named != null) { + return "currentScope.resolve<$typeName>(named: '$named')"; + } + return "currentScope.resolve<$typeName>()"; + } +} diff --git a/cherrypick_generator/lib/src/bind_spec.dart b/cherrypick_generator/lib/src/bind_spec.dart new file mode 100644 index 0000000..33baa80 --- /dev/null +++ b/cherrypick_generator/lib/src/bind_spec.dart @@ -0,0 +1,196 @@ +// +// 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. +// + +import 'package:analyzer/dart/element/element.dart'; +import 'package:source_gen/source_gen.dart'; + +import 'bind_parameters_spec.dart'; +import 'metadata_utils.dart'; + +/// +/// Описывает параметры для создания одного биндинга зависимости (binding spec). +/// +/// Каждый биндинг соответствует одному публичному методу класса-модуля. +/// +class BindSpec { + /// Тип, который предоставляет биндинг (например, SomeService) + final String returnType; + + /// Имя метода, который реализует биндинг + final String methodName; + + /// Необязательное имя, для именованной зависимости (используется с @named) + final String? named; + + /// Является ли зависимость синглтоном (имеется ли аннотация @singleton) + final bool isSingleton; + + /// Список параметров, которые требуются методу для внедрения зависимостей + final List parameters; + + final String bindingType; // 'instance' | 'provide' + + final bool isAsyncInstance; + + final bool isAsyncProvide; + + 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, + }); + + /// Формирует dart-код для биндинга, например: + /// bind().toProvide(() => method(args)).withName('name').singleton(); + /// + /// Параметр [indent] задаёт отступ для красивого форматирования кода. + String generateBind(int indent) { + final indentStr = ' ' * indent; + + // Если есть @params() + const paramVar = 'args'; // <= новое имя для безопасности + + if (hasParams) { + final fnArgs = parameters + .map((p) => p.isParams ? paramVar : p.generateArg(paramVar)) + .join(', '); + String provide; + if (bindingType == 'instance') { + provide = isAsyncInstance + ? '.toInstanceAsync(($fnArgs) => $methodName($fnArgs))' + : '.toInstance(($fnArgs) => $methodName($fnArgs))'; + } else if (isAsyncProvide) { + provide = (fnArgs.length > 60 || fnArgs.contains('\n')) + ? '.toProvideAsyncWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))' + : '.toProvideAsyncWithParams(($paramVar) => $methodName($fnArgs))'; + } else { + provide = (fnArgs.length > 60 || fnArgs.contains('\n')) + ? '.toProvideWithParams(\n${' ' * (indent + 2)}($paramVar) => $methodName($fnArgs))' + : '.toProvideWithParams(($paramVar) => $methodName($fnArgs))'; + } + final namePart = named != null ? ".withName('$named')" : ''; + final singletonPart = isSingleton ? '.singleton()' : ''; + return '$indentStr' + 'bind<$returnType>()' + '$provide' + '$namePart' + '$singletonPart;'; + } + + // Собираем строку аргументов для вызова метода + final argsStr = parameters.map((p) => p.generateArg()).join(', '); + + // Если аргументов много или они длинные — разбиваем вызов на несколько строк + //final needMultiline = argsStr.length > 60 || argsStr.contains('\n'); + + String provide; + if (bindingType == 'instance') { + // Добавляем async вариант для Future + if (isAsyncInstance) { + provide = '.toInstanceAsync($methodName($argsStr))'; + } else { + provide = '.toInstance($methodName($argsStr))'; + } + } else { + // provide + if (isAsyncProvide) { + // Асинхронная фабрика + provide = (argsStr.length > 60 || argsStr.contains('\n')) + ? '.toProvideAsync(\n${' ' * (indent + 2)}() => $methodName($argsStr))' + : '.toProvideAsync(() => $methodName($argsStr))'; + } else { + provide = (argsStr.length > 60 || argsStr.contains('\n')) + ? '.toProvide(\n${' ' * (indent + 2)}() => $methodName($argsStr))' + : '.toProvide(() => $methodName($argsStr))'; + } + } + + final namePart = named != null ? ".withName('$named')" : ''; + final singletonPart = isSingleton ? '.singleton()' : ''; + + // Итоговый bind: bind().toProvide(...).withName(...).singleton(); + return '$indentStr' + 'bind<$returnType>()' + '$provide' + '$namePart' + '$singletonPart;'; + // Всегда заканчиваем точкой с запятой! + } + + /// Создаёт спецификацию биндинга (BindSpec) из метода класса-модуля + static BindSpec fromMethod(MethodElement method) { + var returnType = method.returnType.getDisplayString(); + + final methodName = method.displayName; + // Проверим, помечен ли метод аннотацией @singleton + final isSingleton = MetadataUtils.anyMeta(method.metadata, 'singleton'); + + // Получаем имя из @named(), если есть + final named = MetadataUtils.getNamedValue(method.metadata); + + // Для каждого параметра метода + 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)); + } + + // определяем bindingType + final hasInstance = MetadataUtils.anyMeta(method.metadata, 'instance'); + final hasProvide = MetadataUtils.anyMeta(method.metadata, 'provide'); + if (!hasInstance && !hasProvide) { + throw InvalidGenerationSourceError( + 'Метод $methodName класса-модуля должен быть помечен либо @instance(), либо @provide().', + element: method, + ); + } + final bindingType = hasInstance ? 'instance' : 'provide'; + + // --- Новый участок: извлекаем внутренний тип из Future<> и выставляем флаги + bool isAsyncInstance = false; + bool isAsyncProvide = false; + + if (returnType.startsWith('Future<')) { + final futureMatch = RegExp(r'^Future<(.+)>$').firstMatch(returnType); + if (futureMatch != null) { + returnType = futureMatch.group(1)!.trim(); + if (bindingType == 'instance') isAsyncInstance = true; + if (bindingType == 'provide') isAsyncProvide = true; + } + } + + return BindSpec( + returnType: returnType, + methodName: methodName, + isSingleton: isSingleton, + named: named, + parameters: params, + bindingType: bindingType, + isAsyncInstance: isAsyncInstance, + isAsyncProvide: isAsyncProvide, + hasParams: hasParams, + ); + } +} diff --git a/cherrypick_generator/lib/src/generated_class.dart b/cherrypick_generator/lib/src/generated_class.dart new file mode 100644 index 0000000..782483c --- /dev/null +++ b/cherrypick_generator/lib/src/generated_class.dart @@ -0,0 +1,73 @@ +// +// 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. +// + +import 'package:analyzer/dart/element/element.dart'; + +import 'bind_spec.dart'; + +/// +/// Результат обработки одного класса-модуля: имя класса, его биндинги, +/// имя генерируемого класса и т.д. +/// +class GeneratedClass { + /// Имя исходного класса-модуля + final String className; + + /// Имя генерируемого класса (например, $SomeModule) + final String generatedClassName; + + /// Список всех обнаруженных биндингов + final List binds; + + GeneratedClass( + this.className, + this.generatedClassName, + this.binds, + ); + + /// Обрабатывает объект ClassElement (отображение класса в AST) + /// и строит структуру _GeneratedClass для генерации кода. + static GeneratedClass fromClassElement(ClassElement element) { + final className = element.displayName; + // Имя с префиксом $ (стандартная практика для ген-кода) + final generatedClassName = r'$' + className; + // Собираем биндинги по всем методам класса, игнорируем абстрактные (без реализации) + final binds = element.methods + .where((m) => !m.isAbstract) + .map(BindSpec.fromMethod) + .toList(); + + return GeneratedClass(className, generatedClassName, binds); + } + + /// Генерирует исходный Dart-код для созданного класса DI-модуля. + /// + /// Внутри builder(Scope currentScope) регистрируются все bind-методы. + String generate() { + final buffer = StringBuffer(); + + buffer.writeln('final class $generatedClassName extends $className {'); + buffer.writeln(' @override'); + buffer.writeln(' void builder(Scope currentScope) {'); + + // Для каждого биндинга — генерируем строку bind()... + for (final bind in binds) { + buffer.writeln(bind.generateBind(4)); + } + + buffer.writeln(' }'); + buffer.writeln('}'); + + return buffer.toString(); + } +} diff --git a/cherrypick_generator/lib/src/metadata_utils.dart b/cherrypick_generator/lib/src/metadata_utils.dart new file mode 100644 index 0000000..56b6f72 --- /dev/null +++ b/cherrypick_generator/lib/src/metadata_utils.dart @@ -0,0 +1,49 @@ +// +// 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. +// + +import 'package:analyzer/dart/element/element.dart'; + +/// +/// Утилиты для разбора аннотаций методов и параметров. +/// Позволяют найти @named() и @singleton() у метода/параметра. +/// +class MetadataUtils { + /// Проверяет: есть ли среди аннотаций метка, имя которой содержит [typeName] + /// (регистр не учитывается) + static bool anyMeta(List meta, String typeName) { + return meta.any((m) => + m + .computeConstantValue() + ?.type + ?.getDisplayString() + .toLowerCase() + .contains(typeName.toLowerCase()) ?? + false); + } + + /// Находит значение из аннотации @named('значение'). + /// Возвращает строку значения, если аннотация присутствует; иначе null. + static String? getNamedValue(List meta) { + for (final m in meta) { + final cv = m.computeConstantValue(); + + final typeStr = cv?.type?.getDisplayString().toLowerCase(); + + if (typeStr?.contains('named') ?? false) { + return cv?.getField('value')?.toStringValue(); + } + } + + return null; + } +}