diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 6e039a9236..e13c7e3c4d 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -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 @@ -17,7 +18,7 @@ ## 19.0.2 -* [kotlin] Adds the `@JvmOverloads` to the `HostApi` setUp method. This prevents the calling Java code from having to provide an empty `String` as Kotlin provides it by default +* [kotlin] Adds the `@JvmOverloads` to the `HostApi` setUp method. This prevents the calling Java code from having to provide an empty `String` as Kotlin provides it by default ## 19.0.1 diff --git a/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java b/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java index 4993909460..8b07206873 100644 --- a/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java +++ b/packages/pigeon/example/app/android/app/src/main/java/io/flutter/plugins/Messages.java @@ -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; diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 7c43243b0f..c78761ecef 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -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. /// diff --git a/packages/pigeon/lib/java_generator.dart b/packages/pigeon/lib/java_generator.dart index 55622995b7..329bf37dab 100644 --- a/packages/pigeon/lib/java_generator.dart +++ b/packages/pigeon/lib/java_generator.dart @@ -141,6 +141,7 @@ class JavaGenerator extends StructuredGenerator { 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 { indent.writeln('${classDefinition.name}() {}'); indent.newln(); } + _writeEquality(indent, classDefinition); _writeClassBuilder(generatorOptions, root, indent, classDefinition); writeClassEncode( @@ -282,6 +284,62 @@ class JavaGenerator extends StructuredGenerator { }); } + 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 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 arrayFieldNames = classDefinition.fields + .where((NamedType field) => _javaTypeIsArray(field.type)) + .map((NamedType field) => field.name); + final Iterable 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 javaTypeForDartTypeMap = { 'bool': 'Boolean', diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java index d0b5b60083..d7c5ea64ff 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/main/java/com/example/alternate_language_test_plugin/CoreTests.java @@ -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 testList; diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/AllDatatypesTest.java b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/AllDatatypesTest.java index c5f7857915..20146fdc0e 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/AllDatatypesTest.java +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/src/test/java/com/example/alternate_language_test_plugin/AllDatatypesTest.java @@ -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(); diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 413804a069..411d3bdb58 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -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