Skip to content

Commit

Permalink
[pigeon] Implement equals for Java data classes (#6992)
Browse files Browse the repository at this point in the history
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 flutter/flutter#118087
  • Loading branch information
stuartmorgan authored Jun 27, 2024
1 parent 1612774 commit 50ad6ee
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 7 deletions.
5 changes: 3 additions & 2 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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"})
Expand Down Expand Up @@ -132,6 +133,26 @@ public void setData(@NonNull Map<String, String> setterArg) {
/** 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;
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
62 changes: 62 additions & 0 deletions packages/pigeon/lib/java_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -233,6 +234,7 @@ class JavaGenerator extends StructuredGenerator<JavaOptions> {
indent.writeln('${classDefinition.name}() {}');
indent.newln();
}
_writeEquality(indent, classDefinition);

_writeClassBuilder(generatorOptions, root, indent, classDefinition);
writeClassEncode(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down
Loading

0 comments on commit 50ad6ee

Please sign in to comment.