Skip to content

Commit cf9b0f2

Browse files
authored
[pigeon] Dart Class Equality (#8788)
Adds Equality and Hash methods to custom classes in Dart. I am still working on other languages, but this can be a standalone pr while I complete the others. fixes flutter/flutter#118087
1 parent dd781d4 commit cf9b0f2

File tree

19 files changed

+1173
-326
lines changed

19 files changed

+1173
-326
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 25.1.0
2+
3+
* [dart] Adds equality methods to generated data classes.
4+
15
## 25.0.0
26

37
* **Breaking Change** Removes `oneLanguage` field from `PigeonOptions`.
@@ -11,8 +15,7 @@
1115

1216
## 24.2.1
1317

14-
* [dart] Fixes potential race condition caused by a ProxyApi constructor message call being made in
15-
an async method.
18+
* [dart] Fixes potential race condition caused by a ProxyApi constructor message call being made in an async method.
1619

1720
## 24.2.0
1821

packages/pigeon/example/app/lib/src/event_channel_messages.g.dart

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,38 @@ class IntEvent extends PlatformEvent {
2020

2121
int data;
2222

23-
Object encode() {
23+
List<Object?> _toList() {
2424
return <Object?>[
2525
data,
2626
];
2727
}
2828

29+
Object encode() {
30+
return _toList();
31+
}
32+
2933
static IntEvent decode(Object result) {
3034
result as List<Object?>;
3135
return IntEvent(
3236
data: result[0]! as int,
3337
);
3438
}
39+
40+
@override
41+
// ignore: avoid_equals_and_hash_code_on_mutable_classes
42+
bool operator ==(Object other) {
43+
if (other is! IntEvent || other.runtimeType != runtimeType) {
44+
return false;
45+
}
46+
if (identical(this, other)) {
47+
return true;
48+
}
49+
return data == other.data;
50+
}
51+
52+
@override
53+
// ignore: avoid_equals_and_hash_code_on_mutable_classes
54+
int get hashCode => Object.hashAll(_toList());
3555
}
3656

3757
class StringEvent extends PlatformEvent {
@@ -41,18 +61,38 @@ class StringEvent extends PlatformEvent {
4161

4262
String data;
4363

44-
Object encode() {
64+
List<Object?> _toList() {
4565
return <Object?>[
4666
data,
4767
];
4868
}
4969

70+
Object encode() {
71+
return _toList();
72+
}
73+
5074
static StringEvent decode(Object result) {
5175
result as List<Object?>;
5276
return StringEvent(
5377
data: result[0]! as String,
5478
);
5579
}
80+
81+
@override
82+
// ignore: avoid_equals_and_hash_code_on_mutable_classes
83+
bool operator ==(Object other) {
84+
if (other is! StringEvent || other.runtimeType != runtimeType) {
85+
return false;
86+
}
87+
if (identical(this, other)) {
88+
return true;
89+
}
90+
return data == other.data;
91+
}
92+
93+
@override
94+
// ignore: avoid_equals_and_hash_code_on_mutable_classes
95+
int get hashCode => Object.hashAll(_toList());
5696
}
5797

5898
class _PigeonCodec extends StandardMessageCodec {

packages/pigeon/example/app/lib/src/messages.g.dart

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ List<Object?> wrapResponse(
2929
return <Object?>[error.code, error.message, error.details];
3030
}
3131

32+
bool _deepEquals(Object? a, Object? b) {
33+
if (a is List && b is List) {
34+
return a.length == b.length &&
35+
a.indexed
36+
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
37+
}
38+
if (a is Map && b is Map) {
39+
final Iterable<Object?> keys = (a as Map<Object?, Object?>).keys;
40+
return a.length == b.length &&
41+
keys.every((Object? key) =>
42+
(b as Map<Object?, Object?>).containsKey(key) &&
43+
_deepEquals(a[key], b[key]));
44+
}
45+
return a == b;
46+
}
47+
3248
enum Code {
3349
one,
3450
two,
@@ -50,7 +66,7 @@ class MessageData {
5066

5167
Map<String, String> data;
5268

53-
Object encode() {
69+
List<Object?> _toList() {
5470
return <Object?>[
5571
name,
5672
description,
@@ -59,6 +75,10 @@ class MessageData {
5975
];
6076
}
6177

78+
Object encode() {
79+
return _toList();
80+
}
81+
6282
static MessageData decode(Object result) {
6383
result as List<Object?>;
6484
return MessageData(
@@ -68,6 +88,25 @@ class MessageData {
6888
data: (result[3] as Map<Object?, Object?>?)!.cast<String, String>(),
6989
);
7090
}
91+
92+
@override
93+
// ignore: avoid_equals_and_hash_code_on_mutable_classes
94+
bool operator ==(Object other) {
95+
if (other is! MessageData || other.runtimeType != runtimeType) {
96+
return false;
97+
}
98+
if (identical(this, other)) {
99+
return true;
100+
}
101+
return name == other.name &&
102+
description == other.description &&
103+
code == other.code &&
104+
_deepEquals(data, other.data);
105+
}
106+
107+
@override
108+
// ignore: avoid_equals_and_hash_code_on_mutable_classes
109+
int get hashCode => Object.hashAll(_toList());
71110
}
72111

73112
class _PigeonCodec extends StandardMessageCodec {

packages/pigeon/lib/src/dart/dart_generator.dart

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ class DartGenerator extends StructuredGenerator<InternalDartOptions> {
214214
indent.writeln('$datatype ${field.name};');
215215
indent.newln();
216216
}
217+
_writeToList(indent, classDefinition);
218+
indent.newln();
217219
writeClassEncode(
218220
generatorOptions,
219221
root,
@@ -229,6 +231,14 @@ class DartGenerator extends StructuredGenerator<InternalDartOptions> {
229231
classDefinition,
230232
dartPackageName: dartPackageName,
231233
);
234+
indent.newln();
235+
writeClassEquality(
236+
generatorOptions,
237+
root,
238+
indent,
239+
classDefinition,
240+
dartPackageName: dartPackageName,
241+
);
232242
});
233243
}
234244

@@ -248,6 +258,17 @@ class DartGenerator extends StructuredGenerator<InternalDartOptions> {
248258
});
249259
}
250260

261+
void _writeToList(Indent indent, Class classDefinition) {
262+
indent.writeScoped('List<Object?> _toList() {', '}', () {
263+
indent.writeScoped('return <Object?>[', '];', () {
264+
for (final NamedType field
265+
in getFieldsInSerializationOrder(classDefinition)) {
266+
indent.writeln('${field.name},');
267+
}
268+
});
269+
});
270+
}
271+
251272
@override
252273
void writeClassEncode(
253274
InternalDartOptions generatorOptions,
@@ -259,14 +280,8 @@ class DartGenerator extends StructuredGenerator<InternalDartOptions> {
259280
indent.write('Object encode() ');
260281
indent.addScoped('{', '}', () {
261282
indent.write(
262-
'return <Object?>',
283+
'return _toList();',
263284
);
264-
indent.addScoped('[', '];', () {
265-
for (final NamedType field
266-
in getFieldsInSerializationOrder(classDefinition)) {
267-
indent.writeln('${field.name},');
268-
}
269-
});
270285
});
271286
}
272287

@@ -317,6 +332,48 @@ class DartGenerator extends StructuredGenerator<InternalDartOptions> {
317332
});
318333
}
319334

335+
@override
336+
void writeClassEquality(
337+
InternalDartOptions generatorOptions,
338+
Root root,
339+
Indent indent,
340+
Class classDefinition, {
341+
required String dartPackageName,
342+
}) {
343+
indent.writeln('@override');
344+
indent.writeln('// ignore: avoid_equals_and_hash_code_on_mutable_classes');
345+
indent.writeScoped('bool operator ==(Object other) {', '}', () {
346+
indent.writeScoped(
347+
'if (other is! ${classDefinition.name} || other.runtimeType != runtimeType) {',
348+
'}', () {
349+
indent.writeln('return false;');
350+
});
351+
indent.writeScoped('if (identical(this, other)) {', '}', () {
352+
indent.writeln('return true;');
353+
});
354+
indent.writeScoped('return ', '', () {
355+
indent.format(
356+
classDefinition.fields
357+
.map((NamedType field) => field.type.baseName == 'List' ||
358+
field.type.baseName == 'Float64List' ||
359+
field.type.baseName == 'Int32List' ||
360+
field.type.baseName == 'Int64List' ||
361+
field.type.baseName == 'Uint8List' ||
362+
field.type.baseName == 'Map'
363+
? '_deepEquals(${field.name}, other.${field.name})'
364+
: '${field.name} == other.${field.name}')
365+
.join('\n&& '),
366+
trailingNewline: false);
367+
indent.addln(';');
368+
}, addTrailingNewline: false);
369+
});
370+
indent.newln();
371+
indent.writeln('@override');
372+
indent.writeln('// ignore: avoid_equals_and_hash_code_on_mutable_classes');
373+
indent.writeln('int get hashCode => Object.hashAll(_toList())');
374+
indent.addln(';');
375+
}
376+
320377
@override
321378
void writeGeneralCodec(
322379
InternalDartOptions generatorOptions,
@@ -1031,6 +1088,13 @@ final BinaryMessenger? ${varNamePrefix}binaryMessenger;
10311088
generatorOptions.testOut != null) {
10321089
_writeWrapResponse(generatorOptions, root, indent);
10331090
}
1091+
if (root.classes.isNotEmpty &&
1092+
root.classes.any((Class dataClass) => dataClass.fields.any(
1093+
(NamedType field) =>
1094+
field.type.baseName.startsWith('List') ||
1095+
field.type.baseName.startsWith('Map')))) {
1096+
_writeDeepEquals(indent);
1097+
}
10341098
}
10351099

10361100
/// Writes [wrapResponse] method.
@@ -1050,6 +1114,25 @@ final BinaryMessenger? ${varNamePrefix}binaryMessenger;
10501114
});
10511115
}
10521116

1117+
void _writeDeepEquals(Indent indent) {
1118+
indent.format(r'''
1119+
bool _deepEquals(Object? a, Object? b) {
1120+
if (a is List && b is List) {
1121+
return a.length == b.length &&
1122+
a.indexed
1123+
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
1124+
}
1125+
if (a is Map && b is Map) {
1126+
final Iterable<Object?> keys = (a as Map<Object?, Object?>).keys;
1127+
return a.length == b.length && keys.every((Object? key) =>
1128+
(b as Map<Object?, Object?>).containsKey(key) &&
1129+
_deepEquals(a[key], b[key]));
1130+
}
1131+
return a == b;
1132+
}
1133+
''');
1134+
}
1135+
10531136
void _writeCreateConnectionError(Indent indent) {
10541137
indent.newln();
10551138
indent.format('''

packages/pigeon/lib/src/generator.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,15 @@ abstract class StructuredGenerator<InternalOptions>
251251
required String dartPackageName,
252252
}) {}
253253

254+
/// Writes a single class decode method to [indent].
255+
void writeClassEquality(
256+
InternalOptions generatorOptions,
257+
Root root,
258+
Indent indent,
259+
Class classDefinition, {
260+
required String dartPackageName,
261+
}) {}
262+
254263
/// Writes all apis to [indent].
255264
///
256265
/// Can be overridden to add extra code before/after classes.

packages/pigeon/lib/src/generator_tools.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import 'ast.dart';
1414
/// The current version of pigeon.
1515
///
1616
/// This must match the version in pubspec.yaml.
17-
const String pigeonVersion = '25.0.0';
17+
const String pigeonVersion = '25.1.0';
1818

1919
/// Read all the content from [stdin] to a String.
2020
String readStdin() {
@@ -119,7 +119,7 @@ class Indent {
119119
_sink.write(begin + newline);
120120
}
121121
nest(nestCount, func);
122-
if (end != null) {
122+
if (end != null && end.isNotEmpty) {
123123
_sink.write(str() + end);
124124
if (addTrailingNewline) {
125125
_sink.write(newline);
@@ -130,12 +130,18 @@ class Indent {
130130
/// Like `addScoped` but writes the current indentation level.
131131
void writeScoped(
132132
String? begin,
133-
String end,
133+
String? end,
134134
Function func, {
135+
int nestCount = 1,
135136
bool addTrailingNewline = true,
136137
}) {
137-
addScoped(str() + (begin ?? ''), end, func,
138-
addTrailingNewline: addTrailingNewline);
138+
addScoped(
139+
str() + (begin ?? ''),
140+
end,
141+
func,
142+
nestCount: nestCount,
143+
addTrailingNewline: addTrailingNewline,
144+
);
139145
}
140146

141147
/// Scoped increase of the indent level.

0 commit comments

Comments
 (0)