codegen-java/kotlin: Fix generation of hashCode methods (#802)

codegen-java:
- use `pattern.pattern()` instead of `pattern` in hashCode method
  (consistent with equals method)

codegen-kotlin:
- use `regex.pattern` instead of `regex` in hashCode method
  (consistent with equals method)
- if a data class has a Regex property, generate not only
  an equals method but also a hashCode method
This commit is contained in:
translatenix
2024-11-13 15:42:45 -08:00
committed by GitHub
parent b8d90eddec
commit df38011c9e
5 changed files with 39 additions and 16 deletions

View File

@@ -221,6 +221,9 @@ class JavaCodeGenerator(
val properties = renameIfReservedWord(pClass.properties).filterValues { !it.isHidden } val properties = renameIfReservedWord(pClass.properties).filterValues { !it.isHidden }
val allProperties = superProperties + properties val allProperties = superProperties + properties
fun PClass.Property.isRegex(): Boolean =
(this.type as? PType.Class)?.pClass?.info == PClassInfo.Regex
fun addCtorParameter( fun addCtorParameter(
builder: MethodSpec.Builder, builder: MethodSpec.Builder,
propJavaName: String, propJavaName: String,
@@ -283,9 +286,7 @@ class JavaCodeGenerator(
.addStatement("\$T other = (\$T) obj", javaPoetClassName, javaPoetClassName) .addStatement("\$T other = (\$T) obj", javaPoetClassName, javaPoetClassName)
for ((propertyName, property) in allProperties) { for ((propertyName, property) in allProperties) {
val accessor = val accessor = if (property.isRegex()) "\$N.pattern()" else "\$N"
if ((property.type as? PType.Class)?.pClass?.info == PClassInfo.Regex) "\$N.pattern()"
else "\$N"
builder.addStatement( builder.addStatement(
"if (!\$T.equals(this.$accessor, other.$accessor)) return false", "if (!\$T.equals(this.$accessor, other.$accessor)) return false",
Objects::class.java, Objects::class.java,
@@ -306,9 +307,10 @@ class JavaCodeGenerator(
.returns(Int::class.java) .returns(Int::class.java)
.addStatement("int result = 1") .addStatement("int result = 1")
for (propertyName in allProperties.keys) { for ((propertyName, property) in allProperties) {
val accessor = if (property.isRegex()) "this.\$N.pattern()" else "this.\$N"
builder.addStatement( builder.addStatement(
"result = 31 * result + \$T.hashCode(this.\$N)", "result = 31 * result + \$T.hashCode($accessor)",
Objects::class.java, Objects::class.java,
propertyName propertyName
) )

View File

@@ -306,7 +306,7 @@ public final class Mod {
result = 31 * result + Objects.hashCode(this.container); result = 31 * result + Objects.hashCode(this.container);
result = 31 * result + Objects.hashCode(this.container2); result = 31 * result + Objects.hashCode(this.container2);
result = 31 * result + Objects.hashCode(this.other); result = 31 * result + Objects.hashCode(this.other);
result = 31 * result + Objects.hashCode(this.regex); result = 31 * result + Objects.hashCode(this.regex.pattern());
result = 31 * result + Objects.hashCode(this.any); result = 31 * result + Objects.hashCode(this.any);
result = 31 * result + Objects.hashCode(this.nonNull); result = 31 * result + Objects.hashCode(this.nonNull);
result = 31 * result + Objects.hashCode(this._enum); result = 31 * result + Objects.hashCode(this._enum);

View File

@@ -227,8 +227,6 @@ class KotlinCodeGenerator(
fun PClass.Property.isRegex(): Boolean = fun PClass.Property.isRegex(): Boolean =
(this.type as? PType.Class)?.pClass?.info == PClassInfo.Regex (this.type as? PType.Class)?.pClass?.info == PClassInfo.Regex
val containRegexProperty = properties.values.any { it.isRegex() }
fun generateConstructor(): FunSpec { fun generateConstructor(): FunSpec {
val builder = FunSpec.constructorBuilder() val builder = FunSpec.constructorBuilder()
for ((name, property) in allProperties) { for ((name, property) in allProperties) {
@@ -324,11 +322,12 @@ class KotlinCodeGenerator(
.returns(INT) .returns(INT)
.addStatement("var result = 1") .addStatement("var result = 1")
for (propertyName in allProperties.keys) { for ((propertyName, property) in allProperties) {
val accessor = if (property.isRegex()) "this.%N.pattern" else "this.%N"
// use Objects.hashCode() because Kotlin's Any?.hashCode() // use Objects.hashCode() because Kotlin's Any?.hashCode()
// doesn't work for platform types (will get NPE if null) // doesn't work for platform types (will get NPE if null)
builder.addStatement( builder.addStatement(
"result = 31 * result + %T.hashCode(this.%N)", "result = 31 * result + %T.hashCode($accessor)",
Objects::class, Objects::class,
propertyName propertyName
) )
@@ -490,15 +489,17 @@ class KotlinCodeGenerator(
builder.addKdoc(renderAsKdoc(docComment)) builder.addKdoc(renderAsKdoc(docComment))
} }
var hasRegex = false
for ((name, property) in properties) { for ((name, property) in properties) {
hasRegex = hasRegex || property.isRegex()
builder.addProperty(generateProperty(name, property)) builder.addProperty(generateProperty(name, property))
} }
// Regex requires special approach when compared to another Regex // kotlin.text.Regex (and java.util.regex.Pattern) defines equality as identity.
// So we need to override `.equals` method even for kotlin's `data class`es if // To match Pkl semantics and compare regexes by their String pattern,
// any of the properties is of Regex type // override equals and hashCode if the data class has a property of type Regex.
if (containRegexProperty) { if (hasRegex) {
builder.addFunction(generateEqualsMethod()) builder.addFunction(generateEqualsMethod()).addFunction(generateHashCodeMethod())
} }
return builder return builder

View File

@@ -561,6 +561,26 @@ class KotlinCodeGeneratorTest {
) )
} }
@Test
fun `data class with Regex property has custom equals and hashCode methods`() {
val kotlinCode =
generateKotlinCode(
"""
module my.mod
class Person {
age: Int
name: Regex
}
"""
.trimIndent()
)
assertThat(kotlinCode)
.contains("if (this.name.pattern != other.name.pattern) return false")
.contains("result = 31 * result + Objects.hashCode(this.name.pattern)")
}
@Test @Test
fun `recursive types`() { fun `recursive types`() {
val kotlinCode = val kotlinCode =

View File

@@ -139,7 +139,7 @@ object Mod {
result = 31 * result + Objects.hashCode(this.container) result = 31 * result + Objects.hashCode(this.container)
result = 31 * result + Objects.hashCode(this.container2) result = 31 * result + Objects.hashCode(this.container2)
result = 31 * result + Objects.hashCode(this.other) result = 31 * result + Objects.hashCode(this.other)
result = 31 * result + Objects.hashCode(this.regex) result = 31 * result + Objects.hashCode(this.regex.pattern)
result = 31 * result + Objects.hashCode(this.any) result = 31 * result + Objects.hashCode(this.any)
result = 31 * result + Objects.hashCode(this.nonNull) result = 31 * result + Objects.hashCode(this.nonNull)
result = 31 * result + Objects.hashCode(this.enum) result = 31 * result + Objects.hashCode(this.enum)