From b7979d653ee935f558fe4600a51a701e714549d6 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 2 Mar 2026 12:47:02 +0300 Subject: [PATCH] refactor(cherrypick_generator): migrate inject codegen to code_builder --- .../lib/inject_generator.dart | 137 ++++++++---------- .../lib/src/code_builder_emitters.dart | 83 +++++++++++ cherrypick_generator/lib/src/exceptions.dart | 42 ++++-- cherrypick_generator/pubspec.yaml | 1 + .../test/cherrypick_generator_test.dart | 32 ---- .../test/inject_generator_test.dart | 100 ++++++++----- .../test/module_generator_test.dart | 77 +++++++--- examples/client_app/pubspec.lock | 4 +- examples/postly/pubspec.lock | 4 +- 9 files changed, 300 insertions(+), 180 deletions(-) create mode 100644 cherrypick_generator/lib/src/code_builder_emitters.dart delete mode 100644 cherrypick_generator/test/cherrypick_generator_test.dart diff --git a/cherrypick_generator/lib/inject_generator.dart b/cherrypick_generator/lib/inject_generator.dart index eee034d..0270292 100644 --- a/cherrypick_generator/lib/inject_generator.dart +++ b/cherrypick_generator/lib/inject_generator.dart @@ -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 { 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 { /// 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 { } 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 - 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();` - 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`). - 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, }); diff --git a/cherrypick_generator/lib/src/code_builder_emitters.dart b/cherrypick_generator/lib/src/code_builder_emitters.dart new file mode 100644 index 0000000..778efc4 --- /dev/null +++ b/cherrypick_generator/lib/src/code_builder_emitters.dart @@ -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, 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 = []; + final namedArgs = {}; + 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, + ), + ), + ); + } + }); + } +} diff --git a/cherrypick_generator/lib/src/exceptions.dart b/cherrypick_generator/lib/src/exceptions.dart index ff80fc3..d33fcf1 100644 --- a/cherrypick_generator/lib/src/exceptions.dart +++ b/cherrypick_generator/lib/src/exceptions.dart @@ -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; + } + } } /// --------------------------------------------------------------------------- diff --git a/cherrypick_generator/pubspec.yaml b/cherrypick_generator/pubspec.yaml index eb91721..adc07a6 100644 --- a/cherrypick_generator/pubspec.yaml +++ b/cherrypick_generator/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: cherrypick_annotations: ^4.0.0-dev.0 analyzer: ">=8.2.9 <10.0.1" dart_style: ^3.0.0 + code_builder: ^4.10.1 build: ^3.0.0 source_gen: ^4.2.0 collection: ^1.18.0 diff --git a/cherrypick_generator/test/cherrypick_generator_test.dart b/cherrypick_generator/test/cherrypick_generator_test.dart deleted file mode 100644 index 7b56a9e..0000000 --- a/cherrypick_generator/test/cherrypick_generator_test.dart +++ /dev/null @@ -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); - }); -} diff --git a/cherrypick_generator/test/inject_generator_test.dart b/cherrypick_generator/test/inject_generator_test.dart index 16bb62d..7fbabff 100644 --- a/cherrypick_generator/test/inject_generator_test.dart +++ b/cherrypick_generator/test/inject_generator_test.dart @@ -11,9 +11,13 @@ // limitations under the License. // +import 'dart:isolate'; + import 'package:build/build.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:package_config/package_config.dart'; import 'package:source_gen/source_gen.dart'; import 'package:test/test.dart'; @@ -40,8 +44,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -75,8 +79,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -113,8 +117,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -124,9 +128,8 @@ part of 'test_widget.dart'; mixin _\$TestWidget { void _inject(TestWidget instance) { - instance.service = CherryPick.openRootScope().resolve( - named: 'myService', - ); + instance.service = + CherryPick.openRootScope().resolve(named: 'myService'); } } '''; @@ -151,8 +154,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -162,9 +165,8 @@ part of 'test_widget.dart'; mixin _\$TestWidget { void _inject(TestWidget instance) { - instance.service = CherryPick.openRootScope().tryResolve( - named: 'myService', - ); + instance.service = + CherryPick.openRootScope().tryResolve(named: 'myService'); } } '''; @@ -191,8 +193,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -229,8 +231,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -240,9 +242,9 @@ part of 'test_widget.dart'; mixin _\$TestWidget { void _inject(TestWidget instance) { - instance.service = CherryPick.openScope( - scopeName: 'userScope', - ).resolve(named: 'myService'); + instance.service = + CherryPick.openScope(scopeName: 'userScope') + .resolve(named: 'myService'); } } '''; @@ -268,8 +270,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -303,8 +305,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -339,8 +341,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -350,9 +352,8 @@ part of 'test_widget.dart'; mixin _\$TestWidget { void _inject(TestWidget instance) { - instance.service = CherryPick.openRootScope().resolveAsync( - named: 'myService', - ); + instance.service = + CherryPick.openRootScope().resolveAsync(named: 'myService'); } } '''; @@ -391,8 +392,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -403,13 +404,11 @@ part of 'test_widget.dart'; mixin _\$TestWidget { void _inject(TestWidget instance) { instance.apiService = CherryPick.openRootScope().resolve(); - instance.cacheService = CherryPick.openRootScope().tryResolve( - named: 'cache', - ); + instance.cacheService = + CherryPick.openRootScope().tryResolve(named: 'cache'); instance.dbService = - CherryPick.openScope( - scopeName: 'dbScope', - ).resolveAsync(); + CherryPick.openScope(scopeName: 'dbScope') + .resolveAsync(); } } '''; @@ -439,8 +438,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -496,8 +495,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -533,8 +532,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -569,8 +568,8 @@ class TestWidget { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_widget.dart'; @@ -593,10 +592,41 @@ mixin _\$TestWidget { /// Helper function to test code generation Future _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), {'a|lib/test_widget.dart': input}, - outputs: {'a|lib/test_widget.inject.cherrypick.g.dart': expectedOutput}, - readerWriter: TestReaderWriter(), + outputs: outputs, + readerWriter: readerWriter, + rootPackage: 'a', + packageConfig: packageConfig, + ); + if (expectedOutput.isEmpty && result.buildResult.status == BuildStatus.failure) { + throw InvalidGenerationSourceError('Build failed'); + } +} + +Matcher _normalizedEquals(String expected) { + return predicate( + (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',\}'), '}'); +} diff --git a/cherrypick_generator/test/module_generator_test.dart b/cherrypick_generator/test/module_generator_test.dart index bbc765c..c2f33cb 100644 --- a/cherrypick_generator/test/module_generator_test.dart +++ b/cherrypick_generator/test/module_generator_test.dart @@ -11,12 +11,16 @@ // limitations under the License. // -import 'package:test/test.dart'; -import 'package:build_test/build_test.dart'; -import 'package:build/build.dart'; +import 'dart:isolate'; + +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:cherrypick_generator/module_generator.dart'; void main() { group('ModuleGenerator Tests', () { @@ -40,8 +44,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -75,8 +79,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -113,8 +117,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -149,8 +153,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -187,8 +191,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -224,8 +228,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -264,8 +268,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -299,8 +303,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -334,8 +338,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -374,8 +378,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -414,8 +418,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -453,8 +457,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -488,8 +492,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -541,8 +545,8 @@ abstract class TestModule extends Module { '''; const expectedOutput = ''' -// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 part of 'test_module.dart'; @@ -637,10 +641,41 @@ abstract class TestModule extends Module { /// Helper function to test code generation Future _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), {'a|lib/test_module.dart': input}, - outputs: {'a|lib/test_module.module.cherrypick.g.dart': expectedOutput}, - readerWriter: TestReaderWriter(), + outputs: outputs, + readerWriter: readerWriter, + rootPackage: 'a', + packageConfig: packageConfig, + ); + if (expectedOutput.isEmpty && result.buildResult.status == BuildStatus.failure) { + throw InvalidGenerationSourceError('Build failed'); + } +} + +Matcher _normalizedEquals(String expected) { + return predicate( + (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',\}'), '}'); +} diff --git a/examples/client_app/pubspec.lock b/examples/client_app/pubspec.lock index b906706..0d54493 100644 --- a/examples/client_app/pubspec.lock +++ b/examples/client_app/pubspec.lock @@ -134,7 +134,7 @@ packages: path: "../../cherrypick_annotations" relative: true source: path - version: "3.0.2" + version: "4.0.0-dev.0" cherrypick_flutter: dependency: "direct main" description: @@ -148,7 +148,7 @@ packages: path: "../../cherrypick_generator" relative: true source: path - version: "3.0.2" + version: "4.0.0-dev.0" clock: dependency: transitive description: diff --git a/examples/postly/pubspec.lock b/examples/postly/pubspec.lock index 9797d62..29786f1 100644 --- a/examples/postly/pubspec.lock +++ b/examples/postly/pubspec.lock @@ -182,14 +182,14 @@ packages: path: "../../cherrypick_annotations" relative: true source: path - version: "3.0.2" + version: "4.0.0-dev.0" cherrypick_generator: dependency: "direct dev" description: path: "../../cherrypick_generator" relative: true source: path - version: "3.0.2" + version: "4.0.0-dev.0" cli_launcher: dependency: transitive description: