Skip to content

Commit

Permalink
Fixing the generic annotations parsing (#22)
Browse files Browse the repository at this point in the history
* adding the failing test and shortening the extension name

* test fix

* adding a sophisticated test case

* fixing the generic annotation parsing

* comments, cleanup, and asserts

* extending _typeParametersNames function and removing _typeParametersAnnotation

* naming

* version bump

* minor correction
  • Loading branch information
numen31337 authored Oct 25, 2020
1 parent 81c7ad4 commit 9ef4e7e
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 38 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Provides [source_gen](https://pub.dev/packages/source_gen) `Generator` for generating `copyWith` extensions. For more info on this package check out [my blog article](https://www.alexander-kirsch.com/blog/dart-extensions/).
Provides [source_gen](https://pub.dev/packages/source_gen) `Generator` for generating `copyWith` extensions. For more info on this package check out [my blog article](https://www.alexander-kirsch.com/blog/dart-extensions/) or the [main documentation file](https://pub.dev/packages/copy_with_extension_gen).

## copy_with_extension
* Package: [copy_with_extension](https://pub.dev/packages/copy_with_extension)
Expand Down
3 changes: 3 additions & 0 deletions copy_with_extension/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 1.4.0 Improving genetic compatibility
* Fixes issue with generating code for some classes with generic type parameters.

## 1.3.0 Immutable Fields
* Fixes the `boolean-expression-must-not-be-null-exception` issue
* Introduces `immutable` field annotation
Expand Down
4 changes: 2 additions & 2 deletions copy_with_extension/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: copy_with_extension
version: 1.3.0
version: 1.4.0
description: Annotation for generating `copyWith` extensions code using `copy_with_extension_gen`.
homepage: https://github.com/numen31337/copy_with_extension/tree/master/copy_with_extension
repository: https://github.com/numen31337/copy_with_extension
Expand All @@ -12,4 +12,4 @@ dev_dependencies:
pedantic: ^1.9.0
build_runner: ^1.10.3
test: ^1.0.0
copy_with_extension_gen: ^1.3.0
copy_with_extension_gen: ^1.4.0
3 changes: 3 additions & 0 deletions copy_with_extension_gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 1.4.0 Improving genetic compatibility
* Fixes issue with generating code for some classes with generic type parameters.

## 1.3.1 README Update
* Update README.md

Expand Down
8 changes: 4 additions & 4 deletions copy_with_extension_gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Provides [Dart Build System](https://pub.dev/packages/build) builder for generat
## Usage

#### In your `pubspec.yaml` file:
- Add to `dependencies` section `copy_with_extension: ^1.3.0`
- Add to `dev_dependencies` section `copy_with_extension_gen: ^1.3.0`
- Add to `dependencies` section `copy_with_extension: ^1.4.0`
- Add to `dev_dependencies` section `copy_with_extension_gen: ^1.4.0`
- Add to `dev_dependencies` section `build_runner: ^1.10.3`
- Set `environment` to at least Dart 2.9.0 version like so: `">=2.9.0 <3.0.0"`

Expand All @@ -22,12 +22,12 @@ environment:

dependencies:
...
copy_with_extension: ^1.3.0
copy_with_extension: ^1.4.0

dev_dependencies:
...
build_runner: ^1.10.3
copy_with_extension_gen: ^1.3.0
copy_with_extension_gen: ^1.4.0
```
#### Annotate your class with `CopyWith` annotation:
Expand Down
56 changes: 34 additions & 22 deletions copy_with_extension_gen/lib/src/copy_with_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,52 +20,52 @@ class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
BuildStep buildStep,
) {
if (element is! ClassElement) throw '$element is not a ClassElement';

final classElement = element as ClassElement;
final sortedFields = _sortedConstructorFields(classElement);
final generateCopyWithNull =
annotation.read('generateCopyWithNull').boolValue;

final typeParametersAnnotation = _typeParametersAnnotation(classElement);
final typeParametersNames = _typeParametersNames(classElement);
final typeParametersAnnotation = _typeParametersString(classElement, false);
final typeParametersNames = _typeParametersString(classElement, true);
final typeAnnotation = classElement.name + typeParametersNames;

return '''
extension ${classElement.name}CopyWithExtension$typeParametersAnnotation on ${classElement.name}$typeParametersNames {
extension ${classElement.name}CopyWith$typeParametersAnnotation on ${classElement.name}$typeParametersNames {
${_copyWithPart(typeAnnotation, sortedFields)}
${generateCopyWithNull ? _copyWithNullPart(typeAnnotation, sortedFields) : ""}
}
''';
}

String _typeParametersNames(ClassElement classElement) {
final names = classElement.typeParameters.map((e) => e.name).join(',');
///Returns parameter names or full parameters declaration declared by this class or an empty string.
///
///If `nameOnly` is `true`: `class MyClass<T extends String, Y>` returns `<T, Y>`.
///
///If `nameOnly` is `false`: `class MyClass<T extends String, Y>` returns `<T extends String, Y>`.
String _typeParametersString(ClassElement classElement, bool nameOnly) {
assert(classElement is ClassElement);
assert(nameOnly is bool);

final names = classElement.typeParameters
.map(
(e) => nameOnly ? e.name : e.getDisplayString(withNullability: false),
)
.join(',');
if (names.isNotEmpty) {
return '<$names>';
} else {
return '';
}
}

String _typeParametersAnnotation(ClassElement classElement) {
final classDisplayString =
classElement.getDisplayString(withNullability: false);
final startIndex = classDisplayString.indexOf('<');
final endIndex = classDisplayString.indexOf('>');

if (startIndex != -1 && endIndex != -1) {
return classDisplayString.substring(
startIndex,
endIndex + 1,
);
} else {
return '';
}
}

///Generates the complete `copyWith` function.
String _copyWithPart(
String typeAnnotation,
List<_FieldInfo> sortedFields,
) {
assert(typeAnnotation is String && sortedFields is List<_FieldInfo>);

final constructorInput = sortedFields.fold<String>(
'',
(r, v) {
Expand Down Expand Up @@ -94,10 +94,13 @@ class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
''';
}

///Generates the complete `copyWithNull` function.
String _copyWithNullPart(
String typeAnnotation,
List<_FieldInfo> sortedFields,
) {
assert(typeAnnotation is String && sortedFields is List<_FieldInfo>);

final nullConstructorInput = sortedFields.fold<String>(
'',
(r, v) {
Expand Down Expand Up @@ -126,6 +129,8 @@ class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
''';
}

///Generates a list of `_FieldInfo` for each class field that will be a part of the code generation process.
///The resulting array is sorted by the field name. `Throws` on error.
List<_FieldInfo> _sortedConstructorFields(ClassElement element) {
assert(element is ClassElement);

Expand All @@ -152,6 +157,7 @@ class CopyWithGenerator extends GeneratorForAnnotation<CopyWith> {
}
}

///Represents a single class field with the additional metadata needed for code generation.
class _FieldInfo {
final String name;
final String type;
Expand All @@ -160,12 +166,18 @@ class _FieldInfo {
_FieldInfo(ParameterElement element, ClassElement classElement)
: name = element.name,
type = element.type.getDisplayString(withNullability: false),
immutable = _readFieldOptions(element, classElement).immutable;
immutable = _readFieldOptions(element, classElement).immutable,
assert(element.name is String),
assert(element.type.getDisplayString(withNullability: false) is String),
assert(_readFieldOptions(element, classElement).immutable is bool);

static CopyWithField _readFieldOptions(
ParameterElement element,
ClassElement classElement,
) {
assert(element is Element);
assert(classElement is ClassElement);

final fieldElement = classElement.getField(element.name);
if (fieldElement is! FieldElement) {
return CopyWithField();
Expand Down
4 changes: 2 additions & 2 deletions copy_with_extension_gen/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: copy_with_extension_gen
version: 1.3.1
version: 1.4.0
description: Automatically generating `copyWith` extensions code for classes with `@CopyWith()` annotation.
repository: https://github.com/numen31337/copy_with_extension
homepage: https://github.com/numen31337/copy_with_extension/tree/master/copy_with_extension_gen
Expand All @@ -12,7 +12,7 @@ dependencies:
analyzer: ">=0.39.0 <1.0.0"
build: ^1.0.0
source_gen: ">=0.9.0 <1.0.0"
copy_with_extension: ^1.3.0
copy_with_extension: ^1.4.0

dev_dependencies:
pedantic: ^1.9.0
Expand Down
3 changes: 1 addition & 2 deletions copy_with_extension_gen/test/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ part of 'test_case_class.dart';
// CopyWithGenerator
// **************************************************************************
extension Test_Case_ClassCopyWithExtension<T extends String>
on Test_Case_Class<T> {
extension Test_Case_ClassCopyWith<T extends String> on Test_Case_Class<T> {
Test_Case_Class<T> copyWith({
T id,
}) {
Expand Down
2 changes: 1 addition & 1 deletion copy_with_extension_test/lib/basic_test_case.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion copy_with_extension_test/lib/immutable_test_case.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions copy_with_extension_test/lib/implements_test_case.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:meta/meta.dart';
import 'package:copy_with_extension/copy_with_extension.dart';

//Won't work without it!
part 'implements_test_case.g.dart';

abstract class Abstract {
final String aField;
Abstract(this.aField);
}

abstract class AbstractWithType<T> {
final T tField;
AbstractWithType(this.tField);
}

abstract class AbstractWithType1<T> {
final T t1Field;
AbstractWithType1(this.t1Field);
}

abstract class AbstractWithType2<T, Y> {
final T saField;
final Y sa1Field;
AbstractWithType2(this.saField, this.sa1Field);
}

@immutable
@CopyWith()
class Basic implements Abstract {
Basic({this.aField});

@override
final String aField;
}

@immutable
@CopyWith()
class WithGenericType<T> implements AbstractWithType<T> {
WithGenericType({this.tField});

@override
final T tField;
}

@immutable
@CopyWith()
class WithSpecificType implements AbstractWithType<String> {
WithSpecificType({this.tField});

@override
final String tField;
}

@immutable
@CopyWith()
class WithBoth<T extends String, Y>
implements
Abstract,
AbstractWithType<T>,
AbstractWithType1<int>,
AbstractWithType2<String, Y> {
WithBoth({
this.aField,
this.tField,
this.t1Field,
this.saField,
this.sa1Field,
});

@override
final String aField;
@override
final T tField;
@override
final int t1Field;
@override
final String saField;
@override
final Y sa1Field;
}
55 changes: 55 additions & 0 deletions copy_with_extension_test/lib/implements_test_case.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions copy_with_extension_test/lib/subclass_test_case.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9ef4e7e

Please sign in to comment.