From 4d4a18ff632cafb18c187fae9f794ce015bdef75 Mon Sep 17 00:00:00 2001 From: elihart Date: Fri, 9 Jan 2026 18:54:58 -0800 Subject: [PATCH 1/5] Fix error from "PSI has changed since creation" --- .../java/com/airbnb/epoxy/processor/BaseProcessor.kt | 4 ++++ .../processor/resourcescanning/KspResourceScanner.kt | 6 ++++++ .../epoxy/processor/resourcescanning/ResourceScanner.kt | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt index d773d49fd8..1c7c94dbfd 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt @@ -217,6 +217,10 @@ abstract class BaseProcessor(val kspEnvironment: SymbolProcessorEnvironment? = n // for reuse. val memoizer = Memoizer(environment, logger) + // Clear resource scanner caches to avoid holding stale KSP element references. + // In KSP2, accessing elements from previous rounds triggers "PSI has changed since creation" errors. + resourceProcessor.clearCachesForNewRound() + val deferredElements: List = try { tryOrPrintError?> { timer.markStepCompleted("round initialization") diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/KspResourceScanner.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/KspResourceScanner.kt index b5c34f022c..21b9dbfe8c 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/KspResourceScanner.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/KspResourceScanner.kt @@ -37,6 +37,12 @@ class KspResourceScanner(environmentProvider: () -> XProcessingEnv) : private val cache = mutableMapOf, XElement>, List>() + override fun clearCachesForNewRound() { + // Clear the cache to avoid holding references to stale XElement objects from previous rounds. + // In KSP2, accessing elements from previous rounds triggers "PSI has changed since creation" errors. + cache.clear() + } + override fun getResourceValueListInternal( annotation: KClass, element: XElement, diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/ResourceScanner.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/ResourceScanner.kt index 792304b4ee..7b7623721c 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/ResourceScanner.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/resourcescanning/ResourceScanner.kt @@ -122,4 +122,13 @@ abstract class ResourceScanner(val environmentProvider: () -> XProcessingEnv) { } abstract fun getImports(classElement: XTypeElement): List + + /** + * Clears any caches that may hold references to KSP elements. + * This must be called at the start of each round to avoid accessing stale PSI elements + * in KSP2, which would trigger "PSI has changed since creation" errors. + */ + open fun clearCachesForNewRound() { + // Default implementation does nothing. KspResourceScanner overrides this to clear its cache. + } } From e2be2656f471593c00268aef4b6e05ad25b5f6e3 Mon Sep 17 00:00:00 2001 From: elihart Date: Fri, 9 Jan 2026 19:06:04 -0800 Subject: [PATCH 2/5] fix signing config --- gradle.properties | 3 ++- publishing.gradle | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index f52b4854de..5a34adb1ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,7 +23,8 @@ org.gradle.configuration-cache=true # Publishing configuration for vanniktech/gradle-maven-publish-plugin mavenCentralPublishing=true -RELEASE_SIGNING_ENABLED=true +# Signing can be disabled with -PdoNotSignRelease=true +signAllPublications=true mavenCentralAutomaticPublishing=true # Dokka fails without a larger metaspace https://github.com/Kotlin/dokka/issues/1405 diff --git a/publishing.gradle b/publishing.gradle index 3c859c39b6..d3f21a3bc6 100644 --- a/publishing.gradle +++ b/publishing.gradle @@ -19,11 +19,13 @@ publishing { } } -mavenPublishing { - if (findProperty("doNotSignRelease").toString().toBoolean()) { - println("Skipping release signing") - } else { - println("Signing release with gpg") - signAllPublications() - } +// Apply conditional signing logic by setting the property at configuration time +// This must happen before the mavenPublishing plugin reads the property +if (findProperty("doNotSignRelease").toString().toBoolean()) { + println("Skipping release signing") + // Override the signAllPublications property to disable signing + ext.set("signAllPublications", "false") +} else { + println("Signing release with gpg") + // signAllPublications is already set to true in gradle.properties } \ No newline at end of file From 4d0eff6120601625a3daa39cf4ce90930de7830f Mon Sep 17 00:00:00 2001 From: elihart Date: Mon, 12 Jan 2026 17:34:36 -0800 Subject: [PATCH 3/5] Fixes so far --- .../processor/BasicGeneratedModelInfo.kt | 2 +- .../airbnb/epoxy/processor/ConfigManager.kt | 10 ++++-- .../processor/DataBindingAttributeInfo.kt | 4 ++- .../airbnb/epoxy/processor/EpoxyProcessor.kt | 2 +- .../epoxy/processor/GeneratedModelInfo.kt | 17 ++++++++-- .../epoxy/processor/GeneratedModelWriter.kt | 31 +++++++++++-------- .../com/airbnb/epoxy/processor/Memoizer.kt | 28 +++++++++++------ .../processor/ModelBuilderInterfaceWriter.kt | 5 +-- .../airbnb/epoxy/processor/ModelViewInfo.kt | 6 ++-- .../epoxy/processor/ModelViewProcessor.kt | 4 +-- .../processor/PackageModelViewSettings.kt | 18 +++++------ .../processor/ParisStyleAttributeInfo.kt | 5 ++- .../java/com/airbnb/epoxy/processor/Type.kt | 10 +++--- gradle.properties | 2 +- 14 files changed, 90 insertions(+), 54 deletions(-) diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt index c1047b425d..df94d71a73 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt @@ -89,5 +89,5 @@ internal class BasicGeneratedModelInfo( ) } - override fun additionalOriginatingElements(): List = listOf(superClassElement) + override fun additionalOriginatingElements(): List = listOf(safeSuperClassElement()) } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ConfigManager.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ConfigManager.kt index 0536637c5a..c3fb896c9d 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ConfigManager.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ConfigManager.kt @@ -224,8 +224,14 @@ class ConfigManager internal constructor( ) } - fun getDefaultBaseModel(viewElement: XTypeElement): XType? { - return getModelViewConfig(viewElement)?.defaultBaseModel + fun getDefaultBaseModel(viewElement: XTypeElement, currentEnvironment: XProcessingEnv): XType? { + val className = getModelViewConfig(viewElement)?.defaultBaseModelClassName + ?: return null + // Resolve the type fresh each round using the current environment to avoid + // accessing stale KSP elements from previous rounds. + // IMPORTANT: Use the currentEnvironment parameter, not the instance field, + // since the instance field was captured at ConfigManager creation time (round 1). + return currentEnvironment.findType(className) } fun includeAlternateLayoutsForViews(viewElement: XTypeElement): Boolean { diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingAttributeInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingAttributeInfo.kt index 1f96bec4ce..07cbf97260 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingAttributeInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingAttributeInfo.kt @@ -12,7 +12,9 @@ internal class DataBindingAttributeInfo( init { fieldName = removeSetPrefix(setterMethod.name) - setXType(setterMethod.parameters[0].type, modelInfo.memoizer) + // Use the memoizer parameter from the current round, not modelInfo.memoizer. + // modelInfo may have been created in a previous round and its memoizer holds stale KSP references. + setXType(setterMethod.parameters[0].type, memoizer) rootClass = modelInfo.generatedName.simpleName() packageName = modelInfo.generatedName.packageName() useInHash = !modelInfo.enableDoNotHash || diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt index 2334bfa5d7..fed613b489 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt @@ -106,7 +106,7 @@ class EpoxyProcessor @JvmOverloads constructor( val styleableModels = modelInfos .filterIsInstance() .filter { modelInfo -> - modelInfo.superClassElement.getAnnotation(EpoxyModelClass::class)?.getAsInt("layout") == 0 && + modelInfo.safeSuperClassElement().getAnnotation(EpoxyModelClass::class)?.getAsInt("layout") == 0 && modelInfo.boundObjectTypeElement?.hasStyleableAnnotation() == true } timer.markStepCompleted("check for styleable models") diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt index 253268d9a4..26d80b5408 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt @@ -17,6 +17,15 @@ import javax.lang.model.element.Modifier abstract class GeneratedModelInfo(val memoizer: Memoizer) { lateinit var superClassElement: XTypeElement + + /** + * Looks up and returns the super class element by FQN, to avoid errors from accessing + * an XTypeElement from a previous processing round. + */ + fun safeSuperClassElement(): XTypeElement { + return memoizer.environment.requireTypeElement(superClassElement.qualifiedName) + } + lateinit var superClassName: TypeName lateinit var parameterizedGeneratedName: TypeName lateinit var generatedName: ClassName @@ -86,7 +95,7 @@ abstract class GeneratedModelInfo(val memoizer: Memoizer) { */ fun collectMethodsReturningClassType(superModelClass: XTypeElement) { methodsReturningClassType - .addAll(memoizer.getMethodsReturningClassType(superModelClass.type, memoizer)) + .addAll(memoizer.getMethodsReturningClassType(superModelClass.qualifiedName, memoizer)) } @Synchronized @@ -152,8 +161,10 @@ abstract class GeneratedModelInfo(val memoizer: Memoizer) { * @return True if the super class of this generated model is also extended from a generated * model. */ - val isSuperClassAlsoGenerated: Boolean - get() = superClassElement.type.isSubTypeOf(memoizer.generatedModelType) + fun isSuperClassAlsoGenerated(currentMemoizer: Memoizer): Boolean { + val generatedModelType = currentMemoizer.generatedModelType + return safeSuperClassElement().type.isSubTypeOf(generatedModelType) + } data class ConstructorInfo internal constructor( val modifiers: Set, diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt index 640227f54e..a022ca3ffc 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt @@ -3,7 +3,6 @@ package com.airbnb.epoxy.processor import androidx.annotation.LayoutRes import androidx.room.compiler.processing.XElement import androidx.room.compiler.processing.XFiler -import androidx.room.compiler.processing.XMethodElement import androidx.room.compiler.processing.XProcessingEnv import androidx.room.compiler.processing.XTypeElement import androidx.room.compiler.processing.addOriginatingElement @@ -608,7 +607,7 @@ class GeneratedModelWriter( addParameter(boundObjectParam) addParameter(TypeName.INT, "position") - if (modelInfo.isSuperClassAlsoGenerated) { + if (modelInfo.isSuperClassAlsoGenerated(memoizer)) { // If a super class is also generated we need to make sure to call through to these // methods on it as well. This is particularly important for EpoxyModelGroup. addStatement("super.handlePostBind(\$L, position)", boundObjectParam.name) @@ -836,8 +835,8 @@ class GeneratedModelWriter( // bind!!! So we mustn't do that. So, we only call the super diff binding if we think // it's a custom implementation. if (modelImplementsBindWithDiff( - classInfo.superClassElement, - memoizer.baseBindWithDiffMethod + classInfo.safeSuperClassElement(), + memoizer ) ) { addStatement( @@ -870,7 +869,7 @@ class GeneratedModelWriter( .addParameter(boundObjectParam) .addParameter(TypeName.INT, positionParamName, Modifier.FINAL) - if (modelInfo.isSuperClassAlsoGenerated) { + if (modelInfo.isSuperClassAlsoGenerated(memoizer)) { // If a super class is also generated we need to make sure to call through to these // methods on it as well. This is particularly important for EpoxyModelGroup. preBindBuilder.addStatement( @@ -1103,7 +1102,7 @@ class GeneratedModelWriter( methods: MutableList ) { - val originalClassElement = modelClassInfo.superClassElement + val originalClassElement = modelClassInfo.safeSuperClassElement() if (!originalClassElement.type.isEpoxyModelWithHolder(memoizer)) { return } @@ -1179,7 +1178,7 @@ class GeneratedModelWriter( return modelInfo.getLayoutResource(resourceProcessor) } - val superClassElement = modelInfo.superClassElement + val superClassElement = modelInfo.safeSuperClassElement() if (implementsMethod(superClassElement, buildDefaultLayoutMethodBase(), environment)) { return null } @@ -1206,7 +1205,7 @@ class GeneratedModelWriter( * variables that changed. */ private fun generateDataBindingMethodsIfNeeded(info: GeneratedModelInfo): Iterable { - if (!info.superClassElement.type.isDataBindingEpoxyModel(memoizer)) { + if (!info.safeSuperClassElement().type.isDataBindingEpoxyModel(memoizer)) { return emptyList() } @@ -1222,7 +1221,7 @@ class GeneratedModelWriter( // If the base method is already implemented don't bother checking for the payload method if (implementsMethod( - info.superClassElement, + info.safeSuperClassElement(), bindVariablesMethod, environment ) @@ -1233,7 +1232,7 @@ class GeneratedModelWriter( val generatedModelClass = info.generatedName val moduleName = (info as? DataBindingModelInfo)?.moduleName - ?: dataBindingModuleLookup.getModuleName(info.superClassElement) + ?: dataBindingModuleLookup.getModuleName(info.safeSuperClassElement()) val baseMethodBuilder = bindVariablesMethod.toBuilder() @@ -2102,14 +2101,20 @@ class GeneratedModelWriter( fun modelImplementsBindWithDiff( clazz: XTypeElement, - baseBindWithDiffMethod: XMethodElement + currentMemoizer: Memoizer ): Boolean { - return clazz.getAllMethods().any { + // Re-resolve the type element using the current memoizer's environment to avoid + // accessing stale KSP elements from previous rounds. In KSP2, this triggers + // "PSI has changed since creation" errors. + val freshClazz = currentMemoizer.environment.requireType(clazz.qualifiedName).typeElement!! + val baseBindWithDiffMethod = currentMemoizer.baseBindWithDiffMethod + + return freshClazz.getAllMethods().any { it.name == baseBindWithDiffMethod.name && !it.isAbstract() && it.overrides( other = baseBindWithDiffMethod, - owner = clazz + owner = freshClazz ) } } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt index 2fbfe41795..4bdbb054c5 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt @@ -124,9 +124,16 @@ class Memoizer( private val methodsReturningClassType = mutableMapOf>() - fun getMethodsReturningClassType(classType: XType, memoizer: Memoizer): Set { + fun getMethodsReturningClassType(classQualifiedName: String, memoizer: Memoizer): Set { + // Re-resolve the type fresh using the current memoizer's environment. + // The caller may have been created in a previous round, and accessing properties + // of stale KSP types in KSP2 triggers "PSI has changed since creation" errors. + // By passing the qualified name as a String and resolving fresh each time, + // we ensure we always use current-round types. + val classType = memoizer.environment.requireType(classQualifiedName) val classElement = classType.typeElement!! - return methodsReturningClassType.getOrPut(classElement.qualifiedName) { + + return methodsReturningClassType.getOrPut(classQualifiedName) { val methodInfos: List = classElement.getDeclaredMethods().mapNotNull { subElement -> @@ -164,8 +171,9 @@ class Memoizer( // Note: Adding super type methods second preserves any overloads in the base // type that may have changes (ie, a new return type or annotation), since // Set.plus only adds items that don't already exist. - val superClassType = classElement.superClass ?: return@getOrPut emptySet() - methodInfos.toSet() + getMethodsReturningClassType(superClassType, memoizer) + val superClassElement = classElement.superClass?.typeElement + ?: return@getOrPut emptySet() + methodInfos.toSet() + getMethodsReturningClassType(superClassElement.qualifiedName, memoizer) } } @@ -380,12 +388,12 @@ class Memoizer( private val implementsModelCollectorMap = mutableMapOf() fun implementsModelCollector(classElement: XTypeElement): Boolean { return implementsModelCollectorMap.getOrPut(classElement.qualifiedName) { - classElement.getSuperInterfaceElements().any { - it.type.isEpoxyModelCollector(this) - } || classElement.superClass?.typeElement?.let { superClassElement -> - // Also check the class hierarchy - implementsModelCollector(superClassElement) - } ?: false + classElement.getSuperInterfaceElements().any { + it.type.isEpoxyModelCollector(this) + } || classElement.superClass?.typeElement?.let { superClassElement -> + // Also check the class hierarchy + implementsModelCollector(superClassElement) + } ?: false } } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt index 4756da2c41..05d8557a0e 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt @@ -91,12 +91,13 @@ class ModelBuilderInterfaceWriter( addAnnotation(EpoxyBuildScope::class.java) } - if (modelInfo.memoizer.implementsModelCollector(modelInfo.superClassElement)) { + val superClassElement = modelInfo.safeSuperClassElement() + if (modelInfo.memoizer.implementsModelCollector(superClassElement)) { // If the model implements "ModelCollector" we want the builder too addSuperinterface(ClassNames.MODEL_COLLECTOR) } - addOriginatingElement(modelInfo.superClassElement) + addOriginatingElement(superClassElement) } JavaFile.builder(modelInfo.generatedName.packageName(), modelInterface) diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt index b31e7cf261..b00166d243 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt @@ -54,10 +54,10 @@ class ModelViewInfo( superClassElement.name != ClassNames.EPOXY_MODEL_UNTYPED.simpleName() ) { // If the view has a custom base model then we copy any custom constructors on it - constructors.addAll(getClassConstructors(superClassElement)) + constructors.addAll(getClassConstructors(safeSuperClassElement())) } - collectMethodsReturningClassType(superClassElement) + collectMethodsReturningClassType(safeSuperClassElement()) // The bound type is the type of this view modelType = viewElement.type.typeName @@ -91,7 +91,7 @@ class ModelViewInfo( private fun lookUpSuperClassElement(): XTypeElement { val classToExtend = viewAnnotation.getAsType("baseModelClass") ?.takeIf { !it.isVoidObject() && !it.isVoid() } - ?: configManager.getDefaultBaseModel(viewElement) + ?: configManager.getDefaultBaseModel(viewElement, environment) ?: return memoizer.epoxyModelClassElementUntyped val superElement = diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt index a27386602b..fba014610e 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt @@ -612,10 +612,10 @@ class ModelViewProcessor @JvmOverloads constructor( modelClassMap.values.forEach { modelViewInfo -> // Skip generated model super classes since it will already contain all of the functions // necessary for included attributes, and duplicating them is a waste. - if (modelViewInfo.isSuperClassAlsoGenerated) return@forEach + if (modelViewInfo.isSuperClassAlsoGenerated(memoizer)) return@forEach memoizer.getInheritedEpoxyAttributes( - modelViewInfo.superClassElement.type, + modelViewInfo.safeSuperClassElement().type, modelViewInfo.generatedName.packageName(), logger ).let { modelViewInfo.addAttributes(it) } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/PackageModelViewSettings.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/PackageModelViewSettings.kt index 4badc0ddbf..6aba02e0c3 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/PackageModelViewSettings.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/PackageModelViewSettings.kt @@ -1,7 +1,6 @@ package com.airbnb.epoxy.processor import androidx.room.compiler.processing.XAnnotation -import androidx.room.compiler.processing.XType import androidx.room.compiler.processing.XTypeElement import androidx.room.compiler.processing.isVoid import androidx.room.compiler.processing.isVoidObject @@ -29,14 +28,15 @@ class PackageModelViewSettings( annotation.getAsEnum("disableGenerateReset").name ).toBoolean() - val defaultBaseModel: XType? by lazy { - annotation.getAsType("defaultBaseModelClass") - .takeIf { - // The default value of the annotation parameter is Void.class to signal that the user - // does not want to provide a custom base class - !it.isVoid() && !it.isVoidObject() - } - } + // Store the qualified name as a String instead of XType to avoid holding stale KSP references. + // In KSP2, accessing XType from previous rounds triggers "PSI has changed since creation" errors. + // The type must be resolved fresh each round using the current environment. + val defaultBaseModelClassName: String? = annotation.getAsType("defaultBaseModelClass") + .takeIf { + // The default value of the annotation parameter is Void.class to signal that the user + // does not want to provide a custom base class + !it.isVoid() && !it.isVoidObject() + }?.typeElement?.qualifiedName fun getNameForView(viewElement: XTypeElement): ResourceValue { val viewName = Utils.toSnakeCase(viewElement.name) diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ParisStyleAttributeInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ParisStyleAttributeInfo.kt index 358fc5cdb9..b5570a105f 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ParisStyleAttributeInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ParisStyleAttributeInfo.kt @@ -30,7 +30,10 @@ class ParisStyleAttributeInfo( fieldName = PARIS_STYLE_ATTR_NAME rootClass = modelInfo.generatedName.simpleName() this.packageName = packageName - setXType(modelInfo.memoizer.parisStyleType, modelInfo.memoizer) + // Use the memoizer parameter from the current round, not modelInfo.memoizer from round 1. + // modelInfo may have been created in a previous round and its memoizer holds stale KSP references. + // In KSP2, accessing types from previous rounds triggers "PSI has changed since creation" errors. + setXType(memoizer.parisStyleType, memoizer) styleBuilderClass = styleBuilderClassName ignoreRequireHashCode = true isGenerated = true diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Type.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Type.kt index 4fb15f1b46..ec922cbc23 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Type.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Type.kt @@ -11,10 +11,9 @@ import com.squareup.javapoet.WildcardTypeName * This helps to memoize the look up of a type's information. */ class Type(val xType: XType, memoizer: Memoizer) { - val typeName: TypeName by lazy { - xType.typeNameWithWorkaround(memoizer) - } - val typeEnum: TypeEnum by lazy { TypeEnum.from(xType, typeName, memoizer) } + // Compute both typeName and typeEnum eagerly to avoid accessing stale xType in later rounds + val typeName: TypeName = xType.typeNameWithWorkaround(memoizer) + val typeEnum: TypeEnum = TypeEnum.from(xType, memoizer) enum class TypeEnum { StringOrCharSequence, @@ -31,9 +30,10 @@ class Type(val xType: XType, memoizer: Memoizer) { Unknown; companion object { - fun from(xType: XType, typeName: TypeName, memoizer: Memoizer): TypeEnum { + fun from(xType: XType, memoizer: Memoizer): TypeEnum { val nonNullType by lazy { xType.makeNonNullable() } + val typeName by lazy { xType.typeNameWithWorkaround(memoizer) } return when { xType.isInt() -> Int diff --git a/gradle.properties b/gradle.properties index 5a34adb1ee..92d5178113 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=5.2.0 +VERSION_NAME=5.2.1-airbnb14 GROUP=com.airbnb.android POM_DESCRIPTION=Epoxy is a system for composing complex screens with a ReyclerView in Android. POM_URL=https://github.com/airbnb/epoxy From 46e174dc54bd08baa55d62ac0be471666612342e Mon Sep 17 00:00:00 2001 From: elihart Date: Tue, 13 Jan 2026 11:09:44 -0800 Subject: [PATCH 4/5] More fixes for stale elements --- .../airbnb/epoxy/processor/BaseProcessor.kt | 3 ++- .../epoxy/processor/BasicGeneratedModelInfo.kt | 2 +- .../epoxy/processor/DataBindingModelInfo.kt | 2 +- .../epoxy/processor/DataBindingProcessor.kt | 2 +- .../airbnb/epoxy/processor/EpoxyProcessor.kt | 4 ++-- .../epoxy/processor/GeneratedModelInfo.kt | 17 +++++++++++------ .../epoxy/processor/GeneratedModelWriter.kt | 14 +++++++------- .../KotlinModelBuilderExtensionWriter.kt | 18 +++++++++++------- .../processor/ModelBuilderInterfaceWriter.kt | 7 ++++--- .../airbnb/epoxy/processor/ModelViewInfo.kt | 6 +++--- .../epoxy/processor/ModelViewProcessor.kt | 4 ++-- .../airbnb/epoxy/processor/ModelViewWriter.kt | 5 +++-- gradle.properties | 2 +- 13 files changed, 49 insertions(+), 37 deletions(-) diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt index 1c7c94dbfd..bed6d23d24 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BaseProcessor.kt @@ -245,7 +245,8 @@ abstract class BaseProcessor(val kspEnvironment: SymbolProcessorEnvironment? = n // TODO: Potentially generate a single file per model to allow for an isolating processor kotlinExtensionWriter.generateExtensionsForModels( generatedModels, - processorName + processorName, + memoizer ) timer.markStepCompleted("generateKotlinExtensions") } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt index df94d71a73..2455e48232 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/BasicGeneratedModelInfo.kt @@ -89,5 +89,5 @@ internal class BasicGeneratedModelInfo( ) } - override fun additionalOriginatingElements(): List = listOf(safeSuperClassElement()) + override fun additionalOriginatingElements(currentMemoizer: Memoizer): List = listOf(safeSuperClassElement(currentMemoizer)) } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingModelInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingModelInfo.kt index 8fefff0084..64f48d509e 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingModelInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingModelInfo.kt @@ -93,6 +93,6 @@ internal class DataBindingModelInfo( ) } - override fun additionalOriginatingElements() = + override fun additionalOriginatingElements(currentMemoizer: Memoizer) = listOfNotNull(annotatedElement, dataBindingClassElement) } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingProcessor.kt index ad9e80fe0f..08e308f31d 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/DataBindingProcessor.kt @@ -163,7 +163,7 @@ class DataBindingProcessor @JvmOverloads constructor( bindingModelInfo.parseDataBindingClass(logger) ?: return@filter false createModelWriter(memoizer).generateClassForModel( bindingModelInfo, - originatingElements = bindingModelInfo.originatingElements() + originatingElements = bindingModelInfo.originatingElements(memoizer) ) true }.also { writtenModels -> diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt index fed613b489..3c6717b35d 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/EpoxyProcessor.kt @@ -106,7 +106,7 @@ class EpoxyProcessor @JvmOverloads constructor( val styleableModels = modelInfos .filterIsInstance() .filter { modelInfo -> - modelInfo.safeSuperClassElement().getAnnotation(EpoxyModelClass::class)?.getAsInt("layout") == 0 && + modelInfo.safeSuperClassElement(memoizer).getAnnotation(EpoxyModelClass::class)?.getAsInt("layout") == 0 && modelInfo.boundObjectTypeElement?.hasStyleableAnnotation() == true } timer.markStepCompleted("check for styleable models") @@ -136,7 +136,7 @@ class EpoxyProcessor @JvmOverloads constructor( private fun writeModel(modelInfo: GeneratedModelInfo, memoizer: Memoizer) { createModelWriter(memoizer).generateClassForModel( modelInfo, - originatingElements = modelInfo.originatingElements() + originatingElements = modelInfo.originatingElements(memoizer) ) } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt index 26d80b5408..9bb5f9a30b 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelInfo.kt @@ -21,9 +21,12 @@ abstract class GeneratedModelInfo(val memoizer: Memoizer) { /** * Looks up and returns the super class element by FQN, to avoid errors from accessing * an XTypeElement from a previous processing round. + * + * @param currentMemoizer The memoizer from the current processing round. Must be passed + * explicitly to ensure we use a fresh environment, not the stale one stored in `this.memoizer`. */ - fun safeSuperClassElement(): XTypeElement { - return memoizer.environment.requireTypeElement(superClassElement.qualifiedName) + fun safeSuperClassElement(currentMemoizer: Memoizer): XTypeElement { + return currentMemoizer.environment.requireTypeElement(superClassElement.qualifiedName) } lateinit var superClassName: TypeName @@ -73,13 +76,15 @@ abstract class GeneratedModelInfo(val memoizer: Memoizer) { /** * The elements that influence the generation of this model. * eg base model class for @EpoxyModelClass, view class for @ModelView, etc + * + * @param currentMemoizer The memoizer from the current processing round. */ - fun originatingElements(): List { + fun originatingElements(currentMemoizer: Memoizer): List { return listOfNotNull(styleBuilderInfo?.styleBuilderElement) - .plus(additionalOriginatingElements()) + .plus(additionalOriginatingElements(currentMemoizer)) } - open fun additionalOriginatingElements(): List = emptyList() + open fun additionalOriginatingElements(currentMemoizer: Memoizer): List = emptyList() /** * Get information about constructors of the original class so we can duplicate them in the @@ -163,7 +168,7 @@ abstract class GeneratedModelInfo(val memoizer: Memoizer) { */ fun isSuperClassAlsoGenerated(currentMemoizer: Memoizer): Boolean { val generatedModelType = currentMemoizer.generatedModelType - return safeSuperClassElement().type.isSubTypeOf(generatedModelType) + return safeSuperClassElement(currentMemoizer).type.isSubTypeOf(generatedModelType) } data class ConstructorInfo internal constructor( diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt index a022ca3ffc..c95214c6f1 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/GeneratedModelWriter.kt @@ -160,7 +160,7 @@ class GeneratedModelWriter( builderHooks?.beforeFinalBuild(this) - addSuperinterface(modelInterfaceWriter.writeInterface(info, this.build().methodSpecs)) + addSuperinterface(modelInterfaceWriter.writeInterface(info, this.build().methodSpecs, memoizer)) originatingElements.forEach { addOriginatingElement(it) @@ -835,7 +835,7 @@ class GeneratedModelWriter( // bind!!! So we mustn't do that. So, we only call the super diff binding if we think // it's a custom implementation. if (modelImplementsBindWithDiff( - classInfo.safeSuperClassElement(), + classInfo.safeSuperClassElement(memoizer), memoizer ) ) { @@ -1102,7 +1102,7 @@ class GeneratedModelWriter( methods: MutableList ) { - val originalClassElement = modelClassInfo.safeSuperClassElement() + val originalClassElement = modelClassInfo.safeSuperClassElement(memoizer) if (!originalClassElement.type.isEpoxyModelWithHolder(memoizer)) { return } @@ -1178,7 +1178,7 @@ class GeneratedModelWriter( return modelInfo.getLayoutResource(resourceProcessor) } - val superClassElement = modelInfo.safeSuperClassElement() + val superClassElement = modelInfo.safeSuperClassElement(memoizer) if (implementsMethod(superClassElement, buildDefaultLayoutMethodBase(), environment)) { return null } @@ -1205,7 +1205,7 @@ class GeneratedModelWriter( * variables that changed. */ private fun generateDataBindingMethodsIfNeeded(info: GeneratedModelInfo): Iterable { - if (!info.safeSuperClassElement().type.isDataBindingEpoxyModel(memoizer)) { + if (!info.safeSuperClassElement(memoizer).type.isDataBindingEpoxyModel(memoizer)) { return emptyList() } @@ -1221,7 +1221,7 @@ class GeneratedModelWriter( // If the base method is already implemented don't bother checking for the payload method if (implementsMethod( - info.safeSuperClassElement(), + info.safeSuperClassElement(memoizer), bindVariablesMethod, environment ) @@ -1232,7 +1232,7 @@ class GeneratedModelWriter( val generatedModelClass = info.generatedName val moduleName = (info as? DataBindingModelInfo)?.moduleName - ?: dataBindingModuleLookup.getModuleName(info.safeSuperClassElement()) + ?: dataBindingModuleLookup.getModuleName(info.safeSuperClassElement(memoizer)) val baseMethodBuilder = bindVariablesMethod.toBuilder() diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/KotlinModelBuilderExtensionWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/KotlinModelBuilderExtensionWriter.kt index 66e7119190..c2ec9d8981 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/KotlinModelBuilderExtensionWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/KotlinModelBuilderExtensionWriter.kt @@ -20,7 +20,8 @@ internal class KotlinModelBuilderExtensionWriter( fun generateExtensionsForModels( generatedModels: List, - processorName: String + processorName: String, + memoizer: Memoizer ) { generatedModels .filter { it.shouldGenerateModel } @@ -29,7 +30,8 @@ internal class KotlinModelBuilderExtensionWriter( buildExtensionFile( packageName, models, - processorName + processorName, + memoizer ) }.forEach("writeExtensionsForModels", parallel = false) { // Cannot be done in parallel since filer is not thread safe @@ -40,7 +42,8 @@ internal class KotlinModelBuilderExtensionWriter( private fun buildExtensionFile( packageName: String, models: List, - processorName: String + processorName: String, + memoizer: Memoizer ): FileSpec { val fileBuilder = FileSpec.builder( packageName, @@ -49,10 +52,10 @@ internal class KotlinModelBuilderExtensionWriter( models.map { if (it.constructors.isEmpty()) { - listOf(buildExtensionsForModel(it, null)) + listOf(buildExtensionsForModel(it, null, memoizer)) } else { it.constructors.map { constructor -> - buildExtensionsForModel(it, constructor) + buildExtensionsForModel(it, constructor, memoizer) } } } @@ -76,7 +79,8 @@ internal class KotlinModelBuilderExtensionWriter( private fun buildExtensionsForModel( model: GeneratedModelInfo, - constructor: GeneratedModelInfo.ConstructorInfo? + constructor: GeneratedModelInfo.ConstructorInfo?, + memoizer: Memoizer ): FunSpec { val constructorIsNotPublic = constructor != null && Modifier.PUBLIC !in constructor.modifiers @@ -118,7 +122,7 @@ internal class KotlinModelBuilderExtensionWriter( endControlFlow() addStatement(")") - model.originatingElements().forEach { + model.originatingElements(memoizer).forEach { addOriginatingElement(it) } return build() diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt index 05d8557a0e..997227e169 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt @@ -44,7 +44,8 @@ class ModelBuilderInterfaceWriter( fun writeInterface( modelInfo: GeneratedModelInfo, - methods: MutableList + methods: MutableList, + memoizer: Memoizer ): TypeName { val interfaceName = getBuilderInterfaceClassName(modelInfo) @@ -91,8 +92,8 @@ class ModelBuilderInterfaceWriter( addAnnotation(EpoxyBuildScope::class.java) } - val superClassElement = modelInfo.safeSuperClassElement() - if (modelInfo.memoizer.implementsModelCollector(superClassElement)) { + val superClassElement = modelInfo.safeSuperClassElement(memoizer) + if (memoizer.implementsModelCollector(superClassElement)) { // If the model implements "ModelCollector" we want the builder too addSuperinterface(ClassNames.MODEL_COLLECTOR) } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt index b00166d243..2f7ab3025f 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewInfo.kt @@ -54,10 +54,10 @@ class ModelViewInfo( superClassElement.name != ClassNames.EPOXY_MODEL_UNTYPED.simpleName() ) { // If the view has a custom base model then we copy any custom constructors on it - constructors.addAll(getClassConstructors(safeSuperClassElement())) + constructors.addAll(getClassConstructors(safeSuperClassElement(memoizer))) } - collectMethodsReturningClassType(safeSuperClassElement()) + collectMethodsReturningClassType(safeSuperClassElement(memoizer)) // The bound type is the type of this view modelType = viewElement.type.typeName @@ -182,5 +182,5 @@ class ModelViewInfo( return element.parameters.singleOrNull()?.hasDefaultValue == true } - override fun additionalOriginatingElements() = listOf(viewElement) + override fun additionalOriginatingElements(currentMemoizer: Memoizer) = listOf(viewElement) } diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt index fba014610e..ef73e2c294 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewProcessor.kt @@ -615,7 +615,7 @@ class ModelViewProcessor @JvmOverloads constructor( if (modelViewInfo.isSuperClassAlsoGenerated(memoizer)) return@forEach memoizer.getInheritedEpoxyAttributes( - modelViewInfo.safeSuperClassElement().type, + modelViewInfo.safeSuperClassElement(memoizer).type, modelViewInfo.generatedName.packageName(), logger ).let { modelViewInfo.addAttributes(it) } @@ -690,7 +690,7 @@ class ModelViewProcessor @JvmOverloads constructor( val modelWriter = createModelWriter(memoizer) ModelViewWriter(modelWriter, this) - .writeModels(modelsToWrite, originatingConfigElements()) + .writeModels(modelsToWrite, originatingConfigElements(), memoizer) if (styleableModelsToWrite.isEmpty()) { // Make sure all models have been processed and written before we generate interface information diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewWriter.kt index 50cb223de5..71b369be3f 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelViewWriter.kt @@ -18,12 +18,13 @@ internal class ModelViewWriter( fun writeModels( models: List, - originatingConfigElements: List + originatingConfigElements: List, + memoizer: Memoizer ) { models.forEach("Write model view classes") { modelInfo -> modelWriter.generateClassForModel( modelInfo, - originatingElements = originatingConfigElements + modelInfo.originatingElements(), + originatingElements = originatingConfigElements + modelInfo.originatingElements(memoizer), builderHooks = generateBuilderHook(modelInfo) ) } diff --git a/gradle.properties b/gradle.properties index 92d5178113..2ee4872aa1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=5.2.1-airbnb14 +VERSION_NAME=5.2.1-airbnb16 GROUP=com.airbnb.android POM_DESCRIPTION=Epoxy is a system for composing complex screens with a ReyclerView in Android. POM_URL=https://github.com/airbnb/epoxy From e8543fe4d77fc6595b1e37b75129925e7341d6b4 Mon Sep 17 00:00:00 2001 From: elihart Date: Tue, 13 Jan 2026 11:17:11 -0800 Subject: [PATCH 5/5] Bump version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2ee4872aa1..a21bb5d9ec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=5.2.1-airbnb16 +VERSION_NAME=5.2.1 GROUP=com.airbnb.android POM_DESCRIPTION=Epoxy is a system for composing complex screens with a ReyclerView in Android. POM_URL=https://github.com/airbnb/epoxy