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

Upstream KSP implementation #1393

Merged
merged 29 commits into from Oct 16, 2021
Merged

Upstream KSP implementation #1393

merged 29 commits into from Oct 16, 2021

Conversation

ZacSweers
Copy link
Collaborator

@ZacSweers ZacSweers commented Sep 10, 2021

This upstreams the moshi-ksp implementation from https://github.com/zacsweers/moshix.

Highlights

  • KSP lives in a codegen.ksp subpackage alongside the existing apt version
  • They reuse the same integration tests in :moshi:kotlin:tests. The KSP implementation is functionally compatible!
  • Bringing along a few KSP-specific regression tests
  • CI runs a matrix of both kapt and KSP.
    • This also made me realize we could possibly use a CI matrix to truly achieve what Fix dualkotlintest #1064 was trying to do (but that's for another PR)
  • Switches to auto-service-ksp for for AutoService support and removes kapt itself from the codegen project, as I was really tired of dealing with Kapt's strange shenanigans on JDK 16.

Some other things to do:

@ZacSweers ZacSweers marked this pull request as ready for review September 10, 2021 06:21
@ZacSweers
Copy link
Collaborator Author

Some of these KSP - KotlinPoet bits definitely have a future in KotlinPoet itself, but that's for another day

@ZacSweers ZacSweers mentioned this pull request Sep 20, 2021
addAnnotation(Transient::class)
}
addAnnotations(
this@toPropertySpec.annotations
Copy link

@evant evant Oct 15, 2021

Choose a reason for hiding this comment

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

Don't know if there's an intention to support this, but I noticed a difference from kapt if you qualify the annotation with get/set, ex:

class ClassWithQualiifer(@get:MyQualifier val a: Int)

will see the qualifier in kapt, but ignore it in ksp.

If you do want to look at these, you need to check getter.annotations and/or setter.annotations as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup we don't really have a clear story there as far as what we check, but officially our stance in other issues has been that only properties are supported and anything else is undefined

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'd like to straighten it out more but will save that for a later PR

Copy link

@evant evant left a comment

Choose a reason for hiding this comment

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

New annotation methods look much cleaner!

@ZacSweers ZacSweers merged commit 2db351f into square:master Oct 16, 2021
@ZacSweers ZacSweers deleted the z/upstreamKsp branch October 16, 2021 05:52
@ZacSweers
Copy link
Collaborator Author

Thanks for the review!

* * `"javax.annotation.processing.Generated"` (JRE 9+)
* * `"javax.annotation.Generated"` (JRE <9)
*/
const val OPTION_GENERATED: String = "moshi.generated"
Copy link
Member

Choose a reason for hiding this comment

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

👍🏻

deepCopy(TypeName::unwrapTypeAlias)
}
}
else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.")
Copy link
Member

Choose a reason for hiding this comment

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

I don’t understand who this message is for.

If you’re blaming the caller it should be an IllegalStateException saying that this function doesn’t support its input.

If you’re blaming this function itself for being incomplete then it should be an AssertionError saying that the case is unexpected.

As is if I get this crash I don’t know where blame lies. As a reviewer I don’t even know!

Copy link
Member

Choose a reason for hiding this comment

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

(this feedback is on code that predates this PR)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So this is only possible for Dynamic types, which we don't support here. I suppose we could support it in the future, but for now while we're JVM-only it's not even possible to reach this case. I can rename else to Dynamic to make it more explicit, but I don't know necessarily what the right way to rule that out is in future versions. wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Made dynamic explicit here b62ec84

val adapterGenerator = adapterGenerator(logger, resolver, type) ?: return emptyList()
try {
val preparedAdapter = adapterGenerator
.prepare(generateProguardRules) { spec ->
Copy link
Member

Choose a reason for hiding this comment

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

Would this be simpler as two functions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the whole process() function or splitting prepare and writing?

}
}

internal fun KSAnnotation.toAnnotationSpec(resolver: Resolver): AnnotationSpec {
Copy link
Member

Choose a reason for hiding this comment

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

Yeah KotlinPoet wants to eat this

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

PR for a later day!

else -> CodeBlock.of("%L", value)
}

