diff options
author | Jiaxiang Chen <jiaxiang@google.com> | 2022-02-01 16:49:31 -0800 |
---|---|---|
committer | laszio <ting-yuan@users.noreply.github.com> | 2022-02-06 22:28:03 -0800 |
commit | 0047c43e64f19dd9164d3af2f3aa05475719c163 (patch) | |
tree | 0fabf0b9f3178f9e249cd0f6cb0106482b3a9f4a /common-util | |
parent | d8792ed2c128350d94c58864691a0e88d6264124 (diff) | |
download | ksp-0047c43e64f19dd9164d3af2f3aa05475719c163.tar.gz |
pack util module into symbol-processing-cmdline artifact
Diffstat (limited to 'common-util')
5 files changed, 488 insertions, 0 deletions
diff --git a/common-util/build.gradle.kts b/common-util/build.gradle.kts new file mode 100644 index 00000000..72eccdea --- /dev/null +++ b/common-util/build.gradle.kts @@ -0,0 +1,39 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +description = "Kotlin Symbol Processing Util" + +val kotlinBaseVersion: String by project +val intellijVersion: String by project + +tasks.withType<KotlinCompile> { + kotlinOptions.freeCompilerArgs += "-Xjvm-default=compatibility" +} + +plugins { + kotlin("jvm") + id("org.jetbrains.intellij") version "0.6.4" + id("org.jetbrains.dokka") version ("1.4.32") +} + +intellij { + version = intellijVersion +} + +dependencies { + implementation(kotlin("stdlib", kotlinBaseVersion)) + implementation("org.jetbrains.kotlin:kotlin-compiler:$kotlinBaseVersion") + implementation(project(":api")) +} + +tasks { + val sourcesJar by creating(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets.main.get().allSource) + } +} + +val dokkaJavadocJar by tasks.register<Jar>("dokkaJavadocJar") { + dependsOn(tasks.dokkaJavadoc) + from(tasks.dokkaJavadoc.flatMap { it.outputDirectory }) + archiveClassifier.set("javadoc") +} diff --git a/common-util/src/main/kotlin/com/google/devtools/ksp/DescriptorUtils.kt b/common-util/src/main/kotlin/com/google/devtools/ksp/DescriptorUtils.kt new file mode 100644 index 00000000..d360d263 --- /dev/null +++ b/common-util/src/main/kotlin/com/google/devtools/ksp/DescriptorUtils.kt @@ -0,0 +1,222 @@ +package com.google.devtools.ksp + +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.Modifier +import com.google.devtools.ksp.symbol.Variance +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.MemberDescriptor +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.load.java.JavaDescriptorVisibilities +import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinaryClass +import org.jetbrains.kotlin.load.kotlin.KotlinJvmBinarySourceElement +import org.jetbrains.kotlin.load.kotlin.VirtualFileKotlinClass +import org.jetbrains.kotlin.load.kotlin.getContainingKotlinJvmBinaryClass +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.psi.KtDeclarationWithInitializer +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.resolve.descriptorUtil.isCompanionObject +import org.jetbrains.kotlin.resolve.source.KotlinSourceElement +import org.jetbrains.kotlin.resolve.source.getPsi +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedClassDescriptor +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedPropertyDescriptor +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import org.jetbrains.org.objectweb.asm.ClassReader +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.FieldVisitor +import org.jetbrains.org.objectweb.asm.MethodVisitor +import org.jetbrains.org.objectweb.asm.Opcodes + +fun MemberDescriptor.toKSModifiers(): Set<Modifier> { + val modifiers = mutableSetOf<Modifier>() + if (this.isActual) { + modifiers.add(Modifier.ACTUAL) + } + if (this.isExpect) { + modifiers.add(Modifier.EXPECT) + } + if (this.isExternal) { + modifiers.add(Modifier.EXTERNAL) + } + // we are not checking for JVM_STATIC annotation here intentionally + // see: https://github.com/google/ksp/issues/378 + val isStatic = (this.containingDeclaration as? ClassDescriptor)?.let { containingClass -> + containingClass.staticScope.getContributedDescriptors( + nameFilter = { + it == this.name + } + ).any { + it == this + } + } ?: false + if (isStatic) { + modifiers.add(Modifier.JAVA_STATIC) + } + when (this.modality) { + Modality.SEALED -> modifiers.add(Modifier.SEALED) + Modality.FINAL -> modifiers.add(Modifier.FINAL) + Modality.OPEN -> { + if (!isStatic && this.visibility != DescriptorVisibilities.PRIVATE) { + // private methods still show up as OPEN + modifiers.add(Modifier.OPEN) + } + } + Modality.ABSTRACT -> modifiers.add(Modifier.ABSTRACT) + } + when (this.visibility) { + DescriptorVisibilities.PUBLIC -> modifiers.add(Modifier.PUBLIC) + DescriptorVisibilities.PROTECTED, + JavaDescriptorVisibilities.PROTECTED_AND_PACKAGE, + JavaDescriptorVisibilities.PROTECTED_STATIC_VISIBILITY, + -> modifiers.add(Modifier.PROTECTED) + DescriptorVisibilities.PRIVATE, DescriptorVisibilities.LOCAL -> modifiers.add(Modifier.PRIVATE) + DescriptorVisibilities.INTERNAL -> modifiers.add(Modifier.INTERNAL) + // Since there is no modifier for package-private, use No modifier to tell if a symbol from binary is package private. + JavaDescriptorVisibilities.PACKAGE_VISIBILITY, JavaDescriptorVisibilities.PROTECTED_STATIC_VISIBILITY -> Unit + else -> throw IllegalStateException("unhandled visibility: ${this.visibility}") + } + + return modifiers +} + +fun FunctionDescriptor.toFunctionKSModifiers(): Set<Modifier> { + val modifiers = mutableSetOf<Modifier>() + if (this.isSuspend) { + modifiers.add(Modifier.SUSPEND) + } + if (this.isTailrec) { + modifiers.add(Modifier.TAILREC) + } + if (this.isInline) { + modifiers.add(Modifier.INLINE) + } + if (this.isInfix) { + modifiers.add(Modifier.INFIX) + } + if (this.isOperator) { + modifiers.add(Modifier.OPERATOR) + } + if (this.overriddenDescriptors.isNotEmpty()) { + modifiers.add(Modifier.OVERRIDE) + } + + return modifiers +} + +fun org.jetbrains.kotlin.types.Variance.toKSVariance(): Variance { + return when (this) { + org.jetbrains.kotlin.types.Variance.IN_VARIANCE -> Variance.CONTRAVARIANT + org.jetbrains.kotlin.types.Variance.OUT_VARIANCE -> Variance.COVARIANT + org.jetbrains.kotlin.types.Variance.INVARIANT -> Variance.INVARIANT + else -> throw IllegalStateException("Unexpected variance value $this, $ExceptionMessage") + } +} + +fun DeclarationDescriptor.findPsi(): PsiElement? { + // For synthetic members. + if ((this is CallableMemberDescriptor) && this.kind != CallableMemberDescriptor.Kind.DECLARATION) return null + return (this as? DeclarationDescriptorWithSource)?.source?.getPsi() +} + +/** + * Custom check for backing fields of descriptors that support properties coming from .class files. + * The compiler API always returns true for them even when they don't have backing fields. + */ +fun PropertyDescriptor.hasBackingFieldWithBinaryClassSupport(): Boolean { + // partially take from https://github.com/JetBrains/kotlin/blob/master/compiler/light-classes/src/org/jetbrains/kotlin/asJava/classes/ultraLightMembersCreator.kt#L104 + return when { + extensionReceiverParameter != null -> false // extension properties do not have backing fields + compileTimeInitializer != null -> true // compile time initialization requires backing field + isLateInit -> true // lateinit requires property, faster than parsing class declaration + modality == Modality.ABSTRACT -> false // abstract means false, faster than parsing class declaration + this is DeserializedPropertyDescriptor -> this.hasBackingFieldInBinaryClass() // kotlin class, check binary + this.source is KotlinSourceElement -> this.declaresDefaultValue // kotlin source + else -> true // Java source or class + } +} + +data class BinaryClassInfo( + val fieldAccFlags: Map<String, Int>, + val methodAccFlags: Map<String, Int> +) + +/** + * Lookup cache for field names names for deserialized classes. + * To check if a field has backing field, we need to look for binary field names, hence they are cached here. + */ +object BinaryClassInfoCache : KSObjectCache<ClassId, BinaryClassInfo>() { + fun getCached( + kotlinJvmBinaryClass: KotlinJvmBinaryClass, + ) = cache.getOrPut(kotlinJvmBinaryClass.classId) { + val virtualFileContent = kotlinJvmBinaryClass.safeAs<VirtualFileKotlinClass>()?.file?.contentsToByteArray() + val fieldAccFlags = mutableMapOf<String, Int>() + val methodAccFlags = mutableMapOf<String, Int>() + ClassReader(virtualFileContent).accept( + object : ClassVisitor(Opcodes.API_VERSION) { + override fun visitField( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + value: Any? + ): FieldVisitor? { + if (name != null) { + fieldAccFlags.put(name, access) + } + return null + } + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array<out String>? + ): MethodVisitor? { + if (name != null) { + methodAccFlags.put(name + descriptor, access) + } + return null + } + }, + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES + ) + BinaryClassInfo(fieldAccFlags, methodAccFlags) + } +} + +/** + * Workaround for backingField in deserialized descriptors. + * They always return non-null for backing field even when they don't have a backing field. + */ +private fun DeserializedPropertyDescriptor.hasBackingFieldInBinaryClass(): Boolean { + val kotlinJvmBinaryClass = if (containingDeclaration.isCompanionObject()) { + // Companion objects have backing fields in containing classes. + // https://kotlinlang.org/docs/java-to-kotlin-interop.html#static-fields + val container = containingDeclaration.containingDeclaration as? DeserializedClassDescriptor + container?.source?.safeAs<KotlinJvmBinarySourceElement>()?.binaryClass + } else { + this.getContainingKotlinJvmBinaryClass() + } ?: return false + return BinaryClassInfoCache.getCached(kotlinJvmBinaryClass).fieldAccFlags.containsKey(name.asString()) +} + +// from: https://github.com/JetBrains/kotlin/blob/92d200e093c693b3c06e53a39e0b0973b84c7ec5/plugins/kotlin-serialization/kotlin-serialization-compiler/src/org/jetbrains/kotlinx/serialization/compiler/resolve/SerializableProperty.kt#L45 +private val PropertyDescriptor.declaresDefaultValue: Boolean + get() = when (val declaration = this.source.getPsi()) { + is KtDeclarationWithInitializer -> declaration.initializer != null + is KtParameter -> declaration.defaultValue != null + else -> false + } + +fun KSAnnotated.hasAnnotation(fqn: String): Boolean = + annotations.any { + fqn.endsWith(it.shortName.asString()) && + it.annotationType.resolve().declaration.qualifiedName?.asString() == fqn + } diff --git a/common-util/src/main/kotlin/com/google/devtools/ksp/KSObjectCacheManager.kt b/common-util/src/main/kotlin/com/google/devtools/ksp/KSObjectCacheManager.kt new file mode 100644 index 00000000..08a8bd7f --- /dev/null +++ b/common-util/src/main/kotlin/com/google/devtools/ksp/KSObjectCacheManager.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2010-2020 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 + +class KSObjectCacheManager { + companion object { + val caches = arrayListOf<KSObjectCache<*, *>>() + + fun register(cache: KSObjectCache<*, *>) = caches.add(cache) + fun clear() = caches.forEach { it.clear() } + } +} + +abstract class KSObjectCache<K, V> { + val cache = mutableMapOf<K, V>() + + init { + KSObjectCacheManager.register(this) + } + + open fun clear() = cache.clear() +} diff --git a/common-util/src/main/kotlin/com/google/devtools/ksp/MemoizedSequence.kt b/common-util/src/main/kotlin/com/google/devtools/ksp/MemoizedSequence.kt new file mode 100644 index 00000000..49bfcecc --- /dev/null +++ b/common-util/src/main/kotlin/com/google/devtools/ksp/MemoizedSequence.kt @@ -0,0 +1,31 @@ +package com.google.devtools.ksp + +// TODO: garbage collect underlying sequence after exhaust. +class MemoizedSequence<T>(sequence: Sequence<T>) : Sequence<T> { + + private val cache = mutableListOf<T>() + + private val iter: Iterator<T> by lazy { + sequence.iterator() + } + + private inner class CachedIterator(val iterCache: Iterator<T>, var buffer: Int) : Iterator<T> { + override fun hasNext(): Boolean { + return buffer > 0 || iter.hasNext() + } + + override fun next(): T { + if (buffer > 0) { + buffer -- + return iterCache.next() + } + val value = iter.next() + cache.add(value) + return value + } + } + + override fun iterator(): Iterator<T> { + return CachedIterator(cache.iterator(), cache.size) + } +} diff --git a/common-util/src/main/kotlin/com/google/devtools/ksp/PsiUtils.kt b/common-util/src/main/kotlin/com/google/devtools/ksp/PsiUtils.kt new file mode 100644 index 00000000..34d5c2ae --- /dev/null +++ b/common-util/src/main/kotlin/com/google/devtools/ksp/PsiUtils.kt @@ -0,0 +1,159 @@ +package com.google.devtools.ksp + +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.Modifier +import com.intellij.lang.jvm.JvmModifier +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiModifierListOwner +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtEnumEntry +import org.jetbrains.kotlin.psi.KtModifierList +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.jetbrains.kotlin.psi.psiUtil.siblings + +val jvmModifierMap = mapOf( + JvmModifier.PUBLIC to Modifier.PUBLIC, + JvmModifier.PRIVATE to Modifier.PRIVATE, + JvmModifier.ABSTRACT to Modifier.ABSTRACT, + JvmModifier.FINAL to Modifier.FINAL, + JvmModifier.PROTECTED to Modifier.PROTECTED, + JvmModifier.STATIC to Modifier.JAVA_STATIC, + JvmModifier.STRICTFP to Modifier.JAVA_STRICT, + JvmModifier.NATIVE to Modifier.JAVA_NATIVE, + JvmModifier.SYNCHRONIZED to Modifier.JAVA_SYNCHRONIZED, + JvmModifier.TRANSIENT to Modifier.JAVA_TRANSIENT, + JvmModifier.VOLATILE to Modifier.JAVA_VOLATILE +) + +val javaModifiers = setOf( + Modifier.ABSTRACT, + Modifier.FINAL, + Modifier.JAVA_DEFAULT, + Modifier.JAVA_NATIVE, + Modifier.JAVA_STATIC, + Modifier.JAVA_STRICT, + Modifier.JAVA_SYNCHRONIZED, + Modifier.JAVA_TRANSIENT, + Modifier.JAVA_VOLATILE, + Modifier.PRIVATE, + Modifier.PROTECTED, + Modifier.PUBLIC, +) + +val modifierMap = mapOf( + KtTokens.PUBLIC_KEYWORD to Modifier.PUBLIC, + KtTokens.PRIVATE_KEYWORD to Modifier.PRIVATE, + KtTokens.INTERNAL_KEYWORD to Modifier.INTERNAL, + KtTokens.PROTECTED_KEYWORD to Modifier.PROTECTED, + KtTokens.IN_KEYWORD to Modifier.IN, + KtTokens.OUT_KEYWORD to Modifier.OUT, + KtTokens.OVERRIDE_KEYWORD to Modifier.OVERRIDE, + KtTokens.LATEINIT_KEYWORD to Modifier.LATEINIT, + KtTokens.ENUM_KEYWORD to Modifier.ENUM, + KtTokens.SEALED_KEYWORD to Modifier.SEALED, + KtTokens.ANNOTATION_KEYWORD to Modifier.ANNOTATION, + KtTokens.DATA_KEYWORD to Modifier.DATA, + KtTokens.INNER_KEYWORD to Modifier.INNER, + KtTokens.FUN_KEYWORD to Modifier.FUN, + KtTokens.VALUE_KEYWORD to Modifier.VALUE, + KtTokens.SUSPEND_KEYWORD to Modifier.SUSPEND, + KtTokens.TAILREC_KEYWORD to Modifier.TAILREC, + KtTokens.OPERATOR_KEYWORD to Modifier.OPERATOR, + KtTokens.INFIX_KEYWORD to Modifier.INFIX, + KtTokens.INLINE_KEYWORD to Modifier.INLINE, + KtTokens.EXTERNAL_KEYWORD to Modifier.EXTERNAL, + KtTokens.ABSTRACT_KEYWORD to Modifier.ABSTRACT, + KtTokens.FINAL_KEYWORD to Modifier.FINAL, + KtTokens.OPEN_KEYWORD to Modifier.OPEN, + KtTokens.VARARG_KEYWORD to Modifier.VARARG, + KtTokens.NOINLINE_KEYWORD to Modifier.NOINLINE, + KtTokens.CROSSINLINE_KEYWORD to Modifier.CROSSINLINE, + KtTokens.REIFIED_KEYWORD to Modifier.REIFIED, + KtTokens.EXPECT_KEYWORD to Modifier.EXPECT, + KtTokens.ACTUAL_KEYWORD to Modifier.ACTUAL, + KtTokens.CONST_KEYWORD to Modifier.CONST +) + +fun KtModifierList?.toKSModifiers(): Set<Modifier> { + if (this == null) + return emptySet() + val modifiers = mutableSetOf<Modifier>() + modifiers.addAll( + modifierMap.entries + .filter { hasModifier(it.key) } + .map { it.value } + ) + return modifiers +} + +fun KtModifierListOwner.toKSModifiers(): Set<Modifier> { + val modifierList = this.modifierList + return modifierList.toKSModifiers() +} + +fun PsiModifierListOwner.toKSModifiers(): Set<Modifier> { + val modifiers = mutableSetOf<Modifier>() + modifiers.addAll( + jvmModifierMap.entries.filter { this.hasModifier(it.key) } + .map { it.value } + .toSet() + ) + if (this.modifierList?.hasExplicitModifier("default") == true) { + modifiers.add(Modifier.JAVA_DEFAULT) + } + return modifiers +} + +fun Project.findLocationString(file: PsiFile, offset: Int): String { + val psiDocumentManager = PsiDocumentManager.getInstance(this) + val document = psiDocumentManager.getDocument(file) ?: return "<unknown>" + val lineNumber = document.getLineNumber(offset) + val offsetInLine = offset - document.getLineStartOffset(lineNumber) + return "${file.virtualFile.path}: (${lineNumber + 1}, ${offsetInLine + 1})" +} + +private fun parseDocString(raw: String): String? { + val t1 = raw.trim() + if (!t1.startsWith("/**") || !t1.endsWith("*/")) + return null + val lineSep = t1.findAnyOf(listOf("\r\n", "\n", "\r"))?.second ?: "" + return t1.trim('/').trim('*').lines().joinToString(lineSep) { + it.trimStart().trimStart('*') + } +} + +fun PsiElement.getDocString(): String? = + this.firstChild.siblings().firstOrNull { it is PsiComment }?.let { + parseDocString(it.text) + } + +fun KtClassOrObject.getClassType(): ClassKind { + return when (this) { + is KtObjectDeclaration -> ClassKind.OBJECT + is KtEnumEntry -> ClassKind.ENUM_ENTRY + is KtClass -> when { + this.isEnum() -> ClassKind.ENUM_CLASS + this.isInterface() -> ClassKind.INTERFACE + this.isAnnotation() -> ClassKind.ANNOTATION_CLASS + else -> ClassKind.CLASS + } + else -> throw IllegalStateException("Unexpected psi type ${this.javaClass}, $ExceptionMessage") + } +} + +inline fun <reified T> PsiElement.findParentOfType(): T? { + var parent = this.parent + while (parent != null && parent !is T) { + parent = parent.parent + } + return parent as? T +} + +fun <T> Sequence<T>.memoized() = MemoizedSequence(this) |