[pigeon] Implement equals for Java data classes (#6992)

Adds implementations of `equals` and `hashCode` to Java data classes. This is frequently useful for native unit tests of plugins using Pigeon (e.g., when using a mock FlutterApi implementation to check that the expected call is being made with the right arguments).

Part of https://github.com/flutter/flutter/issues/118087
This commit is contained in:
stuartmorgan
2024-06-26 20:08:00 -04:00
committed by GitHub
parent 161277461a
commit 50ad6ee0da
7 changed files with 311 additions and 7 deletions

View File

@ -1,5 +1,6 @@
## NEXT
## 20.0.2
* [java] Adds `equals` and `hashCode` support for data classes.
* [swift] Fully-qualifies types in Equatable extension test.
## 20.0.1

View File

@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Generated class from Pigeon. */
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
@ -132,6 +133,26 @@ public class Messages {
/** Constructor is non-public to enforce null safety; use Builder. */
MessageData() {}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MessageData that = (MessageData) o;
return Objects.equals(name, that.name)
&& Objects.equals(description, that.description)
&& code.equals(that.code)
&& data.equals(that.data);
}
@Override
public int hashCode() {
return Objects.hash(name, description, code, data);
}
public static final class Builder {
private @Nullable String name;

View File

@ -13,7 +13,7 @@ import 'ast.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '20.0.1';
const String pigeonVersion = '20.0.2';
/// Prefix for all local variables in methods.
///

View File

@ -141,6 +141,7 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
indent.writeln('import java.util.HashMap;');
indent.writeln('import java.util.List;');
indent.writeln('import java.util.Map;');
indent.writeln('import java.util.Objects;');
indent.newln();
}
@ -233,6 +234,7 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
indent.writeln('${classDefinition.name}() {}');
indent.newln();
}
_writeEquality(indent, classDefinition);
_writeClassBuilder(generatorOptions, root, indent, classDefinition);
writeClassEncode(
@ -282,6 +284,62 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
});
}
void _writeEquality(Indent indent, Class classDefinition) {
// Implement equals(...).
indent.writeln('@Override');
indent.writeScoped('public boolean equals(Object o) {', '}', () {
indent.writeln('if (this == o) { return true; }');
indent.writeln(
'if (o == null || getClass() != o.getClass()) { return false; }');
indent.writeln(
'${classDefinition.name} that = (${classDefinition.name}) o;');
final Iterable<String> checks = classDefinition.fields.map(
(NamedType field) {
// Objects.equals only does pointer equality for array types.
if (_javaTypeIsArray(field.type)) {
return 'Arrays.equals(${field.name}, that.${field.name})';
}
return field.type.isNullable
? 'Objects.equals(${field.name}, that.${field.name})'
: '${field.name}.equals(that.${field.name})';
},
);
indent.writeln('return ${checks.join(' && ')};');
});
indent.newln();
// Implement hashCode().
indent.writeln('@Override');
indent.writeScoped('public int hashCode() {', '}', () {
// As with equalty checks, arrays need special handling.
final Iterable<String> arrayFieldNames = classDefinition.fields
.where((NamedType field) => _javaTypeIsArray(field.type))
.map((NamedType field) => field.name);
final Iterable<String> nonArrayFieldNames = classDefinition.fields
.where((NamedType field) => !_javaTypeIsArray(field.type))
.map((NamedType field) => field.name);
final String nonArrayHashValue = nonArrayFieldNames.isNotEmpty
? 'Objects.hash(${nonArrayFieldNames.join(', ')})'
: '0';
if (arrayFieldNames.isEmpty) {
// Return directly if there are no array variables, to avoid redundant
// variable lint warnings.
indent.writeln('return $nonArrayHashValue;');
} else {
const String resultVar = '${varNamePrefix}result';
indent.writeln('int $resultVar = $nonArrayHashValue;');
// Manually mix in the Arrays.hashCode values.
for (final String name in arrayFieldNames) {
indent.writeln(
'$resultVar = 31 * $resultVar + Arrays.hashCode($name);');
}
indent.writeln('return $resultVar;');
}
});
indent.newln();
}
void _writeClassBuilder(
JavaOptions generatorOptions,
Root root,
@ -1022,6 +1080,10 @@ String _javaTypeForBuiltinGenericDartType(
}
}
bool _javaTypeIsArray(TypeDeclaration type) {
return _javaTypeForBuiltinDartType(type)?.endsWith('[]') ?? false;
}
String? _javaTypeForBuiltinDartType(TypeDeclaration type) {
const Map<String, String> javaTypeForDartTypeMap = <String, String>{
'bool': 'Boolean',

View File

@ -26,6 +26,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Generated class from Pigeon. */
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
@ -318,6 +319,58 @@ public class CoreTests {
/** Constructor is non-public to enforce null safety; use Builder. */
AllTypes() {}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AllTypes that = (AllTypes) o;
return aBool.equals(that.aBool)
&& anInt.equals(that.anInt)
&& anInt64.equals(that.anInt64)
&& aDouble.equals(that.aDouble)
&& Arrays.equals(aByteArray, that.aByteArray)
&& Arrays.equals(a4ByteArray, that.a4ByteArray)
&& Arrays.equals(a8ByteArray, that.a8ByteArray)
&& Arrays.equals(aFloatArray, that.aFloatArray)
&& anEnum.equals(that.anEnum)
&& aString.equals(that.aString)
&& anObject.equals(that.anObject)
&& list.equals(that.list)
&& stringList.equals(that.stringList)
&& intList.equals(that.intList)
&& doubleList.equals(that.doubleList)
&& boolList.equals(that.boolList)
&& map.equals(that.map);
}
@Override
public int hashCode() {
int __pigeon_result =
Objects.hash(
aBool,
anInt,
anInt64,
aDouble,
anEnum,
aString,
anObject,
list,
stringList,
intList,
doubleList,
boolList,
map);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(a4ByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(a8ByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aFloatArray);
return __pigeon_result;
}
public static final class Builder {
private @Nullable Boolean aBool;
@ -772,6 +825,68 @@ public class CoreTests {
this.map = setterArg;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AllNullableTypes that = (AllNullableTypes) o;
return Objects.equals(aNullableBool, that.aNullableBool)
&& Objects.equals(aNullableInt, that.aNullableInt)
&& Objects.equals(aNullableInt64, that.aNullableInt64)
&& Objects.equals(aNullableDouble, that.aNullableDouble)
&& Arrays.equals(aNullableByteArray, that.aNullableByteArray)
&& Arrays.equals(aNullable4ByteArray, that.aNullable4ByteArray)
&& Arrays.equals(aNullable8ByteArray, that.aNullable8ByteArray)
&& Arrays.equals(aNullableFloatArray, that.aNullableFloatArray)
&& Objects.equals(nullableNestedList, that.nullableNestedList)
&& Objects.equals(nullableMapWithAnnotations, that.nullableMapWithAnnotations)
&& Objects.equals(nullableMapWithObject, that.nullableMapWithObject)
&& Objects.equals(aNullableEnum, that.aNullableEnum)
&& Objects.equals(aNullableString, that.aNullableString)
&& Objects.equals(aNullableObject, that.aNullableObject)
&& Objects.equals(allNullableTypes, that.allNullableTypes)
&& Objects.equals(list, that.list)
&& Objects.equals(stringList, that.stringList)
&& Objects.equals(intList, that.intList)
&& Objects.equals(doubleList, that.doubleList)
&& Objects.equals(boolList, that.boolList)
&& Objects.equals(nestedClassList, that.nestedClassList)
&& Objects.equals(map, that.map);
}
@Override
public int hashCode() {
int __pigeon_result =
Objects.hash(
aNullableBool,
aNullableInt,
aNullableInt64,
aNullableDouble,
nullableNestedList,
nullableMapWithAnnotations,
nullableMapWithObject,
aNullableEnum,
aNullableString,
aNullableObject,
allNullableTypes,
list,
stringList,
intList,
doubleList,
boolList,
nestedClassList,
map);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullableByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullable4ByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullable8ByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullableFloatArray);
return __pigeon_result;
}
public static final class Builder {
private @Nullable Boolean aNullableBool;
@ -1272,6 +1387,64 @@ public class CoreTests {
this.map = setterArg;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AllNullableTypesWithoutRecursion that = (AllNullableTypesWithoutRecursion) o;
return Objects.equals(aNullableBool, that.aNullableBool)
&& Objects.equals(aNullableInt, that.aNullableInt)
&& Objects.equals(aNullableInt64, that.aNullableInt64)
&& Objects.equals(aNullableDouble, that.aNullableDouble)
&& Arrays.equals(aNullableByteArray, that.aNullableByteArray)
&& Arrays.equals(aNullable4ByteArray, that.aNullable4ByteArray)
&& Arrays.equals(aNullable8ByteArray, that.aNullable8ByteArray)
&& Arrays.equals(aNullableFloatArray, that.aNullableFloatArray)
&& Objects.equals(nullableNestedList, that.nullableNestedList)
&& Objects.equals(nullableMapWithAnnotations, that.nullableMapWithAnnotations)
&& Objects.equals(nullableMapWithObject, that.nullableMapWithObject)
&& Objects.equals(aNullableEnum, that.aNullableEnum)
&& Objects.equals(aNullableString, that.aNullableString)
&& Objects.equals(aNullableObject, that.aNullableObject)
&& Objects.equals(list, that.list)
&& Objects.equals(stringList, that.stringList)
&& Objects.equals(intList, that.intList)
&& Objects.equals(doubleList, that.doubleList)
&& Objects.equals(boolList, that.boolList)
&& Objects.equals(map, that.map);
}
@Override
public int hashCode() {
int __pigeon_result =
Objects.hash(
aNullableBool,
aNullableInt,
aNullableInt64,
aNullableDouble,
nullableNestedList,
nullableMapWithAnnotations,
nullableMapWithObject,
aNullableEnum,
aNullableString,
aNullableObject,
list,
stringList,
intList,
doubleList,
boolList,
map);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullableByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullable4ByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullable8ByteArray);
__pigeon_result = 31 * __pigeon_result + Arrays.hashCode(aNullableFloatArray);
return __pigeon_result;
}
public static final class Builder {
private @Nullable Boolean aNullableBool;
@ -1589,6 +1762,25 @@ public class CoreTests {
/** Constructor is non-public to enforce null safety; use Builder. */
AllClassesWrapper() {}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AllClassesWrapper that = (AllClassesWrapper) o;
return allNullableTypes.equals(that.allNullableTypes)
&& Objects.equals(allNullableTypesWithoutRecursion, that.allNullableTypesWithoutRecursion)
&& Objects.equals(allTypes, that.allTypes);
}
@Override
public int hashCode() {
return Objects.hash(allNullableTypes, allNullableTypesWithoutRecursion, allTypes);
}
public static final class Builder {
private @Nullable AllNullableTypes allNullableTypes;
@ -1663,6 +1855,23 @@ public class CoreTests {
this.testList = setterArg;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TestMessage that = (TestMessage) o;
return Objects.equals(testList, that.testList);
}
@Override
public int hashCode() {
return Objects.hash(testList);
}
public static final class Builder {
private @Nullable List<Object> testList;

View File

@ -24,10 +24,10 @@ public class AllDatatypesTest {
if (firstTypes == null || secondTypes == null) {
return;
}
// Check all the fields individually to ensure that everything is as expected.
assertEquals(firstTypes.getABool(), secondTypes.getABool());
assertEquals(firstTypes.getAnInt(), secondTypes.getAnInt());
assertEquals(firstTypes.getAnInt64(), secondTypes.getAnInt64());
assertEquals(firstTypes.getADouble(), secondTypes.getADouble());
assertArrayEquals(firstTypes.getAByteArray(), secondTypes.getAByteArray());
assertArrayEquals(firstTypes.getA4ByteArray(), secondTypes.getA4ByteArray());
@ -44,6 +44,10 @@ public class AllDatatypesTest {
firstTypes.getMap().keySet().toArray(), secondTypes.getMap().keySet().toArray());
assertArrayEquals(
firstTypes.getMap().values().toArray(), secondTypes.getMap().values().toArray());
// Also check that the implementation of equality works.
assertEquals(firstTypes, secondTypes);
assertEquals(firstTypes.hashCode(), secondTypes.hashCode());
}
void compareAllNullableTypes(AllNullableTypes firstTypes, AllNullableTypes secondTypes) {
@ -51,6 +55,7 @@ public class AllDatatypesTest {
if (firstTypes == null || secondTypes == null) {
return;
}
// Check all the fields individually to ensure that everything is as expected.
assertEquals(firstTypes.getANullableBool(), secondTypes.getANullableBool());
assertEquals(firstTypes.getANullableInt(), secondTypes.getANullableInt());
assertEquals(firstTypes.getANullableDouble(), secondTypes.getANullableDouble());
@ -74,6 +79,10 @@ public class AllDatatypesTest {
firstTypes.getMap().keySet().toArray(), secondTypes.getMap().keySet().toArray());
assertArrayEquals(
firstTypes.getMap().values().toArray(), secondTypes.getMap().values().toArray());
// Also check that the implementation of equality works.
assertEquals(firstTypes, secondTypes);
assertEquals(firstTypes.hashCode(), secondTypes.hashCode());
}
@Test
@ -152,6 +161,8 @@ public class AllDatatypesTest {
@Test
public void hasValues() {
// Not inline due to warnings about an ambiguous varargs call when inline.
final Object[] genericList = new Boolean[] {true, false};
AllTypes allEverything =
new AllTypes.Builder()
.setABool(false)
@ -168,7 +179,7 @@ public class AllDatatypesTest {
.setBoolList(Arrays.asList(new Boolean[] {true, false}))
.setDoubleList(Arrays.asList(new Double[] {0.5, 0.25, 1.5, 1.25}))
.setIntList(Arrays.asList(new Long[] {1l, 2l, 3l, 4l}))
.setList(Arrays.asList(new int[] {1, 2, 3, 4}))
.setList(Arrays.asList(genericList))
.setStringList(Arrays.asList(new String[] {"string", "another one"}))
.setMap(makeMap("hello", 1234))
.build();
@ -188,7 +199,7 @@ public class AllDatatypesTest {
.setBoolList(Arrays.asList(new Boolean[] {true, false}))
.setDoubleList(Arrays.asList(new Double[] {0.5, 0.25, 1.5, 1.25}))
.setIntList(Arrays.asList(new Long[] {1l, 2l, 3l, 4l}))
.setList(Arrays.asList(new int[] {1, 2, 3, 4}))
.setList(Arrays.asList(genericList))
.setStringList(Arrays.asList(new String[] {"string", "another one"}))
.setMap(makeMap("hello", 1234))
.build();

View File

@ -2,7 +2,7 @@ name: pigeon
description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
repository: https://github.com/flutter/packages/tree/main/packages/pigeon
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22
version: 20.0.1 # This must match the version in lib/generator_tools.dart
version: 20.0.2 # This must match the version in lib/generator_tools.dart
environment:
sdk: ^3.2.0