Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scan Kotlin constants for JavaPsiFacade #1138

Merged
merged 1 commit into from Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -31,8 +31,14 @@ import com.google.devtools.ksp.processing.impl.NativePlatformInfoImpl
import com.google.devtools.ksp.processing.impl.ResolverImpl
import com.google.devtools.ksp.processing.impl.UnknownPlatformInfoImpl
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclarationContainer
import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSVisitorVoid
import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Origin
import com.google.devtools.ksp.symbol.Visibility
import com.google.devtools.ksp.symbol.impl.java.KSFileJavaImpl
import com.google.devtools.ksp.symbol.impl.kotlin.KSFileImpl
import com.intellij.openapi.project.Project
Expand Down Expand Up @@ -198,6 +204,42 @@ abstract class AbstractKotlinSymbolProcessingExtension(
newFiles, deferredSymbols, bindingTrace, project, componentProvider, incrementalContext, options
)

if (!initialized) {
// Visit constants so that JavaPsiFacade knows them.
// The annotation visitor in ResolverImpl covered newFiles already.
// Skip private and local members, which are not visible to Java files.
ksFiles.filterIsInstance<KSFileImpl>().filter { it !in dirtyFiles }.forEach {
try {
it.accept(
object : KSVisitorVoid() {
private fun visitDeclarationContainer(container: KSDeclarationContainer) {
container.declarations.filterNot {
it.getVisibility() == Visibility.PRIVATE
}.forEach {
it.accept(this, Unit)
}
}

override fun visitFile(file: KSFile, data: Unit) =
visitDeclarationContainer(file)

override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) =
visitDeclarationContainer(classDeclaration)

override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: Unit) {
if (property.modifiers.contains(Modifier.CONST)) {
property.getter // force resolution
}
}
},
Unit
)
} catch (_: Exception) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the case for exception here and why it is simply ignored for the general Exception rather than more specific exception types?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's similar to the initialization blocks protected by handleException in the above. Exceptions can arise from, e.g., broken inputs. This is the best effort to make constants in Kotlin available to JavaPsiFacade and try not to error out if possible. It's up to the processors to decide whether to error out or not.

// Do nothing.
}
}
}

val providers = loadProviders()
if (!initialized) {
codeGenerator = CodeGeneratorImpl(
Expand Down
@@ -0,0 +1,42 @@
package com.google.devtools.ksp.test

import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Assert
import org.junit.Assume
import org.junit.Rule
import org.junit.Test
import java.io.File

class KotlinConstsInJavaIT {
@Rule
@JvmField
val project: TemporaryTestProject = TemporaryTestProject("kotlin-consts-in-java")

private fun GradleRunner.buildAndCheck(vararg args: String, extraCheck: (BuildResult) -> Unit = {}) =
buildAndCheckOutcome(*args, outcome = TaskOutcome.SUCCESS, extraCheck = extraCheck)

private fun GradleRunner.buildAndCheckOutcome(
vararg args: String,
outcome: TaskOutcome,
extraCheck: (BuildResult) -> Unit = {}
) {
val result = this.withArguments(*args).build()

Assert.assertEquals(outcome, result.task(":workload:kspKotlin")?.outcome)

extraCheck(result)
}

@Test
fun testKotlinConstsInJava() {
// FIXME: `clean` fails to delete files on windows.
Assume.assumeFalse(System.getProperty("os.name").startsWith("Windows", ignoreCase = true))
val gradleRunner = GradleRunner.create().withProjectDir(project.root).withDebug(true)
gradleRunner.buildAndCheck(":workload:kspKotlin")

File(project.root, "workload/src/main/java/com/example/JavaClass.java").appendText("\n")
gradleRunner.buildAndCheck(":workload:kspKotlin")
}
}
@@ -0,0 +1,8 @@
plugins {
kotlin("jvm")
}

repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap/")
}
@@ -0,0 +1,19 @@
pluginManagement {
val kotlinVersion: String by settings
val kspVersion: String by settings
val testRepo: String by settings
plugins {
id("com.google.devtools.ksp") version kspVersion
kotlin("jvm") version kotlinVersion
}
repositories {
maven(testRepo)
gradlePluginPortal()
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap/")
}
}

rootProject.name = "playground"

include(":workload")
include(":test-processor")
@@ -0,0 +1,24 @@
val kspVersion: String by project
val testRepo: String by project

plugins {
kotlin("jvm")
}

group = "com.example"
version = "1.0-SNAPSHOT"

repositories {
maven(testRepo)
mavenCentral()
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap/")
}

dependencies {
implementation(kotlin("stdlib"))
implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion")
}

sourceSets.main {
java.srcDirs("src/main/kotlin")
}
@@ -0,0 +1,27 @@
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSFunctionDeclaration

class TestProcessor(
private val codeGenerator: CodeGenerator,
private val options: Map<String, String>,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
resolver
.getSymbolsWithAnnotation("com.example.ann.MyAnn")
.filterIsInstance<KSFunctionDeclaration>()
.forEach { func ->
val arg = func.annotations.first().arguments.first().value.toString()
if (!arg.startsWith("REPLACE"))
throw IllegalStateException(arg)
}

return emptyList()
}
}

class TestProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
TestProcessor(environment.codeGenerator, environment.options, environment.logger)
}
@@ -0,0 +1 @@
TestProcessorProvider
@@ -0,0 +1,20 @@
val testRepo: String by project

plugins {
id("com.google.devtools.ksp")
kotlin("jvm")
}

version = "1.0-SNAPSHOT"

repositories {
maven(testRepo)
mavenCentral()
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap/")
}

dependencies {
implementation(kotlin("stdlib"))
implementation(project(":test-processor"))
ksp(project(":test-processor"))
}
@@ -0,0 +1,9 @@
package com.example;

import com.example.ann.MyAnn;

public class JavaClass {
@MyAnn(KotlinConsts.ACTION)
public void f() {
}
}
@@ -0,0 +1,8 @@
package com.example

class KotlinConsts {
companion object {
const val ACTION = "REPLACE"
const val ACTION2 = "REPLACE"
}
}
@@ -0,0 +1,3 @@
package com.example.ann

annotation class MyAnn(val value: String)