internal inline fun KSPLogger.check(condition: Boolean, message: () -> String) {
Copy link
Member

Choose a reason for hiding this comment

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

cute

Copy link
Member

@swankjesse swankjesse left a comment

Choose a reason for hiding this comment

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

LGTM

Lots of feedback; I might send PRs to address some. (Also please feel free to send your own follow ups!)

parameter = parameter,
visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC,
jsonName = parameter?.jsonName ?: property.jsonName()
?: name.escapeDollarSigns()
Copy link
Member

Choose a reason for hiding this comment

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

This escapeDollarSigns call reads very suspicious to me . .. why doesn’t it escape when it comes from a parameter or property JSON name

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is leftover from kapt (which didn't escape them) and I believe is still necessary here too. I can test, basically it shows fine in the annotation string but doesn't play nice in generated code because of Kotlin templating


internal fun <T : Annotation> KSAnnotated.getAnnotationsByType(annotationKClass: KClass<T>): Sequence<T> {
return this.annotations.filter {
it.shortName.getShortName() == annotationKClass.simpleName && it.annotationType.resolve().declaration
Copy link
Member

Choose a reason for hiding this comment

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

any idea what the utility of the short name check is?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Simple early check before doing a more expensive resolve() call in the second check

getAnnotationsByType(annotationKClass).firstOrNull() != null

@Suppress("UNCHECKED_CAST")
private fun <T : Annotation> KSAnnotation.toAnnotation(annotationClass: Class<T>): T {
Copy link
Member

Choose a reason for hiding this comment

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

Whoa, I dislike this. What the heck are we doing instantiating an annotation instance from the processed code inside the processor?

Copy link
Member

Choose a reason for hiding this comment

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

Let’s clean this up to definitely not use a dynamic proxy to create an annotation instance; this is a ton of mechanism to just read the properties of @Json and @JsonClass and @Retention

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is what apt is doing too though, that's why KSP added it as a familiar API. Parsing members from the annotations themselves were actually quite messy. We can go back to that, but this is supposed to be the long term idiomatic mechanism

image

Copy link
Member

Choose a reason for hiding this comment

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

Why is it in our code and not KSP?

import kotlin.reflect.full.createType

/** Execute kotlinc to confirm that either files are generated or errors are printed. */
class JsonClassSymbolProcessorTest {
Copy link
Member

Choose a reason for hiding this comment

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

great test


// We're checking here that we only generate one `stringAdapter` that's used for both the
// regular string properties as well as the the aliased ones.
// TODO loading compiled classes from results not supported in KSP yet
Copy link
Member

Choose a reason for hiding this comment

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

Replace this assertion with confirming how many files were generated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

number of files wouldn't verify anything. This is about the number of adapter properties in a file

}

@Test
fun `Processor should generate comprehensive proguard rules`() {
Copy link
Member

Choose a reason for hiding this comment

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

This is not a good test, but it serves as nice raw material from which good tests could be created. Rather than covering an assortment of features in one scattershot test, we aught to test each of the interesting features each in their own tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let's do a dedicated PR for that, the existing apt tests are the same and it'd be pretty involved

Copy link
Member

Choose a reason for hiding this comment

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

That'd be great! I'm more ranting than anything.

alias(libs.plugins.ksp) apply false
}

val useKsp = hasProperty("useKsp")
Copy link
Member

Choose a reason for hiding this comment

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

COOL

@ZacSweers
Copy link
Collaborator Author

I thiiiiiink I've covered most of them in #1448, and responded to a few others

swankjesse added a commit that referenced this pull request Dec 6, 2021
* Rename to unwrapTypeAliasInternal + simplify

* Move down isAnyNullable

* Make dynamic explicit

* Clean up supertypes doc and filtering

* Switch to invoke extensions

* Just best guess the annotation

* Clean up redundant sequence and use a regular loop

* element -> type

* supertypes -> superclasses

* Spotless

* Fix copyright

* Add multiple messages check

* Link issue

Co-authored-by: Jesse Wilson <jesse@swank.ca>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants