diff --git a/api/src/main/kotlin/com/google/devtools/ksp/utils.kt b/api/src/main/kotlin/com/google/devtools/ksp/utils.kt index cb1836d539..05d5ba41d3 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/utils.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/utils.kt @@ -176,12 +176,25 @@ fun KSNode.validate( } /** Find the KSClassDeclaration that the alias points to, recursively. */ -fun KSTypeAlias.findActualType(): KSClassDeclaration { - val resolvedType = this.type.resolve().declaration - return if (resolvedType is KSTypeAlias) { - resolvedType.findActualType() - } else { - resolvedType as KSClassDeclaration +fun KSTypeAlias.findActualType(): KSClassDeclaration = expandTypeAlias(mutableSetOf()) + +private fun KSTypeAlias.expandTypeAlias(visited: MutableSet): KSClassDeclaration { + if (!visited.add(this)) { + throw InternalKSPException( + "Circular type alias declaration: '${qualifiedName?.asString()}'", + this.location, + this.javaClass + ) + } + + return when (val resolved = type.resolve().declaration) { + is KSTypeAlias -> resolved.expandTypeAlias(visited) + is KSClassDeclaration -> resolved + else -> throw InternalKSPException( + "Type alias expanded to unknown declaration: '$resolved'", + resolved.location, + resolved.javaClass + ) } } @@ -225,10 +238,22 @@ internal fun KSDeclaration.isKotlinBackingField(): Boolean = * get all super types for a class declaration Calling [getAllSuperTypes] requires type resolution * therefore is expensive and should be avoided if possible. */ -fun KSClassDeclaration.getAllSuperTypes(): Sequence { +fun KSClassDeclaration.getAllSuperTypes(): Sequence = + Sequence { getAllSuperTypes(mutableSetOf(), mutableSetOf()).iterator() } + +private fun KSClassDeclaration.getAllSuperTypes( + visitedClasses: MutableSet, + visitedParams: MutableSet, +): Sequence { + if (!visitedClasses.add(this)) { + return emptySequence() + } - fun KSTypeParameter.getTypesUpperBound(): Sequence = - this.bounds.flatMap { + fun KSTypeParameter.getTypesUpperBound(): Sequence { + if (!visitedParams.add(this)) { + return emptySequence() + } + return bounds.flatMap { when (val resolvedDeclaration = it.resolve().declaration) { is KSClassDeclaration -> sequenceOf(resolvedDeclaration) is KSTypeAlias -> sequenceOf(resolvedDeclaration.findActualType()) @@ -241,29 +266,29 @@ fun KSClassDeclaration.getAllSuperTypes(): Sequence { ) } } + } - return this.superTypes - .map { it.resolve() } - .plus( - this.superTypes - .map { it.resolve().declaration } + fun findClassDeclarations(declaration: KSDeclaration): Sequence = when (declaration) { + is KSClassDeclaration -> sequenceOf(declaration) + is KSTypeAlias -> sequenceOf(declaration.findActualType()) + is KSTypeParameter -> declaration.getTypesUpperBound() + else -> throw InternalKSPException( + "Unhandled super type kind", + declaration.location, + declaration.javaClass, + ) + } + + val immediateSuperTypes = superTypes.map { it.resolve() } + val transitiveSuperTypes = immediateSuperTypes + .flatMap { type -> + findClassDeclarations(type.declaration) .flatMap { - when (it) { - is KSClassDeclaration -> it.getAllSuperTypes() - is KSTypeAlias -> it.findActualType().getAllSuperTypes() - is KSTypeParameter -> - it.getTypesUpperBound().flatMap { it.getAllSuperTypes() } - - else -> - throw InternalKSPException( - "Unhandled super type kind", - it.location, - it.javaClass, - ) - } + it.getAllSuperTypes(visitedClasses, visitedParams) } - ) - .distinct() + } + + return (immediateSuperTypes + transitiveSuperTypes).distinct() } fun KSClassDeclaration.isAbstract() = diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/processor/AllSuperTypesProcessor.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/processor/AllSuperTypesProcessor.kt new file mode 100644 index 0000000000..849e75c7b2 --- /dev/null +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/processor/AllSuperTypesProcessor.kt @@ -0,0 +1,43 @@ +/* + * 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.processor + +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotated + +class AllSuperTypesProcessor(val classNames: List, override val enableNewFeatures: Boolean) : + AbstractTestProcessor() { + private val results: MutableList = mutableListOf() + + override fun toResult(): List { + return results + } + + override fun process(resolver: Resolver): List { + classNames.forEach { className -> + resolver.getClassDeclarationByName(className) + ?.getAllSuperTypes() + ?.forEach { type -> + results.add(type.declaration.qualifiedName!!.asString()) + } + } + return emptyList() + } +} diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/processor/AllSuperTypesRepeatableProcessor.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/processor/AllSuperTypesRepeatableProcessor.kt new file mode 100644 index 0000000000..f222abf088 --- /dev/null +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/processor/AllSuperTypesRepeatableProcessor.kt @@ -0,0 +1,59 @@ +/* + * 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.processor + +import com.google.devtools.ksp.getAllSuperTypes +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSAnnotated +import com.intellij.util.containers.addIfNotNull + +/** + * This test asserts that [getAllSuperTypes] is stateless/referentially transparent, + * i.e. reusing the same sequence gives equal output: + * ``` + * val superTypes = ...getAllSuperTypes() + * superTypes.terminalOperation() == superTypes.terminalOperation() + * ``` + */ +class AllSuperTypesRepeatableProcessor(val classNames: List, override val enableNewFeatures: Boolean) : + AbstractTestProcessor() { + private val results: MutableList = mutableListOf() + + override fun toResult(): List { + return results + } + + override fun process(resolver: Resolver): List { + classNames.forEach { className -> + val superTypes = resolver.getClassDeclarationByName(className)?.getAllSuperTypes() ?: return@forEach + results.add("Original:") + superTypes.mapNotNull { it.declaration.qualifiedName?.asString() }.forEach(results::addIfNotNull) + + results.add("Copy:") + superTypes.mapNotNull { it.declaration.qualifiedName?.asString() }.forEach(results::addIfNotNull) + + // Apply terminal operation that does not consume the entire sequence. + superTypes.any { it.declaration.qualifiedName?.asString() == "L03b" } + + results.add("After Partial:") + superTypes.mapNotNull { it.declaration.qualifiedName?.asString() }.forEach(results::addIfNotNull) + } + return emptyList() + } +} 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 24cb284533..2d1049709c 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 @@ -615,6 +615,18 @@ abstract class KSPUnitTestSuite( runTest("$AA_PATH/jvmModifiersCompanionObject.kt") } + @TestMetadata("diamondInheritance.kt") + @Test + fun testDiamondInheritance() { + runTest("$AA_PATH/diamondInheritance.kt") + } + + @TestMetadata("diamondInheritanceRepeatable.kt") + @Test + fun testDiamondInheritanceRepeatable() { + runTest("$AA_PATH/diamondInheritanceRepeatable.kt") + } + @TestMetadata("lateinitProperties.kt") @Test fun testLateinitProperties() { diff --git a/kotlin-analysis-api/testData/diamondInheritance.kt b/kotlin-analysis-api/testData/diamondInheritance.kt new file mode 100644 index 0000000000..e1a4d3bb13 --- /dev/null +++ b/kotlin-analysis-api/testData/diamondInheritance.kt @@ -0,0 +1,119 @@ +/* + * 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. + */ + +// TEST PROCESSOR: AllSuperTypesProcessor +// PROCESSOR INPUT: Leaf +// EXPECTED: +// L30a +// L30b +// L29a +// L29b +// L28a +// L28b +// L27a +// L27b +// L26a +// L26b +// L25a +// L25b +// L24a +// L24b +// L23a +// L23b +// L22a +// L22b +// L21a +// L21b +// L20a +// L20b +// L19a +// L19b +// L18a +// L18b +// L17a +// L17b +// L16a +// L16b +// L15a +// L15b +// L14a +// L14b +// L13a +// L13b +// L12a +// L12b +// L11a +// L11b +// L10a +// L10b +// L09a +// L09b +// L08a +// L08b +// L07a +// L07b +// L06a +// L06b +// L05a +// L05b +// L04a +// L04b +// L03a +// L03b +// L02a +// L02b +// L01a +// L01b +// T00 +// kotlin.Any +// END + +// FILE: main.kt + +interface T00 +interface L01a : T00; interface L01b : T00 +interface L02a : L01a, L01b; interface L02b : L01a, L01b +interface L03a : L02a, L02b; interface L03b : L02a, L02b +interface L04a : L03a, L03b; interface L04b : L03a, L03b +interface L05a : L04a, L04b; interface L05b : L04a, L04b +interface L06a : L05a, L05b; interface L06b : L05a, L05b +interface L07a : L06a, L06b; interface L07b : L06a, L06b +interface L08a : L07a, L07b; interface L08b : L07a, L07b +interface L09a : L08a, L08b; interface L09b : L08a, L08b +interface L10a : L09a, L09b; interface L10b : L09a, L09b +interface L11a : L10a, L10b; interface L11b : L10a, L10b +interface L12a : L11a, L11b; interface L12b : L11a, L11b +interface L13a : L12a, L12b; interface L13b : L12a, L12b +interface L14a : L13a, L13b; interface L14b : L13a, L13b +interface L15a : L14a, L14b; interface L15b : L14a, L14b +interface L16a : L15a, L15b; interface L16b : L15a, L15b +interface L17a : L16a, L16b; interface L17b : L16a, L16b +interface L18a : L17a, L17b; interface L18b : L17a, L17b +interface L19a : L18a, L18b; interface L19b : L18a, L18b +interface L20a : L19a, L19b; interface L20b : L19a, L19b +interface L21a : L20a, L20b; interface L21b : L20a, L20b +interface L22a : L21a, L21b; interface L22b : L21a, L21b +interface L23a : L22a, L22b; interface L23b : L22a, L22b +interface L24a : L23a, L23b; interface L24b : L23a, L23b +interface L25a : L24a, L24b; interface L25b : L24a, L24b +interface L26a : L25a, L25b; interface L26b : L25a, L25b +interface L27a : L26a, L26b; interface L27b : L26a, L26b +interface L28a : L27a, L27b; interface L28b : L27a, L27b +interface L29a : L28a, L28b; interface L29b : L28a, L28b +interface L30a : L29a, L29b; interface L30b : L29a, L29b + +abstract class Leaf : L30a, L30b diff --git a/kotlin-analysis-api/testData/diamondInheritanceRepeatable.kt b/kotlin-analysis-api/testData/diamondInheritanceRepeatable.kt new file mode 100644 index 0000000000..30eda9d661 --- /dev/null +++ b/kotlin-analysis-api/testData/diamondInheritanceRepeatable.kt @@ -0,0 +1,64 @@ +/* + * 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. + */ + +// TEST PROCESSOR: AllSuperTypesRepeatableProcessor +// PROCESSOR INPUT: Leaf +// EXPECTED: +// Original: +// L04a +// L04b +// L03a +// L03b +// L02a +// L02b +// L01a +// L01b +// T00 +// kotlin.Any +// Copy: +// L04a +// L04b +// L03a +// L03b +// L02a +// L02b +// L01a +// L01b +// T00 +// kotlin.Any +// After Partial: +// L04a +// L04b +// L03a +// L03b +// L02a +// L02b +// L01a +// L01b +// T00 +// kotlin.Any +// END + +// FILE: main.kt + +interface T00 +interface L01a : T00; interface L01b : T00 +interface L02a : L01a, L01b; interface L02b : L01a, L01b +interface L03a : L02a, L02b; interface L03b : L02a, L02b +interface L04a : L03a, L03b; interface L04b : L03a, L03b + +abstract class Leaf : L04a, L04b