Skip to content

Commit

Permalink
AutoBuilder support for Kotlin default constructor parameters.
Browse files Browse the repository at this point in the history
If you have a class whose principal constructor has default parameters, and you make a builder for it, then you can call `build()` without having set a value for some default parameters. They will then get the default values specified in the constructor.

RELNOTES=AutoBuilder now allows you to omit a Kotlin default parameter when building a Kotlin class.
PiperOrigin-RevId: 436871835
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed Mar 24, 2022
1 parent 5e42336 commit d2f91bf
Show file tree
Hide file tree
Showing 12 changed files with 686 additions and 146 deletions.
9 changes: 9 additions & 0 deletions value/processor/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-metadata-jvm</artifactId>
<version>0.4.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
Expand Down Expand Up @@ -232,6 +237,10 @@
<exclude>com.google.code.findbugs:jsr305</exclude>
</excludes>
</artifactSet>
<transformers>
<!-- Needed to avoid "No MetadataExtensions instances found in the classpath" from Kotlin reflection. -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<relocations>
<relocation>
<pattern>org.objectweb</pattern>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package com.google.auto.value;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
Expand All @@ -41,6 +43,7 @@ public void simpleKotlin() {
KotlinData x = KotlinDataBuilder.builder().setInt(23).setString("skidoo").build();
assertThat(x.getInt()).isEqualTo(23);
assertThat(x.getString()).isEqualTo("skidoo");
assertThrows(IllegalStateException.class, () -> KotlinDataBuilder.builder().build());
}

@AutoBuilder(ofClass = KotlinDataWithNullable.class)
Expand Down Expand Up @@ -76,24 +79,105 @@ static KotlinDataWithDefaultsBuilder builder() {

abstract KotlinDataWithDefaultsBuilder setAnInt(int x);

abstract int getAnInt();

abstract ImmutableList.Builder<String> anImmutableListBuilder();

abstract KotlinDataWithDefaultsBuilder setNotDefaulted(long x);

abstract long getNotDefaulted();

abstract KotlinDataWithDefaultsBuilder setAString(String x);

abstract String getAString();

abstract KotlinDataWithDefaults build();
}

@Test
public void kotlinWithDefaults() {
// AutoBuilder doesn't currently try to give the builder the same defaults as the Kotlin class,
// but we do at least check that the presence of defaults doesn't throw AutoBuilder off.
// When a constructor has default parameters, the Kotlin compiler generates an extra constructor
// with two extra parameters: an int bitmask saying which parameters were defaulted, and a
// DefaultConstructorMarker parameter to avoid clashing with another constructor that might have
// an extra int parameter for some other reason. If AutoBuilder found this constructor it might
// be confused, but fortunately the constructor is marked synthetic, and javax.lang.model
// doesn't show synthetic elements.
KotlinDataWithDefaults x =
KotlinDataWithDefaultsBuilder.builder().setAString("answer").setAnInt(42).build();
public void kotlinWithDefaults_explicit() {
KotlinDataWithDefaultsBuilder builder =
KotlinDataWithDefaultsBuilder.builder()
.setAString("answer")
.setNotDefaulted(100L)
.setAnInt(42);
builder.anImmutableListBuilder().add("bar");
KotlinDataWithDefaults x = builder.build();
assertThat(x.getAString()).isEqualTo("answer");
assertThat(x.getAnImmutableList()).containsExactly("bar");
assertThat(x.getNotDefaulted()).isEqualTo(100L);
assertThat(x.getAnInt()).isEqualTo(42);
}

@Test
public void kotlinWithDefaults_defaulted() {
KotlinDataWithDefaults x =
KotlinDataWithDefaultsBuilder.builder().setNotDefaulted(100L).build();
assertThat(x.getAnInt()).isEqualTo(23);
assertThat(x.getAnImmutableList()).containsExactly("foo");
assertThat(x.getAString()).isEqualTo("skidoo");
assertThat(x.getNotDefaulted()).isEqualTo(100L);
}

@Test
public void kotlinWithDefaults_getter() {
KotlinDataWithDefaultsBuilder builder = KotlinDataWithDefaultsBuilder.builder();
assertThrows(IllegalStateException.class, builder::getAnInt);
builder.setAnInt(42);
assertThat(builder.getAnInt()).isEqualTo(42);
assertThrows(IllegalStateException.class, builder::getNotDefaulted);
builder.setNotDefaulted(100L);
assertThat(builder.getNotDefaulted()).isEqualTo(100L);
assertThrows(IllegalStateException.class, builder::getAString);
builder.setAString("answer");
assertThat(builder.getAString()).isEqualTo("answer");
}

@AutoBuilder(ofClass = KotlinDataEightDefaults.class)
interface KotlinDataEightDefaultsBuilder {
static KotlinDataEightDefaultsBuilder builder() {
return new AutoBuilder_AutoBuilderKotlinTest_KotlinDataEightDefaultsBuilder();
}

KotlinDataEightDefaultsBuilder a1(int x);

KotlinDataEightDefaultsBuilder a2(int x);

KotlinDataEightDefaultsBuilder a3(int x);

KotlinDataEightDefaultsBuilder a4(int x);

KotlinDataEightDefaultsBuilder a5(int x);

KotlinDataEightDefaultsBuilder a6(int x);

KotlinDataEightDefaultsBuilder a7(int x);

KotlinDataEightDefaultsBuilder a8(int x);

KotlinDataEightDefaults build();
}

// We test a class that has exactly 8 default parameters because we will use a byte for the
// bitmask in that case and it is possible that we might have an issue with sign extension when
// bit 7 of that bitmask is set.
@Test
public void kotlinEightDefaults() {
KotlinDataEightDefaults allDefaulted = KotlinDataEightDefaultsBuilder.builder().build();
assertThat(allDefaulted.getA1()).isEqualTo(1);
assertThat(allDefaulted.getA8()).isEqualTo(8);
KotlinDataEightDefaults noneDefaulted =
KotlinDataEightDefaultsBuilder.builder()
.a1(-1)
.a2(-2)
.a3(-3)
.a4(-4)
.a5(-5)
.a6(-6)
.a7(-7)
.a8(-8)
.build();
assertThat(noneDefaulted.getA1()).isEqualTo(-1);
assertThat(noneDefaulted.getA8()).isEqualTo(-8);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ public static void setSourceRoot() {

private static final ImmutableSet<String> IGNORED_TEST_FILES =
ImmutableSet.of(
"AutoValueNotEclipseTest.java", "CompileWithEclipseTest.java", "GradleTest.java");
"AutoValueNotEclipseTest.java",
"CompileWithEclipseTest.java",
"GradleTest.java",

// AutoBuilder sometimes needs to generate a .class file for Kotlin that is used in the
// rest of compilation, and Eclipse doesn't seem to handle that well. Presumably not many
// Kotlin users use Eclipse since IntelliJ is obviously much more suitable.
"AutoBuilderKotlinTest.java");

private static final Predicate<File> JAVA_FILE =
f -> f.getName().endsWith(".java") && !IGNORED_TEST_FILES.contains(f.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,27 @@
*/
package com.google.auto.value

import com.google.common.collect.ImmutableList

data class KotlinData(val int: Int, val string: String)

data class KotlinDataWithNullable(val anInt: Int?, val aString: String?)

data class KotlinDataWithDefaults(val anInt: Int = 23, val aString: String = "skidoo")
data class KotlinDataWithDefaults(
val anInt: Int = 23,
val anImmutableList: ImmutableList<String> = ImmutableList.of("foo"),
val notDefaulted: Long,
val aString: String = "skidoo"
)

// Exactly 8 defaulted properties, in case we have a problem with sign-extending byte bitmasks.
data class KotlinDataEightDefaults(
val a1: Int = 1,
val a2: Int = 2,
val a3: Int = 3,
val a4: Int = 4,
val a5: Int = 5,
val a6: Int = 6,
val a7: Int = 7,
val a8: Int = 8,
)
Loading

0 comments on commit d2f91bf

Please sign in to comment.