Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for context-receivers #1233

Merged
merged 22 commits into from Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ebd4464
Add API for context receivers
DRSchlaubi Apr 12, 2022
f0d23a2
Use existing opt-in annotation and make context-receivers not nullable
DRSchlaubi Apr 12, 2022
fe97e3f
Remove collection overloads
DRSchlaubi Apr 12, 2022
37aef5e
Revert unwanted code style changes
DRSchlaubi Apr 12, 2022
a505feb
Add wrongly remove @JvmOverloads annotation
DRSchlaubi Apr 12, 2022
f5f0784
Add code generator and tests
DRSchlaubi Apr 12, 2022
6350d54
Apply requested changes
DRSchlaubi Apr 12, 2022
1e0f847
Update kotlinpoet/src/main/java/com/squareup/kotlinpoet/FunSpec.kt
DRSchlaubi Apr 12, 2022
7f61d7a
Apply requested changes
DRSchlaubi Apr 13, 2022
95eba91
Merge branch 'feature/context-receivers' of github.com:DRSchlaubi/kot…
DRSchlaubi Apr 13, 2022
4d5792e
Update test for suggestion
DRSchlaubi Apr 13, 2022
f2ace6f
Apply suggestions from code review
DRSchlaubi Apr 15, 2022
e6316a0
Apply requested changes
DRSchlaubi Apr 15, 2022
1b1a5d9
Rename contextReceiver to contextReceivers
DRSchlaubi Apr 15, 2022
ec31e4e
Fix compiler error
DRSchlaubi Apr 16, 2022
53d0af2
Update kotlinpoet/src/main/java/com/squareup/kotlinpoet/FunSpec.kt
DRSchlaubi Apr 18, 2022
57f9941
Fix compiler errors in tests
DRSchlaubi Apr 18, 2022
cabea6b
Run spotless
DRSchlaubi Apr 18, 2022
ad2ec58
Update kotlinpoet/src/main/java/com/squareup/kotlinpoet/LambdaTypeNam…
DRSchlaubi Apr 18, 2022
92d9956
Apply requested changes
DRSchlaubi Apr 18, 2022
ef4909d
Don't emit context receiver spacing if there are no context receivers
DRSchlaubi Apr 18, 2022
c97af64
Apply suggestions from code review
DRSchlaubi Apr 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.kotlinpoet

import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.PROPERTY
import kotlin.annotation.AnnotationTarget.TYPEALIAS

/**
* Indicates that a given API is part of an experimental KotlinPoet and is
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
* subject to API changes.
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
*/
@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(CLASS, FUNCTION, PROPERTY, TYPEALIAS)
public annotation class ExperimentalKotlinPoetApi
18 changes: 18 additions & 0 deletions kotlinpoet/src/main/java/com/squareup/kotlinpoet/FunSpec.kt
Expand Up @@ -32,6 +32,7 @@ import kotlin.DeprecationLevel.WARNING
import kotlin.reflect.KClass

/** A generated function declaration. */
@OptIn(ExperimentalKotlinPoetApi::class)
public class FunSpec private constructor(
builder: Builder,
private val tagMap: TagMap = builder.buildTagMap(),
Expand All @@ -45,6 +46,9 @@ public class FunSpec private constructor(
public val modifiers: Set<KModifier> = builder.modifiers.toImmutableSet()
public val typeVariables: List<TypeVariableName> = builder.typeVariables.toImmutableList()
public val receiverType: TypeName? = builder.receiverType

@ExperimentalKotlinPoetApi
public val contextReceiverTypes: List<TypeName> = builder.contextReceiverTypes.toImmutableList()
public val returnType: TypeName? = builder.returnType
public val parameters: List<ParameterSpec> = builder.parameters.toImmutableList()
public val delegateConstructor: String? = builder.delegateConstructor
Expand Down Expand Up @@ -84,6 +88,10 @@ public class FunSpec private constructor(
codeWriter.emitKdoc(kdoc.ensureEndsWithNewLine())
}
codeWriter.emitAnnotations(annotations, false)
if (contextReceiverTypes.isNotEmpty()) {
val receivers = contextReceiverTypes.joinToString(", ") { "%T" }
codeWriter.emitCode("context($receivers)\n", *contextReceiverTypes.toTypedArray())
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
}
codeWriter.emitModifiers(modifiers, implicitModifiers)

if (!isConstructor && !name.isAccessor) {
Expand Down Expand Up @@ -285,6 +293,7 @@ public class FunSpec private constructor(
internal var returnKdoc = CodeBlock.EMPTY
internal var receiverKdoc = CodeBlock.EMPTY
internal var receiverType: TypeName? = null
internal val contextReceiverTypes: MutableList<TypeName> = mutableListOf()
internal var returnType: TypeName? = null
internal var delegateConstructor: String? = null
internal var delegateConstructorArguments = listOf<CodeBlock>()
Expand Down Expand Up @@ -359,6 +368,15 @@ public class FunSpec private constructor(
typeVariables += typeVariable
}

@ExperimentalKotlinPoetApi
public fun contextReceiver(receiverTypes: Collection<TypeName>): Builder = apply {
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
Egorand marked this conversation as resolved.
Show resolved Hide resolved
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
check(!name.isConstructor) { "$name: constructors cannot have context receivers" }
Egorand marked this conversation as resolved.
Show resolved Hide resolved
contextReceiverTypes += receiverTypes
}

@ExperimentalKotlinPoetApi
public fun contextReceiver(vararg receiverType: TypeName): Builder = contextReceiver(receiverType.toList())
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved

@JvmOverloads public fun receiver(
receiverType: TypeName,
kdoc: CodeBlock = CodeBlock.EMPTY
Expand Down
22 changes: 19 additions & 3 deletions kotlinpoet/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt
Expand Up @@ -17,8 +17,11 @@ package com.squareup.kotlinpoet

import kotlin.reflect.KClass

@OptIn(ExperimentalKotlinPoetApi::class)
public class LambdaTypeName private constructor(
public val receiver: TypeName? = null,
@property:ExperimentalKotlinPoetApi
public val contextReceivers: List<TypeName> = emptyList(),
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
parameters: List<ParameterSpec> = emptyList(),
public val returnType: TypeName = UNIT,
nullable: Boolean = false,
Expand Down Expand Up @@ -50,10 +53,14 @@ public class LambdaTypeName private constructor(
suspending: Boolean = this.isSuspending,
tags: Map<KClass<*>, Any> = this.tags.toMap()
): LambdaTypeName {
return LambdaTypeName(receiver, parameters, returnType, nullable, suspending, annotations, tags)
return LambdaTypeName(receiver, contextReceivers, parameters, returnType, nullable, suspending, annotations, tags)
}

override fun emit(out: CodeWriter): CodeWriter {
if (contextReceivers.isNotEmpty()) {
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
val receivers = contextReceivers.joinToString(", ") { "%T" }
out.emitCode("context($receivers)·", *contextReceivers.toTypedArray())
}
if (isNullable) {
out.emit("(")
}
Expand All @@ -80,12 +87,20 @@ public class LambdaTypeName private constructor(
}

public companion object {
/** Returns a lambda type with `returnType` and parameters listed in `parameters`. */
@ExperimentalKotlinPoetApi @JvmStatic public fun get(
receiver: TypeName? = null,
parameters: List<ParameterSpec> = emptyList(),
returnType: TypeName,
contextReceivers: List<TypeName> = emptyList()
): LambdaTypeName = LambdaTypeName(receiver, contextReceivers, parameters, returnType)

/** Returns a lambda type with `returnType` and parameters listed in `parameters`. */
@JvmStatic public fun get(
receiver: TypeName? = null,
parameters: List<ParameterSpec> = emptyList(),
returnType: TypeName
): LambdaTypeName = LambdaTypeName(receiver, parameters, returnType)
): LambdaTypeName = LambdaTypeName(receiver, emptyList(), parameters, returnType)

/** Returns a lambda type with `returnType` and parameters listed in `parameters`. */
@JvmStatic public fun get(
Expand All @@ -95,6 +110,7 @@ public class LambdaTypeName private constructor(
): LambdaTypeName {
return LambdaTypeName(
receiver,
emptyList(),
parameters.toList().map { ParameterSpec.unnamed(it) },
returnType
)
Expand All @@ -105,6 +121,6 @@ public class LambdaTypeName private constructor(
receiver: TypeName? = null,
vararg parameters: ParameterSpec = emptyArray(),
returnType: TypeName
): LambdaTypeName = LambdaTypeName(receiver, parameters.toList(), returnType)
): LambdaTypeName = LambdaTypeName(receiver, emptyList(), parameters.toList(), returnType)
}
}
73 changes: 73 additions & 0 deletions kotlinpoet/src/test/java/com/squareup/kotlinpoet/FunSpecTest.kt
Expand Up @@ -33,6 +33,7 @@ import javax.lang.model.util.Types
import kotlin.test.BeforeTest
import kotlin.test.Test

@OptIn(ExperimentalKotlinPoetApi::class)
class FunSpecTest {
@Rule @JvmField val compilation = CompilationRule()

Expand Down Expand Up @@ -69,6 +70,8 @@ class FunSpecTest {

internal interface ExtendsOthers : Callable<Int>, Comparable<Long>

annotation class TestAnnotation

abstract class InvalidOverrideMethods {
fun finalMethod() {
}
Expand Down Expand Up @@ -429,6 +432,76 @@ class FunSpecTest {
)
}

@Test fun functionWithContextReceiver() {
val stringType = STRING
val funSpec = FunSpec.builder("foo")
.contextReceiver(stringType)
.build()

assertThat(funSpec.toString()).isEqualTo(
"""
|context(kotlin.String)
|public fun foo(): kotlin.Unit {
|}
|""".trimMargin()
)
}

@Test fun functionWithMultipleContextReceiver() {
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
val stringType = STRING
Egorand marked this conversation as resolved.
Show resolved Hide resolved
val intType = INT
val booleanType = BOOLEAN
val funSpec = FunSpec.builder("foo")
.contextReceiver(stringType, intType, booleanType)
.build()

assertThat(funSpec.toString()).isEqualTo(
"""
|context(kotlin.String, kotlin.Int, kotlin.Boolean)
|public fun foo(): kotlin.Unit {
|}
|""".trimMargin()
)
}

@Test fun functionWithGenericContextReceiver() {
val genericType = TypeVariableName("T")
val funSpec = FunSpec.builder("foo")
.addTypeVariable(genericType)
.contextReceiver(genericType)
.build()

assertThat(funSpec.toString()).isEqualTo(
"""
|context(T)
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
|public fun <T> foo(): kotlin.Unit {
|}
|""".trimMargin()
)
}

@Test fun functionWithAnnotatedContextReceiver() {
val genericType = STRING.copy(annotations = listOf(AnnotationSpec.get(TestAnnotation())))
val funSpec = FunSpec.builder("foo")
.contextReceiver(genericType)
.build()

assertThat(funSpec.toString()).isEqualTo(
"""
|context(@com.squareup.kotlinpoet.FunSpecTest.TestAnnotation kotlin.String)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm this is legal syntax?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using it in IntelliJ crashes intellij, however there is no real rules in the KEEP for how to use annotations and the compiler accepts it

|public fun foo(): kotlin.Unit {
|}
|""".trimMargin()
)
}

@Test fun constructorWithContextReceiver() {
assertThrows<IllegalStateException> {
FunSpec.constructorBuilder()
.contextReceiver(STRING)
}.hasMessageThat().isEqualTo("constructor(): constructors cannot have context receivers")
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
}

@Test fun functionParamSingleLambdaParam() {
val unitType = UNIT
val booleanType = BOOLEAN
Expand Down
Expand Up @@ -20,6 +20,7 @@ import com.squareup.kotlinpoet.KModifier.VARARG
import javax.annotation.Nullable
import kotlin.test.Test

@OptIn(ExperimentalKotlinPoetApi::class)
class LambdaTypeNameTest {

@Retention(AnnotationRetention.RUNTIME)
Expand Down Expand Up @@ -51,6 +52,58 @@ class LambdaTypeNameTest {
)
}

@Test fun contextReceiver() {
val typeName = LambdaTypeName.get(
Int::class.asTypeName(),
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
listOf(),
Unit::class.asTypeName(),
listOf(STRING)
)
assertThat(typeName.toString()).isEqualTo(
"context(kotlin.String) kotlin.Int.() -> kotlin.Unit"
)
}

@Test fun functionWithMultipleContextReceiver() {
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
val typeName = LambdaTypeName.get(
Int::class.asTypeName(),
listOf(),
Unit::class.asTypeName(),
listOf(STRING, BOOLEAN)
)
assertThat(typeName.toString()).isEqualTo(
"context(kotlin.String, kotlin.Boolean) kotlin.Int.() -> kotlin.Unit"
)
}

@Test fun functionWithGenericContextReceiver() {
val genericType = TypeVariableName("T")
val typeName = LambdaTypeName.get(
Int::class.asTypeName(),
listOf(),
Unit::class.asTypeName(),
listOf(genericType)
)

assertThat(typeName.toString()).isEqualTo(
"context(T) kotlin.Int.() -> kotlin.Unit"
)
}

@Test fun functionWithAnnotatedContextReceiver() {
val annotatedType = STRING.copy(annotations = listOf(AnnotationSpec.get(FunSpecTest.TestAnnotation())))
val typeName = LambdaTypeName.get(
Int::class.asTypeName(),
listOf(),
Unit::class.asTypeName(),
listOf(annotatedType)
)

assertThat(typeName.toString()).isEqualTo(
"context(@com.squareup.kotlinpoet.FunSpecTest.TestAnnotation kotlin.String) kotlin.Int.() -> kotlin.Unit"
)
}

@Test fun paramsWithAnnotationsForbidden() {
assertThrows<IllegalArgumentException> {
LambdaTypeName.get(
Expand Down