diff --git a/.gitignore b/.gitignore index 70f9369..9668610 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /**/.DS_Store /**/*.draft.dart +.vscode diff --git a/packages/draft_builder/lib/src/generator.dart b/packages/draft_builder/lib/src/generator.dart index df4e878..5884a00 100644 --- a/packages/draft_builder/lib/src/generator.dart +++ b/packages/draft_builder/lib/src/generator.dart @@ -614,7 +614,8 @@ class DraftGenerator extends GeneratorForAnnotation { // Forward computed getters. final handledNames = fields.map((f) => f.name).toSet(); - for (final accessor in classElement.getters.where( + final forwardedGetters = _getForwardedGetters(classElement); + for (final accessor in forwardedGetters.where( (a) => !a.isSynthetic && !handledNames.contains(a.name), )) { final returnType = accessor.returnType.getDisplayString(); @@ -626,7 +627,8 @@ class DraftGenerator extends GeneratorForAnnotation { buffer.writeln(); // Forward non-static, public instance methods. - for (final method in classElement.methods.where( + final forwardedMethods = _getForwardedMethods(classElement); + for (final method in forwardedMethods.where( (m) => !m.isStatic && m.isPublic && !m.isOperator && m.name != 'save', )) { final returnType = method.returnType.getDisplayString(); @@ -679,3 +681,30 @@ class DraftGenerator extends GeneratorForAnnotation { return buffer.toString(); } } + +/// Returns unique getters from [element] and supertypes, excluding Object type. +List _getForwardedGetters(ClassElement element) { + return _distinctByDisplayName([ + ...element.getters, + for (var supertype in element.allSupertypes) + if (!supertype.isDartCoreObject) ...supertype.getters, + ]); +} + +/// Returns unique methods from [element] and supertypes, excluding Object type. +List _getForwardedMethods(ClassElement element) { + return _distinctByDisplayName([ + ...element.methods, + for (var supertype in element.allSupertypes) + if (!supertype.isDartCoreObject) ...supertype.methods, + ]); +} + +/// Returns elements with unique display names, keeping the first occurrence. +List _distinctByDisplayName(List elements) { + final uniqueNames = {}; + return [ + for (var element in elements) + if (uniqueNames.add(element.displayName)) element, + ]; +} diff --git a/packages/draft_builder/test/integration/subtyping.dart b/packages/draft_builder/test/integration/subtyping.dart new file mode 100644 index 0000000..17f6702 --- /dev/null +++ b/packages/draft_builder/test/integration/subtyping.dart @@ -0,0 +1,39 @@ +import 'package:draft/draft.dart'; + +part 'subtyping.draft.dart'; + +sealed class Parent extends Base with Mixin implements Interface { + @override + bool get flagA => true; + + @override + void methodA() {} +} + +@draft +class Child extends Parent { + Child({required this.value}); + + final int value; + + @override + bool get flagB => false; + + @override + void methodB() {} +} + +abstract class Base { + bool get flagA; + void methodA(); +} + +abstract interface class Interface { + bool get flagA; + void methodB(); +} + +abstract mixin class Mixin { + bool get flagB; + void methodA(); +} diff --git a/packages/draft_builder/test/subtyping_test.dart b/packages/draft_builder/test/subtyping_test.dart new file mode 100644 index 0000000..35103bb --- /dev/null +++ b/packages/draft_builder/test/subtyping_test.dart @@ -0,0 +1,53 @@ +import 'package:test/test.dart'; + +import 'common.dart'; +import 'integration/subtyping.dart'; + +void main() { + test('compiles', () async { + await expectLater( + compile(r''' +import 'subtyping.dart'; + +void main() { + Child(value: 1); + Child(value: 1).draft().save(); + Child(value: 1).draft().methodA(); + Child(value: 1).draft().methodB(); + bool a = Child(value: 1).draft().flagA; + bool b = Child(value: 1).draft().flagB; + Child(value: 1).produce((draft) { + draft.value = 2; + draft.methodA(); + draft.methodB(); + bool a1 = draft.flagA; + bool b1 = draft.flagB; + }); + Child(value: 1).methodA(); + Child(value: 1).methodB(); + bool a2 = Child(value: 1).flagA; + bool b2 = Child(value: 1).flagB; +} +'''), + completes, + ); + }); + + test('works correctly', () async { + final instance = Child(value: 1); + final draft = instance.draft(); + expect(draft.value, 1); + draft.value = 2; + expect(draft.value, 2); + expect(instance.value, 1); + final saved = draft.save(); + expect(saved.value, 2); + expect(instance.value, 1); + expect(saved, isNot(same(instance))); + expect(saved, isNot(same(draft))); + expect(saved.flagA, isTrue); + expect(saved.draft().flagA, isTrue); + expect(saved.flagB, isFalse); + expect(saved.draft().flagB, isFalse); + }); +}