feat(ksp): make ksp-codegen KSP2-compatible and refine Q-class rendering#1728
Open
soidisant wants to merge 2 commits into
Open
feat(ksp): make ksp-codegen KSP2-compatible and refine Q-class rendering#1728soidisant wants to merge 2 commits into
soidisant wants to merge 2 commits into
Conversation
271d3d3 to
c0108f6
Compare
Bundles the foundational fixes for running querydsl-ksp-codegen under KSP2 (the default runtime for the KSP Gradle plugin 2.3.x) along with the rendering tweaks that bring the generated Kotlin Q-classes close to what querydsl-apt has long produced for Java. KSP2 lifecycle and reliability: - Move all symbol-API work into process(); KSP2 invalidates the analysis-API lifetime at the process()/finish() boundary, so the previous deferred-extraction split crashed with KaInvalidLifetimeOwnerAccessException. - Allocate a fresh QueryModelExtractor per round so KSClassDeclaration references aren't reused across analysis sessions. - Validate each symbol and defer the unprocessable ones to a later round (canonical KSP pattern). - Match annotations via KSAnnotation.shortName / qualifiedName instead of resolving to ClassName/TypeName via kotlinpoet-ksp; the latter path traverses the lifetime-bound KaFirTypeInformationProvider. - Avoid KSType.toClassName() on parameterized supertypes during the Comparable lookup in TypeExtractor.fallbackType(); kotlinpoet-ksp throws IllegalStateException for any KSType with type arguments (Comparable<X>, Collection<E>, etc.). See OpenFeign#1688. Java entity support: - Java @entity / @embeddable / @MappedSuperclass classes are now first-class. KSP surfaces their fields as KSPropertyDeclaration, so the existing extractor mostly works once we additionally exclude Java static and transient (keyword) modifiers. @transient is honoured via the shared shortName check. - Plain Collection<E> / Iterable<E> properties (a common Java JPA shape: `Collection<Bar> bars`) now produce CollectionPath<E,QE> instead of falling through to fallbackType and emitting an invalid `SimplePath<MutableCollection>` (no type arg). Q-class rendering, hybrid @JvmField/lazy: - _super, inherited properties, scalar paths, enums, collections render as eager @JvmField — Java consumers can field-access them (qFoo.active, qFoo.id) the same way they would Java-APT-generated Q-classes. - @manytoone object references stay `by lazy` with non-null type: handles self-referential entities (Foo.parent : Foo) without stack overflow at construction; Kotlin queries can write `qFoo.bar.id` with no `?.` / `!!`. Lazy avoids the construction-time depth limit Java APT mitigates with PathInits. - The synthesised JVM getter on lazy object refs is renamed via @get:JvmName so Java callers see `qFoo.bar()` instead of `qFoo.getBar()` — one `()` from the Java APT field shape. Test harness: - Adds dev.zacsweers.kctfork (0.12.1) for end-to-end KSP2 integration tests, with KSP runtime artifacts realigned to ksp.version=2.3.7 to avoid mismatched analysis-API behaviour. - New tests cover: the Kotlin baseline under KSP2, a Java entity (with @transient / static-final exclusion), a Java entity extending a Java @MappedSuperclass, a Kotlin entity referencing a Java @embeddable, an entity with parameterized Set/List supertypes (issue OpenFeign#1688 regression), a custom Comparable<X> wrapper, an entity with a Java Collection<E> field, and the hybrid @JvmField + lazy self-reference shape.
…ksp-codegen Adds a Java `Branch` entity (self-referencing via `@ManyToOne`) and a companion test alongside the existing Kotlin entities in `querydsl-example-ksp-codegen`. Demonstrates two things at once: that querydsl-ksp-codegen picks up Java sources during `kspKotlin` (the typical reason to reach for it over `querydsl-apt`), and that the lazy non-null rendering of object references handles self-references without stack overflow. Bumps the example to Gradle 9.5.0 and regenerates the wrapper. Documents the mixed-language workflow: - Adds a "Code Generation for Kotlin" section to `docs/tutorials/kotlin.md` (previously only covered the runtime DSL extensions), pointing to this example for both pure-Kotlin and mixed Java/Kotlin scenarios. Notes the KAPT-based path for Maven users where KSP is unavailable. - Adds a "Mixed Java/Kotlin sources" section to the KSP module README.
c0108f6 to
65d1b70
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
I have a mixed Java/Kotlin project. The entities are Java, but I want to write the Querydsl queries from Kotlin code. With
querydsl-aptthis doesn't work cleanly: Gradle compiles Kotlin before Java, so the Java Q-classes APT generates aren't on the classpath when the Kotlin sources reference them —QFoosimply doesn't resolve from Kotlin. The standard workaround has been to write the queries in Java too, defeating the point of having Kotlin.querydsl-ksp-codegenis the natural answer: KSP runs as part ofkspKotlin, sees Java sources alongside Kotlin sources, and emits.ktQ-classes that the Kotlin code can use immediately. In practice it didn't work — the processor crashed under KSP2 (the default runtime for the KSP Gradle plugin since the 2.x line), Java entities were silently dropped, and a few rendering choices didn't survive contact with real-world entities. This PR makes the processor actually deliver on that promise.Background
Running the existing
querydsl-ksp-codegenunder KSP2 crashes duringkspKotlinwith:Several KSP1-era assumptions in the processor break under KSP2's stricter analysis-API lifetime model. Once those are fixed, related rendering issues surface (Java entities silently dropped,
Collection<E>falling through toSimplePath<MutableCollection>,KSType.toClassName()crashing on parameterised supertypes — fixes #1688). This PR addresses them as one coherent piece.Supersedes #1690. That PR adds a one-line
arguments.isEmpty()guard infallbackType()to dodge the crash. It does fix #1688, but it also dropsComparablePathemission for parameterisedComparable<X>wrappers (custom typed-id classes etc.) which would silently downgrade toSimplePath. This PR usesdeclaration.qualifiedNamecomparison so theComparablemembership check works regardless of type arguments — see thecustomComparableWrapper_emitsComparablePathintegration test.Related to #1739. That PR adds APT-side compile-time detection for the classic bidirectional
<clinit>deadlock. The KSP rendering shape produced here is structurally immune to that deadlock — direct object references areby lazy, so QFoo's static init never triggers QBar's static init transitively. ThebidirectionalEntities_loadConcurrentlyWithoutDeadlocktest pins this dynamically: it compiles two entities with mutual@ManyToOnerefs and races two threads to touchQFoo.fooandQBar.barsimultaneously, asserting neither thread hangs. The edge case where the cycle is hidden behind an@MappedSuperclass(e.g.Auditable.createdBy : UserwithUser extends Auditable) is also handled by rendering inherited object references lazily; see theinheritedObjectReference_doesNotRecurseAtConstructiontest which reflectively loads the static field and asserts noStackOverflowError.Changes
KSP2 lifecycle and reliability
process(); KSP2 invalidates the analysis-API lifetime at theprocess()/finish()boundary.QueryModelExtractorper round (KSClassDeclarationreferences aren't reusable across rounds).KSAnnotation.shortName+qualifiedNameinstead of resolving toClassName/TypeNamevia kotlinpoet-ksp; the latter traverses the lifetime-boundKaFirTypeInformationProvider. The same change coversuserType(used to crash for@Convert/@Typefields under KSP2) and ensures cross-reference annotation matching is FQN-strict (a user's own@com.example.Entitywon't be mistaken for the JPA one).TypeExtractor.fallbackType(), look upComparablevia the supertype'sdeclaration.qualifiedNamerather thanKSType.toClassName()— kotlinpoet-ksp throwsIllegalStateExceptionon any parameterisedKSType(Comparable<X>,Collection<E>, …). Fixes KSP: TypeExtractor crashes on parameterized supertypes (ObjectId with MongoDB) #1688.Java entity support
@Entity/@Embeddable/@MappedSuperclassare now first-class. KSP surfaces their fields asKSPropertyDeclaration; the extractor additionally excludes Javastaticandtransient(keyword) modifiers.Collection<E>/Iterable<E>properties (a common Java JPA shape) now produceCollectionPath<E, QE>instead of falling through tofallbackTypeand emitting an invalidSimplePath<MutableCollection>.Q-class rendering — hybrid
@JvmField+ lazy_super, scalar paths, enums, collections, and inherited scalar properties render as eager@JvmFieldso Java consumers can field-access them (qFoo.active,qFoo.id) the same way they would Java-APT-generated Q-classes.@ManyToOne) and inherited (a@ManyToOneon a@MappedSuperclass) — render asby lazywith a non-null type. Lazy avoids two construction-time hazards: self-referential entities (e.g.Foo.parent : Foo) stack-overflowing on eager init, and inherited bidirectional references (Auditable.createdBy : User,User extends Auditable) infinitely recursing when the child's eager_super.footriggers the parent's per-instance lazy. The non-null type lets Kotlin queries writeqFoo.bar.idwith no?./!!. Each access creates one more level on demand, so depth is unbounded.@get:JvmNameso Java callers seeqFoo.bar()instead ofqFoo.getBar()— one()from the Java APT field shape.Test harness
Adds
dev.zacsweers.kctfork(0.12.1) for end-to-end KSP2 integration tests, with KSP runtime artifacts realigned to${ksp.version}=2.3.7. Tests cover the Kotlin baseline, Java entities (with@Transient/static-finalexclusion), Java-extending-Java@MappedSuperclass, Kotlin entity referencing a Java@Embeddable, parameterised supertypes (#1688 regression), a customComparable<X>wrapper, an entity with a JavaCollection<E>field, the hybrid@JvmField+ lazy self-reference shape, a@Convert-annotated field, cross-reference FQN strictness, an@MappedSuperclasswith a@ManyToOneback to its subclass (verified by reflective static-field load — asserts noStackOverflowError), and a bidirectional@ManyToOnepair loaded concurrently from two threads (verified the<clinit>chain doesn't deadlock).Example
Branchentity (self-referencing) alongside the Kotlin entities inquerydsl-example-ksp-codegen, with a Kotlin test querying it. Demonstrates the motivating use case end-to-end.Docs
docs/tutorials/kotlin.mdcovering the KSP path (Gradle) and the KAPT fallback (Maven).Trade-offs
by lazy. Java callers go throughqFoo.bar()(vs.qFoo.barfield access on Java-APT-generated Q-classes). In a project that writes queries from Kotlin, this is a non-issue; from Java, it's()instead of nothing.@Access(PROPERTY)(JPA annotations on getters) is not yet supported — annotations need to be on fields. Same default asquerydsl-apt.kotlin-maven-plugindoesn't natively run KSP. Use Gradle for KSP, or fall back to KAPT +querydsl-kotlin-codegenon Maven.qFoo.getActived()becomesqFoo.actived(now a@JvmField);qFoo.getBar()for@ManyToOnebecomesqFoo.bar()(renamed via@get:JvmName). Source-breaking on the Java side, transparent on the Kotlin side. Recompile required.Validation
querydsl-example-ksp-codegenbuilds and runs all tests (10 incl. the new Java-entity case) under Gradle 9.5.0 + JDK 21 + KSP 2.3.7.KaInvalidLifetimeOwnerAccessExceptionis fixed, self-referential entities work, generated Q-classes compile cleanly. A long-standing internal workaround (manually constructingSimplePathfrom path metadata strings to dodgePathInits.DIRECT2's depth-2 ceiling) was replaced with the naturalentityPath.parent().parent().parent().idchain.Commits
Two commits, structured for review: