Skip to content

Commit

Permalink
Finish IR V0 (#182)
Browse files Browse the repository at this point in the history
* Set default values appropriately for type

* Port most intermediate tpes

* Copy in NameAllocator + improve raw types

* Finish matching moshi API structure

Resolves #169
Resolves #168
Resolves #167
Resolves #166
Resolves #174
Resolves #178

* Clean up some TODOs and descriptor API usages

* Set non-constructor properties

Resolves #179

* Note for later

* Pass the standard compilation failure tests

Doesn't cover proguard or anonymous objects

* Support default values

Resolves #171

* Finish porting moshi tests and address any remaining issues

* More little cleanup

* Fix copyrights

* README, RELEASING, and snapshots

* Fix generated arg in tests
  • Loading branch information
ZacSweers authored Dec 22, 2021
1 parent 22ea153 commit 10a4e68
Show file tree
Hide file tree
Showing 42 changed files with 5,391 additions and 742 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ jobs:
- name: Build project
run: ./gradlew build check -Pmoshix.useKsp=${{ matrix.ksp_enabled }} -Pksp.incremental=${{ matrix.ksp_incremental_enabled }} --stacktrace
- name: Upload snapshot (main only)
run: |
./gradlew --stop && jps|grep -E 'KotlinCompileDaemon|GradleDaemon'| awk '{print $1}'| xargs kill -9 || true
./gradlew publish -PmavenCentralUsername=${{ secrets.SONATYPE_USERNAME }} -PmavenCentralPassword=${{ secrets.SONATYPE_PASSWORD }}
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SonatypeUsername }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SonatypePassword }}
run: ./publish.sh --snapshot
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && matrix.java_version == '17' && !matrix.ksp_enabled
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Extensions for [Moshi](https://github.com/square/moshi)

* [moshi-ir](https://github.com/ZacSweers/MoshiX/tree/main/moshi-ir) - A Kotlin IR implementation of Moshi code gen.
* [moshi-adapters](https://github.com/ZacSweers/MoshiX/tree/main/moshi-adapters) - A collection of custom adapters for Moshi.
* [moshi-metadata-reflect](https://github.com/ZacSweers/MoshiX/tree/main/moshi-metadata-reflect) - A [kotlinx-metadata](https://github.com/JetBrains/kotlin/tree/master/libraries/kotlinx-metadata/jvm) based implementation of `KotlinJsonAdapterFactory`. This allows for reflective Moshi serialization on Kotlin classes without the cost of including kotlin-reflect.
* [moshi-sealed](https://github.com/ZacSweers/MoshiX/tree/main/moshi-sealed) - Reflective and code gen implementations for serializing Kotlin sealed classes via Moshi polymorphic adapters.
Expand Down
3 changes: 2 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ Releasing
=========

1. Change the version in `gradle.properties` to a non-SNAPSHOT version.
* Note - do this in both the top-level file and `moshi-ir/moshi-gradle-plugin`
2. Update the `CHANGELOG.md` for the impending release.
3. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
4. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version)
5. `./gradlew clean publish --no-daemon --no-parallel && ./gradlew closeAndReleaseRepository`
5. `./publish.sh`
* Make sure to run this with JDK 17
6. Update the `gradle.properties` to the next SNAPSHOT version.
7. `git commit -am "Prepare next development version."`
Expand Down
12 changes: 2 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ apiValidation {
ignoredProjects +=
listOf(
/* :moshi-ir: */
"playground",
"moshi-kotlin-tests",
"extra-moshi-test-module",
/* :moshi-sealed: */
"sample",
)
Expand All @@ -62,32 +63,23 @@ spotless {
googleJavaFormat(libs.versions.gjf.get())
target("**/*.java")
targetExclude("**/spotless.java", "**/build/**")
licenseHeaderFile("spotless/spotless.java")
}
kotlin {
ktfmt("0.30")
target("**/*.kt")
trimTrailingWhitespace()
endWithNewline()
licenseHeaderFile("spotless/spotless.kt").updateYearWithLatest(false)
targetExclude(
"**/Dependencies.kt",
"**/spotless.kt",
"**/build/**",
)
}
// format("externalKotlin", KotlinExtension::class.java) {
// // These don't use our spotless config for header files since we don't want to overwrite the
// // existing copyright headers.
// configureCommonKotlinFormat()
// }
kotlinGradle {
ktfmt("0.30")
target("**/*.gradle.kts")
trimTrailingWhitespace()
endWithNewline()
licenseHeaderFile(
"spotless/spotless.kts", "(import|plugins|buildscript|dependencies|pluginManagement)")
}
}

Expand Down
65 changes: 65 additions & 0 deletions moshi-ir/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
Moshi-IR
========

A Kotlin IR implementation of Moshi code gen.

The goal of this is to have functional parity with Moshi's native Kapt/KSP code gen but run as a fully embedded IR
plugin.

**Benefits**
- Significantly faster build times
- No reflection required at runtime to support default parameter values
- Feature parity with Moshi's native code gen

**Cons**
- No support for Proguard file generation for now. You will need to add this manually to your rules if you use
R8/Proguard.
- One option is to use IR in debug builds and Kapt/KSP in release builds, the latter of which do still generate
proguard rules.
```proguard
# Keep names for JsonClass-annotated classes
-keepnames class @com.squareup.moshi.JsonClass **
# Keep generated adapter classes' constructors
-keepclassmembers class *JsonAdapter {
public <init>(...);
}
```
- Kotlin IR is not a stable API and may change in future Kotlin versions. While I'll try to publish quickly to adjust to
these, you should be aware. If you have any issues, you can always fall back to Kapt/KSP.

### Installation

Simply apply the Gradle plugin in your project to use it.

The Gradle plugin is published to Maven Central, so ensure you have `mavenCentral()` visible to your buildscript
classpath.

[![Maven Central](https://img.shields.io/maven-central/v/dev.zacsweers.moshix/moshi-gradle-plugin.svg)](https://mvnrepository.com/artifact/dev.zacsweers.moshix/moshi-sealed-runtime)
```gradle
plugins {
kotlin("jvm")
id("dev.zacsweers.moshix") version "x.y.z"
}
```

Snapshots of the development version are available in [Sonatype's snapshots repository][snapshots].

License
-------

Copyright (C) 2021 Zac Sweers

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

http://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.

[snapshots]: https://oss.sonatype.org/content/repositories/snapshots/dev/zacsweers/moshix/
2 changes: 1 addition & 1 deletion moshi-ir/moshi-compiler-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
// compileOnly(kotlin("compiler"))
compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.10")
implementation("com.google.auto.service:auto-service-annotations:1.0.1")
implementation("com.squareup.moshi:moshi:1.13.0")
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.0.0")

testImplementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10")
Expand All @@ -42,5 +43,4 @@ dependencies {
testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.4.7")
testImplementation("junit:junit:4.13.2")
testImplementation("com.google.truth:truth:1.1.3")
testImplementation("com.squareup.moshi:moshi:1.13.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2021 Zac Sweers
*
* 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 dev.zacsweers.moshix.ir.compiler

import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable
import org.jetbrains.kotlin.ir.util.getAllSuperclasses

/**
* A concrete type like `List<String>` with enough information to know how to resolve its type
* variables.
*/
internal class AppliedType
private constructor(
val type: IrClass,
) {

/**
* Returns all super classes of this, recursively. Only [IrClass] is used as we can't really use
* other types.
*/
fun superclasses(
pluginContext: IrPluginContext,
): LinkedHashSet<AppliedType> {
val result: LinkedHashSet<AppliedType> = LinkedHashSet()
result.add(this)
for (supertype in type.getAllSuperclasses()) {
if (supertype.kind != ClassKind.CLASS) continue
// TODO do we need to check if it's j.l.Object?
val irType = supertype.defaultType
if (irType == pluginContext.irBuiltIns.anyType) {
// Don't load properties for kotlin.Any/java.lang.Object.
continue
}
result.add(AppliedType(supertype))
}
return result
}

override fun toString() = type.fqNameWhenAvailable!!.asString()

companion object {
operator fun invoke(type: IrClass): AppliedType {
return AppliedType(type)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package dev.zacsweers.moshix.ir.compiler

import dev.zacsweers.moshix.ir.compiler.util.error
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
Expand All @@ -27,13 +28,19 @@ internal class MoshiIrGenerationExtension(
) : IrGenerationExtension {

override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
val generatedAnnotation = generatedAnnotationName?.let(pluginContext::referenceClass)
if (generatedAnnotation == null) {
// TODO eventually error
println("Unknown generated annotation $generatedAnnotationName")
}
val generatedAnnotation =
generatedAnnotationName?.let { fqName ->
pluginContext.referenceClass(fqName).also {
if (it == null) {
messageCollector.error { "Unknown generated annotation $generatedAnnotationName" }
return
}
}
}
val deferred = mutableListOf<GeneratedAdapter>()
val moshiTransformer = MoshiIrVisitor(moduleFragment, pluginContext, messageCollector, deferred)
val moshiTransformer =
MoshiIrVisitor(
moduleFragment, pluginContext, messageCollector, generatedAnnotation, deferred)
moduleFragment.transform(moshiTransformer, null)
for ((file, adapters) in deferred.groupBy { it.irFile }) {
for (adapter in adapters) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (C) 2021 Zac Sweers
*
* 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 dev.zacsweers.moshix.ir.compiler

import com.squareup.moshi.Json
import dev.zacsweers.moshix.ir.compiler.api.DelegateKey
import dev.zacsweers.moshix.ir.compiler.api.PropertyGenerator
import dev.zacsweers.moshix.ir.compiler.api.TargetProperty
import dev.zacsweers.moshix.ir.compiler.util.error
import dev.zacsweers.moshix.ir.compiler.util.locationOf
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrGetEnumValue
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.util.file
import org.jetbrains.kotlin.ir.util.getAnnotation
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.name.FqName

internal val JSON_ANNOTATION = FqName("com.squareup.moshi.Json")
internal val JSON_QUALIFIER_ANNOTATION = FqName("com.squareup.moshi.JsonQualifier")

internal fun IrAnnotationContainer?.jsonQualifiers(): Set<IrConstructorCall> {
if (this == null) return emptySet()
return annotations.filterTo(LinkedHashSet()) {
it.type.classOrNull?.owner?.hasAnnotation(JSON_QUALIFIER_ANNOTATION) == true
}
}

internal fun IrProperty.jsonNameFromAnywhere(): String? {
return jsonName() ?: backingField?.jsonName() ?: getter?.jsonName() ?: setter?.jsonName()
}

internal fun IrProperty.jsonIgnoreFromAnywhere(): Boolean {
return jsonIgnore() ||
backingField?.jsonIgnore() == true ||
getter?.jsonIgnore() == true ||
setter?.jsonIgnore() == true
}

internal fun IrAnnotationContainer.jsonName(): String? {
@Suppress("UNCHECKED_CAST")
return (getAnnotation(JSON_ANNOTATION)?.getValueArgument(0) as? IrConst<String>?)?.value
?.takeUnless { it == Json.UNSET_NAME }
}

internal fun IrAnnotationContainer.jsonIgnore(): Boolean {
@Suppress("UNCHECKED_CAST")
return (getAnnotation(JSON_ANNOTATION)?.getValueArgument(1) as? IrConst<Boolean>?)?.value == true
}

private val TargetProperty.isSettable
get() = property.isVar || parameter != null
private val TargetProperty.isVisible: Boolean
get() {
return visibility == DescriptorVisibilities.INTERNAL ||
visibility == DescriptorVisibilities.PROTECTED ||
visibility == DescriptorVisibilities.PUBLIC
}

/**
* Returns a generator for this property, or null if either there is an error and this property
* cannot be used with code gen, or if no codegen is necessary for this property.
*/
internal fun TargetProperty.generator(
logger: MessageCollector,
originalType: IrClass,
): PropertyGenerator? {
val location = { originalType.file.locationOf(originalType) }
if (jsonIgnore) {
if (!hasDefault) {
logger.error(location) { "No default value for transient/ignored property $name" }
return null
}
return PropertyGenerator(this, DelegateKey(type, emptyList()), true)
}

if (!isVisible) {
logger.error(location) { "property $name is not visible" }
return null
}

if (!isSettable) {
return null // This property is not settable. Ignore it.
}

// Merge parameter and property annotations
val qualifiers = parameter?.qualifiers.orEmpty() + property.jsonQualifiers()
for (jsonQualifier in qualifiers) {
val qualifierRawType = jsonQualifier.type.classOrNull!!.owner
val retentionValue =
qualifierRawType
.getAnnotation(FqName("kotlin.annotation.Retention"))
?.getValueArgument(0) as
IrGetEnumValue?
?: continue
// TODO what about java qualifiers types?
val retention = retentionValue.symbol.owner.name.identifier
// Check Java types since that covers both Java and Kotlin annotations.
if (retention != "RUNTIME") {
logger.error({ originalType.file.locationOf(jsonQualifier) }) {
"JsonQualifier @${qualifierRawType.name} must have RUNTIME retention"
}
}
}

return PropertyGenerator(this, DelegateKey(type, qualifiers.toList()))
}
Loading

0 comments on commit 10a4e68

Please sign in to comment.