Compare commits

..

2 Commits

Author SHA1 Message Date
Sergey Penkovsky
e47adbb7fd style(cherrypick_generator): format generator files 2026-03-24 10:30:06 +03:00
Sergey Penkovsky
b7979d653e refactor(cherrypick_generator): migrate inject codegen to code_builder 2026-03-02 12:47:02 +03:00
12 changed files with 303 additions and 193 deletions

View File

@@ -11,14 +11,17 @@
// limitations under the License. // limitations under the License.
// //
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element2.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:analyzer/dart/element/type.dart';
import 'package:build/build.dart'; import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:source_gen/source_gen.dart'; import 'package:source_gen/source_gen.dart';
import 'package:cherrypick_annotations/cherrypick_annotations.dart' as ann; 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. /// CherryPick DI field injector generator for codegen.
/// ///
/// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`) /// Analyzes all Dart classes marked with `@injectable()` and generates a mixin (for example, `_$ProfileScreen`)
@@ -115,24 +118,54 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
final className = classElement.firstFragment.name2; final className = classElement.firstFragment.name2;
final mixinName = '_\$$className'; final mixinName = '_\$$className';
final buffer = StringBuffer() AnnotationValidator.validateClassAnnotations(classElement);
..writeln('mixin $mixinName {')
..writeln(' void _inject($className instance) {'); final classType = TypeParser.parseType(classElement.thisType, classElement);
// Collect and process all @inject fields
final injectFields = classElement.fields2 final injectFields = classElement.fields2
.where((f) => _isInjectField(f)) .where((f) => _isInjectField(f))
.map((f) => _parseInjectField(f)); .map(_parseInjectField)
.toList();
for (final parsedField in injectFields) { final injectMethod = Method((b) {
buffer.writeln(_generateInjectionLine(parsedField)); 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 final mixin = Mixin((b) {
..writeln(' }') b
..writeln('}'); ..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`. /// Returns true if a field is annotated with `@inject`.
@@ -150,11 +183,13 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
/// Converts Dart field declaration and all parameterizing injection-related /// Converts Dart field declaration and all parameterizing injection-related
/// annotations into a [_ParsedInjectField] which is used for codegen. /// annotations into a [_ParsedInjectField] which is used for codegen.
static _ParsedInjectField _parseInjectField(FieldElement2 field) { static _ParsedInjectField _parseInjectField(FieldElement2 field) {
AnnotationValidator.validateFieldAnnotations(field);
String? scopeName; String? scopeName;
String? namedValue; String? namedValue;
for (final meta in field.firstFragment.metadata2.annotations) { for (final meta in field.firstFragment.metadata2.annotations) {
final DartObject? obj = meta.computeConstantValue(); final obj = meta.computeConstantValue();
final type = obj?.type?.getDisplayString(); final type = obj?.type?.getDisplayString();
if (type == 'scope') { if (type == 'scope') {
scopeName = obj?.getField('name')?.toStringValue(); scopeName = obj?.getField('name')?.toStringValue();
@@ -164,65 +199,15 @@ class InjectGenerator extends GeneratorForAnnotation<ann.injectable> {
} }
final DartType dartType = field.type; final DartType dartType = field.type;
String coreTypeName; final parsedType = TypeParser.parseType(dartType, field);
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,
));
return _ParsedInjectField( return _ParsedInjectField(
fieldName: field.firstFragment.name2 ?? '', fieldName: field.firstFragment.name2 ?? '',
coreType: coreTypeName.replaceAll('?', ''), // удаляем "?" на всякий parsedType: parsedType,
isFuture: isFuture,
isNullable: isNullable,
scopeName: scopeName, scopeName: scopeName,
namedValue: namedValue, 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 /// Internal structure: describes all required information for generating the injection
@@ -233,14 +218,8 @@ class _ParsedInjectField {
/// The name of the field to be injected. /// The name of the field to be injected.
final String fieldName; final String fieldName;
/// The Dart type to resolve (e.g. `Logger` from `Logger` or `Future<Logger>`). /// Parsed type info for the field.
final String coreType; final ParsedType parsedType;
/// 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;
/// The scoping for DI resolution, or null to use root scope. /// The scoping for DI resolution, or null to use root scope.
final String? scopeName; final String? scopeName;
@@ -250,9 +229,7 @@ class _ParsedInjectField {
_ParsedInjectField({ _ParsedInjectField({
required this.fieldName, required this.fieldName,
required this.coreType, required this.parsedType,
required this.isFuture,
required this.isNullable,
this.scopeName, this.scopeName,
this.namedValue, this.namedValue,
}); });

View File

@@ -0,0 +1,80 @@
//
// 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),
),
);
}
});
}
}

View File

@@ -65,6 +65,8 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
Element2 element, Element2 element,
) { ) {
final buffer = StringBuffer(); final buffer = StringBuffer();
final location = _safeLocation(element);
final enclosing = _safeEnclosingDisplayName(element);
// Header with category // Header with category
buffer.writeln('[$category] $message'); buffer.writeln('[$category] $message');
@@ -74,18 +76,11 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
buffer.writeln('Context:'); buffer.writeln('Context:');
buffer.writeln(' Element: ${element.displayName}'); buffer.writeln(' Element: ${element.displayName}');
buffer.writeln(' Type: ${element.runtimeType}'); buffer.writeln(' Type: ${element.runtimeType}');
buffer.writeln( buffer.writeln(' Location: $location');
' Location: ${element.firstFragment.libraryFragment?.source.fullName ?? 'unknown'}',
);
// Try to show enclosing element info for extra context // Try to show enclosing element info for extra context
try {
final enclosing = (element as dynamic).enclosingElement;
if (enclosing != null) { if (enclosing != null) {
buffer.writeln(' Enclosing: ${enclosing.displayName}'); buffer.writeln(' Enclosing: $enclosing');
}
} catch (e) {
// Ignore if enclosingElement is not available
} }
// Arbitrary user context // Arbitrary user context
@@ -105,6 +100,34 @@ class CherryPickGeneratorException extends InvalidGenerationSourceError {
return buffer.toString(); 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;
}
}
} }
/// --------------------------------------------------------------------------- /// ---------------------------------------------------------------------------

View File

@@ -22,6 +22,7 @@ dependencies:
cherrypick_annotations: ^4.0.0-dev.0 cherrypick_annotations: ^4.0.0-dev.0
analyzer: ">=8.2.9 <10.0.1" analyzer: ">=8.2.9 <10.0.1"
dart_style: ^3.0.0 dart_style: ^3.0.0
code_builder: ^4.10.1
build: ^3.0.0 build: ^3.0.0
source_gen: ^4.2.0 source_gen: ^4.2.0
collection: ^1.18.0 collection: ^1.18.0

View File

@@ -1,32 +0,0 @@
//
// 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:test/test.dart';
// Import working test suites
import 'simple_test.dart' as simple_tests;
import 'bind_spec_test.dart' as bind_spec_tests;
import 'metadata_utils_test.dart' as metadata_utils_tests;
// Import integration test suites (now working!)
import 'module_generator_test.dart' as module_generator_tests;
import 'inject_generator_test.dart' as inject_generator_tests;
void main() {
group('CherryPick Generator Tests', () {
group('Simple Tests', simple_tests.main);
group('BindSpec Tests', bind_spec_tests.main);
group('MetadataUtils Tests', metadata_utils_tests.main);
group('ModuleGenerator Tests', module_generator_tests.main);
group('InjectGenerator Tests', inject_generator_tests.main);
});
}

View File

@@ -11,9 +11,13 @@
// limitations under the License. // limitations under the License.
// //
import 'dart:isolate';
import 'package:build/build.dart'; import 'package:build/build.dart';
import 'package:build_test/build_test.dart'; import 'package:build_test/build_test.dart';
import 'package:build_runner_core/build_runner_core.dart';
import 'package:cherrypick_generator/inject_generator.dart'; import 'package:cherrypick_generator/inject_generator.dart';
import 'package:package_config/package_config.dart';
import 'package:source_gen/source_gen.dart'; import 'package:source_gen/source_gen.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@@ -40,8 +44,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -75,8 +79,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -113,8 +117,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -124,9 +128,8 @@ part of 'test_widget.dart';
mixin _\$TestWidget { mixin _\$TestWidget {
void _inject(TestWidget instance) { void _inject(TestWidget instance) {
instance.service = CherryPick.openRootScope().resolve<MyService>( instance.service =
named: 'myService', CherryPick.openRootScope().resolve<MyService>(named: 'myService');
);
} }
} }
'''; ''';
@@ -151,8 +154,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -162,9 +165,8 @@ part of 'test_widget.dart';
mixin _\$TestWidget { mixin _\$TestWidget {
void _inject(TestWidget instance) { void _inject(TestWidget instance) {
instance.service = CherryPick.openRootScope().tryResolve<MyService>( instance.service =
named: 'myService', CherryPick.openRootScope().tryResolve<MyService>(named: 'myService');
);
} }
} }
'''; ''';
@@ -191,8 +193,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -229,8 +231,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -240,9 +242,9 @@ part of 'test_widget.dart';
mixin _\$TestWidget { mixin _\$TestWidget {
void _inject(TestWidget instance) { void _inject(TestWidget instance) {
instance.service = CherryPick.openScope( instance.service =
scopeName: 'userScope', CherryPick.openScope(scopeName: 'userScope')
).resolve<MyService>(named: 'myService'); .resolve<MyService>(named: 'myService');
} }
} }
'''; ''';
@@ -268,8 +270,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -303,8 +305,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -339,8 +341,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -350,9 +352,8 @@ part of 'test_widget.dart';
mixin _\$TestWidget { mixin _\$TestWidget {
void _inject(TestWidget instance) { void _inject(TestWidget instance) {
instance.service = CherryPick.openRootScope().resolveAsync<MyService>( instance.service =
named: 'myService', CherryPick.openRootScope().resolveAsync<MyService>(named: 'myService');
);
} }
} }
'''; ''';
@@ -391,8 +392,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -403,13 +404,11 @@ part of 'test_widget.dart';
mixin _\$TestWidget { mixin _\$TestWidget {
void _inject(TestWidget instance) { void _inject(TestWidget instance) {
instance.apiService = CherryPick.openRootScope().resolve<ApiService>(); instance.apiService = CherryPick.openRootScope().resolve<ApiService>();
instance.cacheService = CherryPick.openRootScope().tryResolve<CacheService>( instance.cacheService =
named: 'cache', CherryPick.openRootScope().tryResolve<CacheService>(named: 'cache');
);
instance.dbService = instance.dbService =
CherryPick.openScope( CherryPick.openScope(scopeName: 'dbScope')
scopeName: 'dbScope', .resolveAsync<DatabaseService>();
).resolveAsync<DatabaseService>();
} }
} }
'''; ''';
@@ -439,8 +438,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -496,8 +495,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -533,8 +532,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -569,8 +568,8 @@ class TestWidget {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_widget.dart'; part of 'test_widget.dart';
@@ -593,10 +592,41 @@ mixin _\$TestWidget {
/// Helper function to test code generation /// Helper function to test code generation
Future<void> _testGeneration(String input, String expectedOutput) async { Future<void> _testGeneration(String input, String expectedOutput) async {
await testBuilder( final readerWriter = TestReaderWriter(rootPackage: 'a');
await readerWriter.testing.loadIsolateSources();
final packageConfig = await loadPackageConfigUri(
(await Isolate.packageConfig)!,
);
final outputs = expectedOutput.isEmpty
? null
: {
'a|lib/test_widget.inject.cherrypick.g.dart':
decodedMatches(_normalizedEquals(expectedOutput)),
};
final result = await testBuilder(
injectBuilder(BuilderOptions.empty), injectBuilder(BuilderOptions.empty),
{'a|lib/test_widget.dart': input}, {'a|lib/test_widget.dart': input},
outputs: {'a|lib/test_widget.inject.cherrypick.g.dart': expectedOutput}, outputs: outputs,
readerWriter: TestReaderWriter(), readerWriter: readerWriter,
rootPackage: 'a',
packageConfig: packageConfig,
);
if (expectedOutput.isEmpty && result.buildResult.status == BuildStatus.failure) {
throw InvalidGenerationSourceError('Build failed');
}
}
Matcher _normalizedEquals(String expected) {
return predicate<String>(
(actual) => _normalize(actual) == _normalize(expected),
'matches after normalization',
); );
} }
String _normalize(String input) {
return input
.replaceAll(RegExp(r'\s+'), '')
.replaceAll(RegExp(r',\)'), ')')
.replaceAll(RegExp(r',\]'), ']')
.replaceAll(RegExp(r',\}'), '}');
}

View File

@@ -11,12 +11,16 @@
// limitations under the License. // limitations under the License.
// //
import 'package:test/test.dart'; import 'dart:isolate';
import 'package:build_test/build_test.dart';
import 'package:build/build.dart'; import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:build_runner_core/build_runner_core.dart';
import 'package:package_config/package_config.dart';
import 'package:test/test.dart';
import 'package:cherrypick_generator/module_generator.dart';
import 'package:source_gen/source_gen.dart'; import 'package:source_gen/source_gen.dart';
import 'package:cherrypick_generator/module_generator.dart';
void main() { void main() {
group('ModuleGenerator Tests', () { group('ModuleGenerator Tests', () {
@@ -40,8 +44,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -75,8 +79,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -113,8 +117,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -149,8 +153,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -187,8 +191,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -224,8 +228,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -264,8 +268,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -299,8 +303,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -334,8 +338,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -374,8 +378,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -414,8 +418,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -453,8 +457,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -488,8 +492,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -541,8 +545,8 @@ abstract class TestModule extends Module {
'''; ''';
const expectedOutput = ''' const expectedOutput = '''
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// dart format width=80
part of 'test_module.dart'; part of 'test_module.dart';
@@ -637,10 +641,41 @@ abstract class TestModule extends Module {
/// Helper function to test code generation /// Helper function to test code generation
Future<void> _testGeneration(String input, String expectedOutput) async { Future<void> _testGeneration(String input, String expectedOutput) async {
await testBuilder( final readerWriter = TestReaderWriter(rootPackage: 'a');
await readerWriter.testing.loadIsolateSources();
final packageConfig = await loadPackageConfigUri(
(await Isolate.packageConfig)!,
);
final outputs = expectedOutput.isEmpty
? null
: {
'a|lib/test_module.module.cherrypick.g.dart':
decodedMatches(_normalizedEquals(expectedOutput)),
};
final result = await testBuilder(
moduleBuilder(BuilderOptions.empty), moduleBuilder(BuilderOptions.empty),
{'a|lib/test_module.dart': input}, {'a|lib/test_module.dart': input},
outputs: {'a|lib/test_module.module.cherrypick.g.dart': expectedOutput}, outputs: outputs,
readerWriter: TestReaderWriter(), readerWriter: readerWriter,
rootPackage: 'a',
packageConfig: packageConfig,
);
if (expectedOutput.isEmpty && result.buildResult.status == BuildStatus.failure) {
throw InvalidGenerationSourceError('Build failed');
}
}
Matcher _normalizedEquals(String expected) {
return predicate<String>(
(actual) => _normalize(actual) == _normalize(expected),
'matches after normalization',
); );
} }
String _normalize(String input) {
return input
.replaceAll(RegExp(r'\s+'), '')
.replaceAll(RegExp(r',\)'), ')')
.replaceAll(RegExp(r',\]'), ']')
.replaceAll(RegExp(r',\}'), '}');
}

View File

@@ -134,7 +134,7 @@ packages:
path: "../../cherrypick_annotations" path: "../../cherrypick_annotations"
relative: true relative: true
source: path source: path
version: "3.0.2" version: "4.0.0-dev.0"
cherrypick_flutter: cherrypick_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -148,7 +148,7 @@ packages:
path: "../../cherrypick_generator" path: "../../cherrypick_generator"
relative: true relative: true
source: path source: path
version: "3.0.2" version: "4.0.0-dev.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:

View File

@@ -182,14 +182,14 @@ packages:
path: "../../cherrypick_annotations" path: "../../cherrypick_annotations"
relative: true relative: true
source: path source: path
version: "3.0.2" version: "4.0.0-dev.0"
cherrypick_generator: cherrypick_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
path: "../../cherrypick_generator" path: "../../cherrypick_generator"
relative: true relative: true
source: path source: path
version: "3.0.2" version: "4.0.0-dev.0"
cli_launcher: cli_launcher:
dependency: transitive dependency: transitive
description: description:

View File

@@ -64,14 +64,10 @@
- **WHEN** метод помечен одновременно `@instance` и `@provide` - **WHEN** метод помечен одновременно `@instance` и `@provide`
- **THEN** генератор завершает сборку с ошибкой валидации - **THEN** генератор завершает сборку с ошибкой валидации
#### Scenario: Требования к @named на provider-методе #### Scenario: Требования к @named
- **WHEN** `@named` на provider-методе использует пустую строку или некорректный идентификатор - **WHEN** `@named` использует пустую строку или некорректный идентификатор
- **THEN** генератор завершает сборку с ошибкой валидации - **THEN** генератор завершает сборку с ошибкой валидации
#### Scenario: Пустой @named на inject-поле
- **WHEN** `@named('')` указан на поле с `@inject`
- **THEN** генератор трактует поле как безымянный резолв (без параметра `named`)
#### Scenario: Валидность @module #### Scenario: Валидность @module
- **WHEN** класс с `@module` не имеет публичных методов - **WHEN** класс с `@module` не имеет публичных методов
- **THEN** генератор завершает сборку с ошибкой валидации - **THEN** генератор завершает сборку с ошибкой валидации

View File

@@ -23,11 +23,11 @@
- **THEN** subscope удаляется из дерева, а связанные ресурсы освобождаются - **THEN** subscope удаляется из дерева, а связанные ресурсы освобождаются
#### Scenario: Путь scope и разделитель #### Scenario: Путь scope и разделитель
- **WHEN** вызывается `CherryPick.openScope(scopeName: ..., separator: ...)` с иерархическим путем - **WHEN** scope открывается по иерархическому пути с разделителем
- **THEN** создается цепочка subscopes по каждому сегменту пути - **THEN** создается цепочка subscopes по каждому сегменту пути
#### Scenario: Пустой scopeName #### Scenario: Пустой scopeName
- **WHEN** вызывается `CherryPick.openScope(scopeName: '')` - **WHEN** scope открывается с пустым именем
- **THEN** возвращается root scope - **THEN** возвращается root scope
### Requirement: Установка и удаление модулей ### Requirement: Установка и удаление модулей
@@ -81,8 +81,8 @@
### Requirement: Ошибки несоответствия sync/async ### Requirement: Ошибки несоответствия sync/async
Резолв MUST выбрасывать ошибки при несоответствии sync/async режима. Резолв MUST выбрасывать ошибки при несоответствии sync/async режима.
#### Scenario: Синхронный резолв для asyncbinding #### Scenario: ResolveSync для asyncинстанса
- **WHEN** binding зарегистрирован как asyncинстанс или asyncprovider, а вызывается `resolve<T>()` или `tryResolve<T>()` - **WHEN** binding зарегистрирован как asyncинстанс или asyncprovider, а вызывается `resolveSync`
- **THEN** выбрасывается ошибка с указанием использовать asyncрезолв - **THEN** выбрасывается ошибка с указанием использовать asyncрезолв
### Requirement: Управление Disposable ### Requirement: Управление Disposable
@@ -111,7 +111,7 @@
- **THEN** observer получает соответствующие уведомления - **THEN** observer получает соответствующие уведомления
### Requirement: Ошибки и сообщения об ошибках ### Requirement: Ошибки и сообщения об ошибках
При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением. Для отсутствующей зависимости `tryResolve`/`tryResolveAsync` MUST возвращать `null` без исключения; ошибки выполнения резолва (например, цикл, sync/async mismatch, отсутствие обязательных params) MAY быть проброшены. При критических сбоях резолва ядро MUST выбрасывать ошибку с понятным сообщением, а для tryResolve MUST не бросать исключения.
#### Scenario: Ошибка отсутствующей зависимости #### Scenario: Ошибка отсутствующей зависимости
- **WHEN** вызывается `resolve<T>()` для незарегистрированной зависимости - **WHEN** вызывается `resolve<T>()` для незарегистрированной зависимости

View File

@@ -41,14 +41,14 @@ Flutterинтеграция MUST предоставлять `CherryPickProvid
#### Scenario: Ошибка lookup #### Scenario: Ошибка lookup
- **WHEN** вызов происходит вне поддерева провайдера - **WHEN** вызов происходит вне поддерева провайдера
- **THEN** в debugрежиме происходит assertionошибка - **THEN** происходит assertionошибка
### Requirement: Ошибки и сообщения ### Requirement: Ошибки и сообщения
При отсутствии провайдера в дереве MUST быть диагностируемая ошибка. При отсутствии провайдера в дереве MUST быть диагностируемая ошибка.
#### Scenario: Диагностика отсутствия провайдера #### Scenario: Диагностика отсутствия провайдера
- **WHEN** `CherryPickProvider.of(context)` не находит провайдер - **WHEN** `CherryPickProvider.of(context)` не находит провайдер
- **THEN** в debugрежиме сообщение assertion указывает на отсутствие провайдера - **THEN** сообщение об ошибке указывает на отсутствие провайдера
### Requirement: Точки расширения ### Requirement: Точки расширения
Flutterинтеграция MUST позволять использовать собственные DIscope стратегии поверх `CherryPickProvider`. Flutterинтеграция MUST позволять использовать собственные DIscope стратегии поверх `CherryPickProvider`.