diff --git a/api/api.base b/api/api.base index 2d22e989a9..c2442ae898 100644 --- a/api/api.base +++ b/api/api.base @@ -290,6 +290,13 @@ package com.google.devtools.ksp.symbol { property @Nullable public abstract com.google.devtools.ksp.symbol.AnnotationUseSiteTarget useSiteTarget; } + public interface KSBackingField extends com.google.devtools.ksp.symbol.KSDeclaration { + method @InaccessibleFromKotlin @NonNull public com.google.devtools.ksp.symbol.KSPropertyDeclaration getProperty(); + method @InaccessibleFromKotlin @NonNull public com.google.devtools.ksp.symbol.KSTypeReference getType(); + property @NonNull public abstract com.google.devtools.ksp.symbol.KSPropertyDeclaration property; + property @NonNull public abstract com.google.devtools.ksp.symbol.KSTypeReference type; + } + public interface KSCallableReference extends com.google.devtools.ksp.symbol.KSReferenceElement { method public default R accept(@NonNull com.google.devtools.ksp.symbol.KSVisitor visitor, D data); method @InaccessibleFromKotlin @NonNull public java.util.List getFunctionParameters(); @@ -434,6 +441,7 @@ package com.google.devtools.ksp.symbol { public interface KSPropertyDeclaration extends com.google.devtools.ksp.symbol.KSDeclaration { method @NonNull public com.google.devtools.ksp.symbol.KSType asMemberOf(@NonNull com.google.devtools.ksp.symbol.KSType containing); method @Nullable public com.google.devtools.ksp.symbol.KSPropertyDeclaration findOverridee(); + method @InaccessibleFromKotlin @Nullable public com.google.devtools.ksp.symbol.KSBackingField getBackingField(); method @InaccessibleFromKotlin @Nullable public com.google.devtools.ksp.symbol.KSTypeReference getExtensionReceiver(); method @InaccessibleFromKotlin @Nullable public com.google.devtools.ksp.symbol.KSPropertyGetter getGetter(); method @InaccessibleFromKotlin public boolean getHasBackingField(); @@ -441,6 +449,7 @@ package com.google.devtools.ksp.symbol { method @InaccessibleFromKotlin @NonNull public com.google.devtools.ksp.symbol.KSTypeReference getType(); method public boolean isDelegated(); method @InaccessibleFromKotlin public boolean isMutable(); + property @Nullable public abstract com.google.devtools.ksp.symbol.KSBackingField backingField; property @Nullable public abstract com.google.devtools.ksp.symbol.KSTypeReference extensionReceiver; property @Nullable public abstract com.google.devtools.ksp.symbol.KSPropertyGetter getter; property public abstract boolean hasBackingField; @@ -552,6 +561,7 @@ package com.google.devtools.ksp.symbol { public interface KSVisitor { method public R visitAnnotated(@NonNull com.google.devtools.ksp.symbol.KSAnnotated annotated, D data); method public R visitAnnotation(@NonNull com.google.devtools.ksp.symbol.KSAnnotation annotation, D data); + method public R visitBackingField(@NonNull com.google.devtools.ksp.symbol.KSBackingField backingField, D data); method public R visitCallableReference(@NonNull com.google.devtools.ksp.symbol.KSCallableReference reference, D data); method public R visitClassDeclaration(@NonNull com.google.devtools.ksp.symbol.KSClassDeclaration classDeclaration, D data); method public R visitClassifierReference(@NonNull com.google.devtools.ksp.symbol.KSClassifierReference reference, D data); @@ -581,6 +591,7 @@ package com.google.devtools.ksp.symbol { ctor public KSVisitorVoid(); method public void visitAnnotated(@NonNull com.google.devtools.ksp.symbol.KSAnnotated annotated, @NonNull kotlin.Unit data); method public void visitAnnotation(@NonNull com.google.devtools.ksp.symbol.KSAnnotation annotation, @NonNull kotlin.Unit data); + method public void visitBackingField(@NonNull com.google.devtools.ksp.symbol.KSBackingField backingField, @NonNull kotlin.Unit data); method public void visitCallableReference(@NonNull com.google.devtools.ksp.symbol.KSCallableReference reference, @NonNull kotlin.Unit data); method public void visitClassDeclaration(@NonNull com.google.devtools.ksp.symbol.KSClassDeclaration classDeclaration, @NonNull kotlin.Unit data); method public void visitClassifierReference(@NonNull com.google.devtools.ksp.symbol.KSClassifierReference reference, @NonNull kotlin.Unit data); @@ -699,6 +710,7 @@ package com.google.devtools.ksp.visitor { method public abstract R defaultHandler(@NonNull com.google.devtools.ksp.symbol.KSNode node, D data); method public R visitAnnotated(@NonNull com.google.devtools.ksp.symbol.KSAnnotated annotated, D data); method public R visitAnnotation(@NonNull com.google.devtools.ksp.symbol.KSAnnotation annotation, D data); + method public R visitBackingField(@NonNull com.google.devtools.ksp.symbol.KSBackingField backingField, D data); method public R visitCallableReference(@NonNull com.google.devtools.ksp.symbol.KSCallableReference reference, D data); method public R visitClassDeclaration(@NonNull com.google.devtools.ksp.symbol.KSClassDeclaration classDeclaration, D data); method public R visitClassifierReference(@NonNull com.google.devtools.ksp.symbol.KSClassifierReference reference, D data); @@ -733,6 +745,7 @@ package com.google.devtools.ksp.visitor { method @NonNull public Boolean defaultHandler(@NonNull com.google.devtools.ksp.symbol.KSNode node, @Nullable com.google.devtools.ksp.symbol.KSNode data); method @NonNull public Boolean visitAnnotated(@NonNull com.google.devtools.ksp.symbol.KSAnnotated annotated, @Nullable com.google.devtools.ksp.symbol.KSNode data); method @NonNull public Boolean visitAnnotation(@NonNull com.google.devtools.ksp.symbol.KSAnnotation annotation, @Nullable com.google.devtools.ksp.symbol.KSNode data); + method @NonNull public Boolean visitBackingField(@NonNull com.google.devtools.ksp.symbol.KSBackingField backingField, @Nullable com.google.devtools.ksp.symbol.KSNode data); method @NonNull public Boolean visitClassDeclaration(@NonNull com.google.devtools.ksp.symbol.KSClassDeclaration classDeclaration, @Nullable com.google.devtools.ksp.symbol.KSNode data); method @NonNull public Boolean visitDeclaration(@NonNull com.google.devtools.ksp.symbol.KSDeclaration declaration, @Nullable com.google.devtools.ksp.symbol.KSNode data); method @NonNull public Boolean visitDeclarationContainer(@NonNull com.google.devtools.ksp.symbol.KSDeclarationContainer declarationContainer, @Nullable com.google.devtools.ksp.symbol.KSNode data); diff --git a/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSBackingField.kt b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSBackingField.kt new file mode 100644 index 0000000000..5ad1f745f9 --- /dev/null +++ b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSBackingField.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +package com.google.devtools.ksp.symbol + +/** + * A backing field for a [KSPropertyDeclaration]. + */ +interface KSBackingField : KSDeclaration { + + /** + * The property that this backing field belongs to. + */ + val property: KSPropertyDeclaration + + /** + * The type of the backing field. + * This is guaranteed to be a subtype of the type of [property]. + */ + val type: KSTypeReference +} diff --git a/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSPropertyDeclaration.kt b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSPropertyDeclaration.kt index 1910e77cc4..50786a9b11 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSPropertyDeclaration.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSPropertyDeclaration.kt @@ -60,6 +60,15 @@ interface KSPropertyDeclaration : KSDeclaration { */ val hasBackingField: Boolean + /** + * The property's backing field if it exists. + * Guaranteed to exist if [hasBackingField] holds. + * + * As with [hasBackingField], the backing field is specific to the current property, + * and does not check for overriding properties. + */ + val backingField: KSBackingField? + /** * Indicates whether this is a delegated property. */ diff --git a/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitor.kt b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitor.kt index 6ec4ca1f6a..d524546090 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitor.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitor.kt @@ -50,6 +50,8 @@ interface KSVisitor { fun visitPropertySetter(setter: KSPropertySetter, data: D): R + fun visitBackingField(backingField: KSBackingField, data: D): R + fun visitReferenceElement(element: KSReferenceElement, data: D): R fun visitTypeAlias(typeAlias: KSTypeAlias, data: D): R diff --git a/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitorVoid.kt b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitorVoid.kt index 1e729d95fb..e9e1a18c25 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitorVoid.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitorVoid.kt @@ -65,6 +65,9 @@ open class KSVisitorVoid : KSVisitor { override fun visitPropertySetter(setter: KSPropertySetter, data: Unit) { } + override fun visitBackingField(backingField: KSBackingField, data: Unit) { + } + override fun visitClassifierReference(reference: KSClassifierReference, data: Unit) { } diff --git a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSDefaultVisitor.kt b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSDefaultVisitor.kt index 19ea484f85..3cfc17a0ea 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSDefaultVisitor.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSDefaultVisitor.kt @@ -70,6 +70,11 @@ abstract class KSDefaultVisitor : KSEmptyVisitor() { return super.visitPropertySetter(setter, data) } + override fun visitBackingField(backingField: KSBackingField, data: D): R { + this.visitAnnotated(backingField, data) + return super.visitBackingField(backingField, data) + } + override fun visitTypeAlias(typeAlias: KSTypeAlias, data: D): R { this.visitDeclaration(typeAlias, data) return super.visitTypeAlias(typeAlias, data) diff --git a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSEmptyVisitor.kt b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSEmptyVisitor.kt index 4704f81e49..b9d85f8d14 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSEmptyVisitor.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSEmptyVisitor.kt @@ -84,6 +84,10 @@ abstract class KSEmptyVisitor : KSVisitor { return defaultHandler(setter, data) } + override fun visitBackingField(backingField: KSBackingField, data: D): R { + return defaultHandler(backingField, data) + } + override fun visitClassifierReference(reference: KSClassifierReference, data: D): R { return defaultHandler(reference, data) } diff --git a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSTopDownVisitor.kt b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSTopDownVisitor.kt index 9a3f5d04e2..ed901e609c 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSTopDownVisitor.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSTopDownVisitor.kt @@ -38,6 +38,7 @@ abstract class KSTopDownVisitor : KSDefaultVisitor() { property.extensionReceiver?.accept(data) property.getter?.accept(data) property.setter?.accept(data) + property.backingField?.accept(data) return super.visitPropertyDeclaration(property, data) } @@ -96,6 +97,11 @@ abstract class KSTopDownVisitor : KSDefaultVisitor() { return super.visitPropertySetter(setter, data) } + override fun visitBackingField(backingField: KSBackingField, data: D): R { + backingField.type.accept(data) + return super.visitBackingField(backingField, data) + } + override fun visitReferenceElement(element: KSReferenceElement, data: D): R { element.typeArguments.accept(data) return super.visitReferenceElement(element, data) diff --git a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSValidateVisitor.kt b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSValidateVisitor.kt index 4947a41f1d..09bfacb3a6 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSValidateVisitor.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/visitor/KSValidateVisitor.kt @@ -98,6 +98,10 @@ open class KSValidateVisitor( return true } + override fun visitBackingField(backingField: KSBackingField, data: KSNode?): Boolean { + TODO("Not yet implemented") + } + override fun visitValueArgument(valueArgument: KSValueArgument, data: KSNode?): Boolean { fun visitValue(value: Any?): Boolean = when (value) { is KSType -> this.validateType(value) diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSBackingFieldImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSBackingFieldImpl.kt new file mode 100644 index 0000000000..365d676544 --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSBackingFieldImpl.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +package com.google.devtools.ksp.impl.symbol.kotlin + +import com.google.devtools.ksp.common.KSObjectCache +import com.google.devtools.ksp.common.impl.KSNameImpl +import com.google.devtools.ksp.impl.symbol.kotlin.resolved.KSTypeReferenceResolvedImpl +import com.google.devtools.ksp.symbol.KSBackingField +import com.google.devtools.ksp.symbol.KSExpectActual +import com.google.devtools.ksp.symbol.KSName +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.symbol.KSVisitor +import org.jetbrains.kotlin.analysis.api.symbols.KaBackingFieldSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol +import org.jetbrains.kotlin.analysis.api.types.abbreviationOrSelf +import org.jetbrains.kotlin.psi.KtBackingField + +class KSBackingFieldImpl private constructor(val kaBackingFieldSymbol: KaBackingFieldSymbol) : + KSBackingField, + AbstractKSDeclarationImpl(), + KSExpectActual by KSExpectActualImpl(kaBackingFieldSymbol) { + override val ktDeclarationSymbol: KaDeclarationSymbol get() = kaBackingFieldSymbol + + companion object : KSObjectCache() { + fun getCached(kaBackingFieldSymbol: KaBackingFieldSymbol) = + cache.getOrPut(kaBackingFieldSymbol) { KSBackingFieldImpl(kaBackingFieldSymbol) } + + @JvmStatic + private fun getFieldNameFrom(parentName: String): String { + val suffix = ".field" + val length = parentName.length + suffix.length + return buildString(length) { + append(parentName) + append(suffix) + } + } + } + + override val type: KSTypeReference by lazy { + ( + kaBackingFieldSymbol.psiIfSource() as? KtBackingField + ) + ?.typeReference?.let { + KSTypeReferenceImpl.getCached(it, this) + } ?: KSTypeReferenceResolvedImpl.getCached( + kaBackingFieldSymbol.returnType.abbreviationOrSelf, + this + ) + } + + override val property: KSPropertyDeclaration by lazy { + KSPropertyDeclarationImpl.getCached(kaBackingFieldSymbol.owningProperty) + } + + override val simpleName: KSName by lazy { + kaBackingFieldSymbol.owningProperty.name.asString().let { propName -> + KSNameImpl.getCached(getFieldNameFrom(propName)) + } + } + + override val qualifiedName: KSName? by lazy { + // N.B.: kaBackingFieldSymbol.callableId is always null so we use the fqn of the property instead + kaBackingFieldSymbol.owningProperty.callableId?.asSingleFqName()?.asString()?.let { propName -> + KSNameImpl.getCached(getFieldNameFrom(propName)) + } + } + + override fun defer(): Restorable = + kaBackingFieldSymbol.defer(::getCached) + + override fun accept(visitor: KSVisitor, data: D): R = + visitor.visitBackingField(this, data) +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSBackingFieldJavaImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSBackingFieldJavaImpl.kt new file mode 100644 index 0000000000..a56c86dffa --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSBackingFieldJavaImpl.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +package com.google.devtools.ksp.impl.symbol.kotlin + +import com.google.devtools.ksp.common.KSObjectCache +import com.google.devtools.ksp.impl.symbol.kotlin.KSPropertyDeclarationJavaImpl.Companion.getCached +import com.google.devtools.ksp.impl.symbol.kotlin.resolved.KSTypeReferenceResolvedImpl +import com.google.devtools.ksp.symbol.KSBackingField +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSName +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.symbol.KSVisitor +import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaJavaFieldSymbol + +class KSBackingFieldJavaImpl private constructor( + val ktJavaFieldSymbol: KaJavaFieldSymbol, + override val property: KSPropertyDeclaration +) : KSBackingField, AbstractKSDeclarationImpl() { + + companion object : KSObjectCache, KSBackingFieldJavaImpl>() { + fun getCached(symbolAndProperty: Pair) = + cache.getOrPut(symbolAndProperty) { + KSBackingFieldJavaImpl(symbolAndProperty.first, symbolAndProperty.second) + } + } + + override val ktDeclarationSymbol: KaDeclarationSymbol + get() = ktJavaFieldSymbol + + override val type: KSTypeReference by lazy { + KSTypeReferenceResolvedImpl.getCached(ktJavaFieldSymbol.returnType, this) + } + override val qualifiedName: KSName? get() = property.qualifiedName + + // Manual delegation for KSExpectActual to avoid eager evaluation in the class header + private val expectActualImpl by lazy { KSExpectActualImpl(ktJavaFieldSymbol) } + override val isActual: Boolean get() = expectActualImpl.isActual + override val isExpect: Boolean get() = expectActualImpl.isExpect + override fun findActuals(): Sequence = expectActualImpl.findActuals() + override fun findExpects(): Sequence = expectActualImpl.findExpects() + + override fun accept(visitor: KSVisitor, data: D): R = + visitor.visitBackingField(this, data) + + override fun defer(): Restorable = ktJavaFieldSymbol.defer(::getCached) +} diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationImpl.kt index b1cced4a18..4f6423a1f3 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationImpl.kt @@ -19,6 +19,7 @@ package com.google.devtools.ksp.impl.symbol.kotlin +import com.google.devtools.ksp.InternalKSPException import com.google.devtools.ksp.closestClassDeclaration import com.google.devtools.ksp.common.KSObjectCache import com.google.devtools.ksp.common.impl.KSNameImpl @@ -28,6 +29,7 @@ import com.google.devtools.ksp.impl.recordLookupForPropertyOrMethod import com.google.devtools.ksp.impl.recordLookupWithSupertypes import com.google.devtools.ksp.impl.symbol.kotlin.resolved.KSAnnotationResolvedImpl import com.google.devtools.ksp.impl.symbol.kotlin.resolved.KSTypeReferenceResolvedImpl +import com.google.devtools.ksp.impl.symbol.kotlin.synthetic.KSSyntheticJavaBackingFieldImpl import com.google.devtools.ksp.impl.symbol.util.BinaryClassInfoCache import com.google.devtools.ksp.symbol.* import com.intellij.psi.PsiClass @@ -39,6 +41,7 @@ import org.jetbrains.kotlin.analysis.api.symbols.KaKotlinPropertySymbol import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySymbol import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolModality import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolVisibility +import org.jetbrains.kotlin.analysis.api.symbols.KaSyntheticJavaPropertySymbol import org.jetbrains.kotlin.analysis.api.symbols.receiverType import org.jetbrains.kotlin.analysis.api.types.abbreviationOrSelf import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget @@ -52,6 +55,7 @@ class KSPropertyDeclarationImpl private constructor(internal val ktPropertySymbo AbstractKSDeclarationImpl(), KSExpectActual by KSExpectActualImpl(ktPropertySymbol) { override val ktDeclarationSymbol get() = ktPropertySymbol + companion object : KSObjectCache() { fun getCached(ktPropertySymbol: KaPropertySymbol) = cache.getOrPut(ktPropertySymbol) { KSPropertyDeclarationImpl(ktPropertySymbol) } @@ -150,6 +154,31 @@ class KSPropertyDeclarationImpl private constructor(internal val ktPropertySymbo } } + override val backingField: KSBackingField? by lazy { + if (hasBackingField) { + when (ktPropertySymbol) { + is KaKotlinPropertySymbol -> + ktPropertySymbol.backingFieldSymbol + ?.let { KSBackingFieldImpl(it) } + ?: throw InternalKSPException( + buildString { + append("Unexpected null backing field symbol for property ") + append(qualifiedName?.asString() ?: simpleName.asString()) + }, + location, + ktPropertySymbol.javaClass + ) + + is KaSyntheticJavaPropertySymbol -> { + // Kotlin calling into a synthetic Java method. + KSSyntheticJavaBackingFieldImpl.getCached(ktPropertySymbol) + } + } + } else { + null + } + } + override fun isDelegated(): Boolean { return ktPropertySymbol.isDelegatedProperty } diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationJavaImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationJavaImpl.kt index 0572f1f775..3ca628bae1 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationJavaImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationJavaImpl.kt @@ -76,6 +76,15 @@ sealed class KSPropertyDeclarationJavaImpl : KSPropertyDeclaration, AbstractKSDe override val hasBackingField: Boolean get() = true + override val backingField: KSBackingField? by lazy { + // N.B.: We cannot use `this` as the backing field + // since it is ambiguous which `visitor` method to call in the `this.accept` then. + // Since we are always calling `visitor.visitPropertyDeclaration`, we will get a stackoverflow + // if we try to recursively visit the backing field. + // Thus, we create a new object with a distinct type here to break the cycle. + KSBackingFieldJavaImpl.getCached(ktJavaFieldSymbol to this) + } + override fun isDelegated(): Boolean { return false } diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationLocalVariableImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationLocalVariableImpl.kt index 2c3056bba7..389d688cdd 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationLocalVariableImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/KSPropertyDeclarationLocalVariableImpl.kt @@ -2,6 +2,7 @@ package com.google.devtools.ksp.impl.symbol.kotlin import com.google.devtools.ksp.common.KSObjectCache import com.google.devtools.ksp.impl.symbol.kotlin.resolved.KSTypeReferenceResolvedImpl +import com.google.devtools.ksp.symbol.KSBackingField import com.google.devtools.ksp.symbol.KSExpectActual import com.google.devtools.ksp.symbol.KSName import com.google.devtools.ksp.symbol.KSPropertyDeclaration @@ -41,6 +42,8 @@ class KSPropertyDeclarationLocalVariableImpl private constructor( override val hasBackingField: Boolean = false + override val backingField: KSBackingField? = null + override fun isDelegated(): Boolean = false override fun findOverridee() = null diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/synthetic/KSSyntheticJavaBackingFieldImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/synthetic/KSSyntheticJavaBackingFieldImpl.kt new file mode 100644 index 0000000000..1c8f576785 --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/symbol/kotlin/synthetic/KSSyntheticJavaBackingFieldImpl.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2026 Google LLC + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + +package com.google.devtools.ksp.impl.symbol.kotlin.synthetic + +import com.google.devtools.ksp.common.KSObjectCache +import com.google.devtools.ksp.impl.symbol.kotlin.AbstractKSDeclarationImpl +import com.google.devtools.ksp.impl.symbol.kotlin.KSExpectActualImpl +import com.google.devtools.ksp.impl.symbol.kotlin.KSPropertyDeclarationImpl +import com.google.devtools.ksp.impl.symbol.kotlin.Restorable +import com.google.devtools.ksp.impl.symbol.kotlin.defer +import com.google.devtools.ksp.symbol.KSBackingField +import com.google.devtools.ksp.symbol.KSExpectActual +import com.google.devtools.ksp.symbol.KSName +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.symbol.KSVisitor +import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaSyntheticJavaPropertySymbol + +/** + * Represents a backing field of a synthetic Java property. + * + * A synthetic Java property is generated by the Kotlin compiler when Kotlin code references a Java field. + * The problem is that [KaSyntheticJavaPropertySymbol] is the synthetic property symbol, which states + * that there is a backing field available (there is a 1:1 correspondence between properties and fields in Java). + * However, the class does not provide access to the field itself or the declared Java property. + */ +class KSSyntheticJavaBackingFieldImpl private constructor( + val kaSyntheticJavaPropertySymbol: KaSyntheticJavaPropertySymbol +) : + KSBackingField, + AbstractKSDeclarationImpl(), + KSExpectActual by KSExpectActualImpl(kaSyntheticJavaPropertySymbol) { + + override val ktDeclarationSymbol: KaDeclarationSymbol get() = kaSyntheticJavaPropertySymbol + + companion object : KSObjectCache() { + fun getCached(symbol: KaSyntheticJavaPropertySymbol) = + cache.getOrPut(symbol) { KSSyntheticJavaBackingFieldImpl(symbol) } + } + + override val property: KSPropertyDeclaration by lazy { + KSPropertyDeclarationImpl.getCached(kaSyntheticJavaPropertySymbol) + } + + override val type: KSTypeReference by lazy { + KSPropertyDeclarationImpl.getCached(kaSyntheticJavaPropertySymbol).type + } + + override val qualifiedName: KSName? by lazy { + KSPropertyDeclarationImpl.getCached(kaSyntheticJavaPropertySymbol).qualifiedName + } + + override fun defer(): Restorable = + kaSyntheticJavaPropertySymbol.defer(::getCached) + + override fun accept(visitor: KSVisitor, data: D): R = + visitor.visitBackingField(this, data) +} diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AAConfiguredUnitTestSuite.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AAConfiguredUnitTestSuite.kt index 5617741590..8de059d156 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AAConfiguredUnitTestSuite.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AAConfiguredUnitTestSuite.kt @@ -34,12 +34,6 @@ class AAConfiguredUnitTestSuite : KSPUnitTestSuite(experimentalPsiResolution = f runFailingTest("$AA_PATH/getSymbolsWithAnnotation/contextParameters.kt") } - @TestMetadata("getSymbolsWithAnnotation/explicitBackFields.kt") - @Test - override fun testExplicitBackingFields() { - runFailingTest("$AA_PATH/getSymbolsWithAnnotation/explicitBackingFields.kt") - } - @TestMetadata("javaSubtypeOfKotlinInterface.kt") @Test override fun testJavaSubtypeOfKotlinInterface() { diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt index e17e20b633..fcf96ebcc1 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/KSPUnitTestSuite.kt @@ -313,12 +313,16 @@ abstract class KSPUnitTestSuite( runTest("$AA_PATH/errorTypes.kt") } + @TestMetadata("getSymbolsWithAnnotation/explicitBackingFields.kt") + @Test @Bug( "https://github.com/google/ksp/issues/2873", - BugState.OPEN, + BugState.FIXED, "KEEP 430: Explicit backing fields added in Kotlin 2.4.0" ) - abstract fun testExplicitBackingFields() + fun testExplicitBackingFields() { + runTest("$AA_PATH/getSymbolsWithAnnotation/explicitBackingFields.kt") + } @TestMetadata("fieldAndPropertyUseSiteTargetOnConstructorParameters.kt") @Test diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/PsiConfiguredUnitTestSuite.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/PsiConfiguredUnitTestSuite.kt index a46ee90a21..7c4cdf84f9 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/PsiConfiguredUnitTestSuite.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/PsiConfiguredUnitTestSuite.kt @@ -34,12 +34,6 @@ class PsiConfiguredUnitTestSuite : KSPUnitTestSuite(experimentalPsiResolution = runThrowingTest("$AA_PATH/getSymbolsWithAnnotation/contextParameters.kt") } - @TestMetadata("getSymbolsWithAnnotation/explicitBackFields.kt") - @Test - override fun testExplicitBackingFields() { - runThrowingTest("$AA_PATH/getSymbolsWithAnnotation/explicitBackingFields.kt") - } - @TestMetadata("javaSubtypeOfKotlinInterface.kt") @Test override fun testJavaSubtypeOfKotlinInterface() { diff --git a/kotlin-analysis-api/testData/annotationInDependencies.kt b/kotlin-analysis-api/testData/annotationInDependencies.kt index 7f9edab6dd..587d4d6a3a 100644 --- a/kotlin-analysis-api/testData/annotationInDependencies.kt +++ b/kotlin-analysis-api/testData/annotationInDependencies.kt @@ -21,6 +21,9 @@ // main.KotlinClass -> // class main.KotlinClass 180 : annotations.ClassTarget{[value = onClass : 179]} : 179 // class main.KotlinClass 180 : annotations.NoTargetAnnotation{[value = onClass : 178]} : 178 +// field of property prop 189 : annotations.AllTarget{[value = all: : 188]} : +// field of property prop 189 : annotations.FieldTarget2{[value = field: : 186]} : +// field of property prop 189 : annotations.FieldTarget{[value = onProp : 182]} : // function myFun 193 : annotations.FunctionTarget{[value = onMyFun : 192]} : 192 // function myFun 193 : annotations.NoTargetAnnotation{[value = onMyFun : 191]} : 191 // getter of property prop 189 : annotations.AllTarget{[value = all: : 188]} : @@ -41,6 +44,9 @@ // lib.KotlinClass -> // class lib.KotlinClass : annotations.ClassTarget{[value = onClass : ]} : // class lib.KotlinClass : annotations.NoTargetAnnotation{[value = onClass : ]} : +// field of property prop : annotations.AllTarget{[value = all: : ]} : +// field of property prop : annotations.FieldTarget2{[value = field: : ]} : +// field of property prop : annotations.FieldTarget{[value = onProp : ]} : // function myFun : annotations.FunctionTarget{[value = onMyFun : ]} : // function myFun : annotations.NoTargetAnnotation{[value = onMyFun : ]} : // getter of property prop : annotations.AllTarget{[value = all: : ]} : @@ -61,6 +67,10 @@ // main.DataClass -> // class main.DataClass 206 : annotations.ClassTarget{[value = onDataClass : 205]} : 205 // class main.DataClass 206 : annotations.NoTargetAnnotation{[value = onDataClass : 204]} : 204 +// field of property constructorParam 217 : annotations.AllTarget{[value = all: : 216]} : +// field of property constructorParam 217 : annotations.FieldTarget2{[value = field: : 212]} : +// field of property constructorParam 217 : annotations.FieldTarget{[value = onConstructorParam : 208]} : +// field of property constructorParam 217 : annotations.ValueParameterAndFieldTarget{[value = valueParameterAndField : 213]} : // getter of property constructorParam 217 : annotations.AllTarget{[value = all: : 216]} : // getter of property constructorParam 217 : annotations.PropertyGetterTarget{[value = get: : 211]} : // parameter constructorParam 217 : annotations.AllTarget{[value = all: : 216]} : @@ -79,6 +89,9 @@ // lib.DataClass -> // class lib.DataClass : annotations.ClassTarget{[value = onDataClass : ]} : // class lib.DataClass : annotations.NoTargetAnnotation{[value = onDataClass : ]} : +// field of property constructorParam : annotations.AllTarget{[value = all: : ]} : +// field of property constructorParam : annotations.FieldTarget2{[value = field: : ]} : +// field of property constructorParam : annotations.FieldTarget{[value = onConstructorParam : ]} : // getter of property constructorParam : annotations.AllTarget{[value = all: : ]} : // getter of property constructorParam : annotations.PropertyGetterTarget{[value = get: : ]} : // parameter constructorParam : annotations.AllTarget{[value = all: : ]} : diff --git a/kotlin-analysis-api/testData/equivalentJavaWildcards.kt b/kotlin-analysis-api/testData/equivalentJavaWildcards.kt index 267414f8a7..43c696439f 100644 --- a/kotlin-analysis-api/testData/equivalentJavaWildcards.kt +++ b/kotlin-analysis-api/testData/equivalentJavaWildcards.kt @@ -18,7 +18,7 @@ // WITH_RUNTIME // TEST PROCESSOR: EquivalentJavaWildcardProcessor // EXPECTED: -// bar1 : [@JvmSuppressWildcards(true)] A -> A +//bar1 : [@JvmSuppressWildcards(true)] A -> A // - INVARIANT X : X -> X // - INVARIANT X : X -> X // - @JvmSuppressWildcards : JvmSuppressWildcards -> JvmSuppressWildcards @@ -36,28 +36,42 @@ // p1.getter() : A -> A // - CONTRAVARIANT X : X -> X // - COVARIANT X : X -> X +// p1.field : A -> A TODO +// - CONTRAVARIANT X : X -> X +// - COVARIANT X : X -> X // p2 : A -> A // - INVARIANT Any : Any -> Any // - INVARIANT Y : Y -> Y // p2.getter() : A -> A // - INVARIANT Any : Any -> Any // - INVARIANT Y : Y -> Y +// p2.field : A -> A +// - INVARIANT Any : Any -> Any +// - INVARIANT Y : Y -> Y // p3 : A<*, *> -> A // p3.getter() : A<*, *> -> A +// p3.field : A<*, *> -> A TODO // p4 : B -> B // - INVARIANT X : X -> X // p4.getter() : B -> B // - INVARIANT X : X -> X +// p4.field : B -> B +// - INVARIANT X : X -> X // p5 : B -> B // - CONTRAVARIANT X : X -> X // p5.getter() : B -> B // - CONTRAVARIANT X : X -> X +// p5.field : B -> B TODO +// - CONTRAVARIANT X : X -> X // p6 : B -> B // - COVARIANT X : X -> X // p6.getter() : B -> B // - COVARIANT X : X -> X +// p6.field : B -> B TODO +// - COVARIANT X : X -> X // p7 : B<*> -> B // p7.getter() : B<*> -> B +// p7.field : B<*> -> B // p8 : B> -> B> // - INVARIANT A : A -> A // - - INVARIANT X : X -> X @@ -66,6 +80,10 @@ // - INVARIANT A : A -> A // - - INVARIANT X : X -> X // - - COVARIANT Y : Y -> Y +// p8.field : B> -> B> +// - INVARIANT A : A -> A +// - - INVARIANT X : X -> X +// - - COVARIANT Y : Y -> Y // v1 : A -> A // - INVARIANT X : X -> X // - INVARIANT X : X -> X diff --git a/kotlin-analysis-api/testData/javaWildcards2.kt b/kotlin-analysis-api/testData/javaWildcards2.kt index 1cc0015a87..d92e4f6581 100644 --- a/kotlin-analysis-api/testData/javaWildcards2.kt +++ b/kotlin-analysis-api/testData/javaWildcards2.kt @@ -30,24 +30,31 @@ // propWithFinalType : String // propWithFinalType.getter() : String // value : String +// propWithFinalType.field : String // propWithOpenType : Number // propWithOpenType.getter() : Number // value : Number +// propWithOpenType.field : Number // propWithFinalGeneric : List // propWithFinalGeneric.getter() : List // value : List +// propWithFinalGeneric.field : List // propWithOpenGeneric : List // propWithOpenGeneric.getter() : List // value : List +// propWithOpenGeneric.field : List // propWithTypeArg : R // propWithTypeArg.getter() : R // value : R +// propWithTypeArg.field : R // propWithTypeArgGeneric : List // propWithTypeArgGeneric.getter() : List // value : List +// propWithTypeArgGeneric.field : List // propWithOpenTypeButSuppressAnnotation : Number // propWithOpenTypeButSuppressAnnotation.getter() : Number // value : Number +// propWithOpenTypeButSuppressAnnotation.field : Number // list2 : List // list1 : List // list3 : List diff --git a/kotlin-analysis-api/testData/javaWildcardsSelfReferencing.kt b/kotlin-analysis-api/testData/javaWildcardsSelfReferencing.kt index 401fe82d21..59714209d8 100644 --- a/kotlin-analysis-api/testData/javaWildcardsSelfReferencing.kt +++ b/kotlin-analysis-api/testData/javaWildcardsSelfReferencing.kt @@ -20,6 +20,7 @@ // EXPECTED: // ref : SelfRef // ref.getter() : SelfRef +// ref.field : SelfRef // SelfRef : Any // A : SelfRef // synthetic constructor for SelfRef : SelfRef diff --git a/kotlin-analysis-api/testData/libOrigins.kt b/kotlin-analysis-api/testData/libOrigins.kt index 0f864a8ef5..922dabd8ee 100644 --- a/kotlin-analysis-api/testData/libOrigins.kt +++ b/kotlin-analysis-api/testData/libOrigins.kt @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +// TODO: Figure out the purpose of this test and if origins are handled correctly for fields // TEST PROCESSOR: LibOriginsProcessor // EXPECTED: diff --git a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationsInDependenciesProcessor.kt b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationsInDependenciesProcessor.kt index e351f730da..fd48aa04dc 100644 --- a/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationsInDependenciesProcessor.kt +++ b/test-utils/src/main/kotlin/com/google/devtools/ksp/processor/AnnotationsInDependenciesProcessor.kt @@ -55,6 +55,7 @@ class AnnotationsInDependenciesProcessor : AbstractTestProcessor() { } ?: "no-name-value-parameter ${this.location.lineNumber}" is KSPropertyGetter -> "getter of ${receiver.toSignature()}" // lineNumber handled by recursive call is KSPropertySetter -> "setter of ${receiver.toSignature()}" // lineNumber handled by recursive call + is KSBackingField -> "field of ${property.toSignature()}" // lineNumber handled by recursive call else -> { error("unexpected annotated") }