From abd75270d469ea4f214c6127c055c9819172a3bd Mon Sep 17 00:00:00 2001 From: Nicklas Ansman Date: Thu, 29 Aug 2024 11:09:33 -0400 Subject: [PATCH] Implement support for reading resource files in a symbol processor --- .../devtools/ksp/processing/Resolver.kt | 4 + api/src/main/resources/foo.txt | 0 .../com/google/devtools/ksp/KSPConfig.kt | 14 ++++ .../devtools/ksp/common/ResourceUtils.kt | 28 +++++++ .../ksp/KotlinSymbolProcessingExtension.kt | 13 +++- .../com/google/devtools/ksp/KspOptions.kt | 12 +++ .../ksp/processing/impl/ResolverImpl.kt | 48 +++++++++--- .../devtools/ksp/gradle/KotlinFactories.kt | 11 ++- .../google/devtools/ksp/gradle/KspAATask.kt | 22 ++++++ .../devtools/ksp/gradle/KspSubplugin.kt | 49 ++++++++---- .../google/devtools/ksp/test/ResourcesIT.kt | 74 +++++++++++++++++++ .../test/resources/resources/build.gradle.kts | 8 ++ .../resources/resources/settings.gradle.kts | 19 +++++ .../resources/test-processor/build.gradle.kts | 25 +++++++ .../src/main/kotlin/TestProcessor.kt | 19 +++++ ...ols.ksp.processing.SymbolProcessorProvider | 1 + .../resources/workload/build.gradle.kts | 20 +++++ .../workload/src/main/resources/foo.txt | 1 + .../src/main/resources/foo/bar/baz/quux | 1 + .../ksp/impl/KotlinSymbolProcessing.kt | 2 + .../devtools/ksp/impl/ResolverAAImpl.kt | 19 +++-- .../devtools/ksp/test/AbstractKSPAATest.kt | 16 ++++ 22 files changed, 368 insertions(+), 38 deletions(-) create mode 100644 api/src/main/resources/foo.txt create mode 100644 common-util/src/main/kotlin/com/google/devtools/ksp/common/ResourceUtils.kt create mode 100644 integration-tests/src/test/kotlin/com/google/devtools/ksp/test/ResourcesIT.kt create mode 100644 integration-tests/src/test/resources/resources/build.gradle.kts create mode 100644 integration-tests/src/test/resources/resources/settings.gradle.kts create mode 100644 integration-tests/src/test/resources/resources/test-processor/build.gradle.kts create mode 100644 integration-tests/src/test/resources/resources/test-processor/src/main/kotlin/TestProcessor.kt create mode 100644 integration-tests/src/test/resources/resources/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider create mode 100644 integration-tests/src/test/resources/resources/workload/build.gradle.kts create mode 100644 integration-tests/src/test/resources/resources/workload/src/main/resources/foo.txt create mode 100644 integration-tests/src/test/resources/resources/workload/src/main/resources/foo/bar/baz/quux diff --git a/api/src/main/kotlin/com/google/devtools/ksp/processing/Resolver.kt b/api/src/main/kotlin/com/google/devtools/ksp/processing/Resolver.kt index cdc3d899c7..529807d16e 100644 --- a/api/src/main/kotlin/com/google/devtools/ksp/processing/Resolver.kt +++ b/api/src/main/kotlin/com/google/devtools/ksp/processing/Resolver.kt @@ -18,6 +18,7 @@ package com.google.devtools.ksp.processing import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.symbol.* +import java.io.InputStream /** * [Resolver] provides [SymbolProcessor] with access to compiler details such as Symbols. @@ -37,6 +38,9 @@ interface Resolver { */ fun getAllFiles(): Sequence + fun getResource(path: String): InputStream? + fun getAllResources(): Sequence + /** * Get all symbols with specified annotation. * Note that in multiple round processing, only symbols from deferred symbols of last round and symbols from newly generated files will be returned in this function. diff --git a/api/src/main/resources/foo.txt b/api/src/main/resources/foo.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt b/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt index dee5e69efd..24ba94d49b 100644 --- a/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt +++ b/common-deps/src/main/kotlin/com/google/devtools/ksp/KSPConfig.kt @@ -9,6 +9,7 @@ abstract class KSPConfig( val moduleName: String, val sourceRoots: List, val commonSourceRoots: List, + val resourceRoots: List, val libraries: List, val processorOptions: Map, @@ -36,6 +37,7 @@ abstract class KSPConfig( abstract class Builder { lateinit var moduleName: String lateinit var sourceRoots: List + lateinit var resourceRoots: List var commonSourceRoots: List = emptyList() var libraries: List = emptyList() @@ -72,6 +74,7 @@ class KSPJvmConfig( moduleName: String, sourceRoots: List, commonSourceRoots: List, + resourceRoots: List, libraries: List, processorOptions: Map, @@ -99,6 +102,7 @@ class KSPJvmConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, @@ -142,6 +146,7 @@ class KSPJvmConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, @@ -174,6 +179,7 @@ class KSPNativeConfig( moduleName: String, sourceRoots: List, commonSourceRoots: List, + resourceRoots: List, libraries: List, processorOptions: Map, @@ -201,6 +207,7 @@ class KSPNativeConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, @@ -235,6 +242,7 @@ class KSPNativeConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, @@ -267,6 +275,7 @@ class KSPJsConfig( moduleName: String, sourceRoots: List, commonSourceRoots: List, + resourceRoots: List, libraries: List, processorOptions: Map, @@ -294,6 +303,7 @@ class KSPJsConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, @@ -328,6 +338,7 @@ class KSPJsConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, @@ -365,6 +376,7 @@ class KSPCommonConfig( moduleName: String, sourceRoots: List, commonSourceRoots: List, + resourceRoots: List, libraries: List, processorOptions: Map, @@ -392,6 +404,7 @@ class KSPCommonConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, @@ -426,6 +439,7 @@ class KSPCommonConfig( moduleName, sourceRoots, commonSourceRoots, + resourceRoots, libraries, processorOptions, diff --git a/common-util/src/main/kotlin/com/google/devtools/ksp/common/ResourceUtils.kt b/common-util/src/main/kotlin/com/google/devtools/ksp/common/ResourceUtils.kt new file mode 100644 index 0000000000..a0713799bc --- /dev/null +++ b/common-util/src/main/kotlin/com/google/devtools/ksp/common/ResourceUtils.kt @@ -0,0 +1,28 @@ +package com.google.devtools.ksp.common + +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.InputStream + +fun Iterable.getResource(path: String): InputStream? = firstNotNullOfOrNull { root -> + val file = root.resolve(path) + try { + if (file.toPath().normalize().startsWith(root.toPath().normalize())) { + FileInputStream(file) + } else { + null + } + } catch (e: FileNotFoundException) { + null + } +} + +fun Iterable.getAllResources(): Sequence = + asSequence().flatMap { root -> + println(root) + root + .walkTopDown() + .filter { it.isFile } + .map { it.toRelativeString(root) } + } diff --git a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KotlinSymbolProcessingExtension.kt b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KotlinSymbolProcessingExtension.kt index 266c9aa720..5370d543f3 100644 --- a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KotlinSymbolProcessingExtension.kt +++ b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KotlinSymbolProcessingExtension.kt @@ -219,11 +219,18 @@ abstract class AbstractKotlinSymbolProcessingExtension( // dirtyFiles cannot be reused because they are created in the old container. val resolver = ResolverImpl( - module, - ksFiles.filterNot { + module = module, + allKSFiles = ksFiles.filterNot { it.filePath in cleanFilenames }, - newFiles, deferredSymbols, bindingTrace, project, componentProvider, incrementalContext, options + newKSFiles = newFiles, + resourceRoots = options.resourceRoots, + deferredSymbols = deferredSymbols, + bindingTrace = bindingTrace, + project = project, + componentProvider = componentProvider, + incrementalContext = incrementalContext, + options = options ) if (!initialized) { diff --git a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt index 6c92c960bb..265b027188 100644 --- a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt +++ b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/KspOptions.kt @@ -27,6 +27,7 @@ class KspOptions( val projectBaseDir: File, val compileClasspath: List, val javaSourceRoots: List, + val resourceRoots: List, val classOutputDir: File, val javaOutputDir: File, @@ -66,6 +67,7 @@ class KspOptions( var projectBaseDir: File? = null val compileClasspath: MutableList = mutableListOf() val javaSourceRoots: MutableList = mutableListOf() + val resourceRoots: MutableList = mutableListOf() var classOutputDir: File? = null var javaOutputDir: File? = null @@ -103,6 +105,7 @@ class KspOptions( requireNotNull(projectBaseDir) { "A non-null projectBaseDir must be provided" }, compileClasspath, javaSourceRoots, + resourceRoots, requireNotNull(classOutputDir) { "A non-null classOutputDir must be provided" }, requireNotNull(javaOutputDir) { "A non-null javaOutputDir must be provided" }, requireNotNull(kotlinOutputDir) { "A non-null kotlinOutputDir must be provided" }, @@ -174,6 +177,14 @@ enum class KspCliOption( false ), + RESOURCE_ROOT_OPTION( + "resourceRoot", + "", + "a root directory for resources", + false, + allowMultipleOccurrences = true + ), + RESOURCE_OUTPUT_DIR_OPTION( "resourceOutputDir", "", @@ -322,6 +333,7 @@ fun KspOptions.Builder.processOption(option: KspCliOption, value: String) = when KspCliOption.CLASS_OUTPUT_DIR_OPTION -> classOutputDir = File(value) KspCliOption.JAVA_OUTPUT_DIR_OPTION -> javaOutputDir = File(value) KspCliOption.KOTLIN_OUTPUT_DIR_OPTION -> kotlinOutputDir = File(value) + KspCliOption.RESOURCE_ROOT_OPTION -> resourceRoots.add(File(value)) KspCliOption.RESOURCE_OUTPUT_DIR_OPTION -> resourceOutputDir = File(value) KspCliOption.CACHES_DIR_OPTION -> cachesDir = File(value) KspCliOption.KSP_OUTPUT_DIR_OPTION -> kspOutputDir = File(value) diff --git a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt index 9a24d49b66..bb4d5a8f33 100644 --- a/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt +++ b/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/processing/impl/ResolverImpl.kt @@ -18,22 +18,13 @@ package com.google.devtools.ksp.processing.impl import com.google.devtools.ksp.* -import com.google.devtools.ksp.common.JVM_DEFAULT_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_DEFAULT_WITHOUT_COMPATIBILITY_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_STATIC_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_STRICTFP_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_SYNCHRONIZED_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_TRANSIENT_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_VOLATILE_ANNOTATION_FQN -import com.google.devtools.ksp.common.extractThrowsAnnotation +import com.google.devtools.ksp.common.* import com.google.devtools.ksp.common.impl.KSNameImpl import com.google.devtools.ksp.common.impl.KSTypeReferenceSyntheticImpl import com.google.devtools.ksp.common.impl.RefPosition import com.google.devtools.ksp.common.impl.findOuterMostRef import com.google.devtools.ksp.common.impl.findRefPosition import com.google.devtools.ksp.common.impl.isReturnTypeOfAnnotationMethod -import com.google.devtools.ksp.common.javaModifiers -import com.google.devtools.ksp.common.memoized import com.google.devtools.ksp.common.visitor.CollectAnnotatedSymbolsVisitor import com.google.devtools.ksp.processing.KSBuiltIns import com.google.devtools.ksp.processing.Resolver @@ -114,12 +105,14 @@ import org.jetbrains.kotlin.types.typeUtil.supertypes import org.jetbrains.kotlin.util.containingNonLocalDeclaration import org.jetbrains.org.objectweb.asm.Opcodes import java.io.File +import java.io.InputStream import java.util.* class ResolverImpl( val module: ModuleDescriptor, val allKSFiles: Collection, val newKSFiles: Collection, + val resourceRoots: Collection, private val deferredSymbols: Map>, val bindingTrace: BindingTrace, val project: Project, @@ -233,6 +226,10 @@ class ResolverImpl( return allKSFiles.asSequence() } + override fun getResource(path: String): InputStream? = resourceRoots.getResource(path) + + override fun getAllResources(): Sequence = resourceRoots.getAllResources() + override fun getClassDeclarationByName(name: KSName): KSClassDeclaration? { nameToKSMap[name]?.let { return it } @@ -364,9 +361,11 @@ class ResolverImpl( else -> throw IllegalStateException("Unexpected descriptor type for declaration: $declaration") } } + is KSPropertyDeclaration -> resolvePropertyDeclaration(declaration)?.let { typeMapper.mapFieldSignature(it.type, it) ?: typeMapper.mapType(it).descriptor } + else -> null } @@ -426,6 +425,7 @@ class ResolverImpl( is KSPropertyDeclaration -> resolvePropertyDeclaration(original) is KSFunctionDeclaration -> (resolveFunctionDeclaration(original) as? FunctionDescriptor)?.propertyIfAccessor + else -> return false } @@ -447,6 +447,7 @@ class ResolverImpl( is KSPropertyDeclaration -> containingClass.getAllProperties().singleOrNull { it.simpleName.asString() == overrider.simpleName.asString() && isOriginal(overrider, it) }?.let { overrides(it, overridee) } ?: false + is KSFunctionDeclaration -> { val candidates = containingClass.getAllFunctions().filter { it.simpleName.asString() == overridee.simpleName.asString() @@ -467,6 +468,7 @@ class ResolverImpl( candidates.singleOrNull { isOriginal(overrider, it) }?.let { overrides(it, overridee) } ?: false } } + else -> false } } @@ -554,6 +556,7 @@ class ResolverImpl( ) } } + is PsiField -> { moduleClassResolver .resolveClass(JavaFieldImpl(psi).containingClass) @@ -563,6 +566,7 @@ class ResolverImpl( filter = { it.correspondsTo(psi) } ) } + else -> throw IllegalStateException("unhandled psi element kind: ${psi.javaClass}") } } @@ -593,6 +597,7 @@ class ResolverImpl( descriptor } } + is KSConstructorSyntheticImpl -> { // we might create synthetic constructor when it is not declared in code // it is either for kotlin, where we can use primary constructor, or for java @@ -600,6 +605,7 @@ class ResolverImpl( val resolved = resolveClassDeclaration(function.ksClassDeclaration) resolved?.unsubstitutedPrimaryConstructor ?: resolved?.constructors?.singleOrNull() } + else -> throw IllegalStateException("unexpected class: ${function.javaClass}") } as? CallableDescriptor } @@ -652,6 +658,7 @@ class ResolverImpl( if (e.psi.isConstructor) JavaConstructorImpl(e.psi) else JavaMethodImpl(e.psi) ) } + is KSClassDeclarationJavaImpl -> { resolverContext = resolverContext .childForClassOrPackage(resolveJavaDeclaration(e.psi) as ClassDescriptor, JavaClassImpl(e.psi)) @@ -685,12 +692,14 @@ class ResolverImpl( is PsiPrimitiveType -> { getClassDeclarationByName(psiType.boxedTypeName!!)!!.asStarProjectedType() } + is PsiArrayType -> { val componentType = resolveJavaTypeInAnnotations(psiType.componentType) val componentTypeRef = createKSTypeReferenceFromKSType(componentType) val typeArgs = listOf(getTypeArgument(componentTypeRef, Variance.INVARIANT)) builtIns.arrayType.replace(typeArgs) } + else -> { getClassDeclarationByName(psiType.canonicalText)?.asStarProjectedType() ?: KSErrorType(psiType.canonicalText) @@ -736,9 +745,11 @@ class ResolverImpl( getKSTypeCached(it, type.element.typeArguments, type.annotations) } } + is KSTypeReferenceDescriptorImpl -> { return getKSTypeCached(type.kotlinType) } + is KSTypeReferenceJavaImpl -> { val psi = (type.psi as? PsiClassReferenceType)?.resolve() if (psi is PsiTypeParameter) { @@ -783,6 +794,7 @@ class ResolverImpl( } } } + else -> throw IllegalStateException("Unable to resolve type for $type, $ExceptionMessage") } } @@ -809,6 +821,7 @@ class ResolverImpl( } else { KSTypeParameterDescriptorImpl.getCached(descriptor) } + is TypeAliasDescriptor -> KSTypeAliasDescriptorImpl.getCached(descriptor) null -> throw IllegalStateException("Failed to resolve descriptor for $kotlinType") else -> throw IllegalStateException( @@ -928,9 +941,11 @@ class ResolverImpl( psi.throwsList.referencedTypes.asSequence() .map { KSTypeReferenceJavaImpl.getCached(it, function).resolve() } } + Origin.KOTLIN -> { extractThrowsAnnotation(function) } + Origin.KOTLIN_LIB, Origin.JAVA_LIB -> { val descriptor = (function as KSFunctionDeclarationDescriptorImpl).descriptor val jvmDesc = this.mapToJvmSignature(function) @@ -948,6 +963,7 @@ class ResolverImpl( } extractThrowsFromClassFile(virtualFileContent, jvmDesc, function.simpleName.asString()) } + else -> emptySequence() } } @@ -958,6 +974,7 @@ class ResolverImpl( Origin.KOTLIN, Origin.SYNTHETIC -> { extractThrowsAnnotation(accessor) } + Origin.KOTLIN_LIB -> { val descriptor = (accessor as KSPropertyAccessorDescriptorImpl).descriptor val jvmDesc = typeMapper.mapAsmMethod(descriptor).descriptor @@ -975,6 +992,7 @@ class ResolverImpl( } extractThrowsFromClassFile(virtualFileContent, jvmDesc, getJvmName(accessor)) } + else -> emptySequence() } } @@ -1191,6 +1209,7 @@ class ResolverImpl( if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.INTERFACE) modifiers.add(Modifier.ABSTRACT) } + Origin.KOTLIN -> { addVisibilityModifiers() if (!declaration.isOpen()) @@ -1216,16 +1235,19 @@ class ResolverImpl( if (declaration.classKind == ClassKind.INTERFACE) modifiers.add(Modifier.ABSTRACT) } + is KSPropertyDeclaration -> { if (declaration.isAbstract()) modifiers.add(Modifier.ABSTRACT) } + is KSFunctionDeclaration -> { if (declaration.isAbstract) modifiers.add(Modifier.ABSTRACT) } } } + Origin.KOTLIN_LIB, Origin.JAVA_LIB -> { when (declaration) { is KSPropertyDeclaration -> { @@ -1234,6 +1256,7 @@ class ResolverImpl( if (declaration.jvmAccessFlag and Opcodes.ACC_VOLATILE != 0) modifiers.add(Modifier.JAVA_VOLATILE) } + is KSFunctionDeclaration -> { if (declaration.jvmAccessFlag and Opcodes.ACC_STRICT != 0) modifiers.add(Modifier.JAVA_STRICT) @@ -1242,6 +1265,7 @@ class ResolverImpl( } } } + else -> Unit } return modifiers @@ -1363,6 +1387,7 @@ class ResolverImpl( RefPosition.PARAMETER_TYPE -> typeSystem.getOptimalModeForValueParameter(kotlinType) RefPosition.RETURN_TYPE -> typeSystem.getOptimalModeForReturnType(kotlinType, ref.isReturnTypeOfAnnotationMethod()) + RefPosition.SUPER_TYPE -> TypeMappingMode.SUPER_TYPE }.updateFromParents(ref) @@ -1530,9 +1555,11 @@ internal fun KSAnnotated.findAnnotationFromUseSiteTarget(): Sequence (this.receiver as? KSDeclarationImpl)?.let { it.originalAnnotations.asSequence().filter { it.useSiteTarget == AnnotationUseSiteTarget.GET } } + is KSPropertySetter -> (this.receiver as? KSDeclarationImpl)?.let { it.originalAnnotations.asSequence().filter { it.useSiteTarget == AnnotationUseSiteTarget.SET } } + is KSValueParameter -> { var parent = when (this) { is KSValueParameterSyntheticImpl -> this.owner @@ -1556,6 +1583,7 @@ internal fun KSAnnotated.findAnnotationFromUseSiteTarget(): Sequence emptySequence() } ?: emptySequence() } diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinFactories.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinFactories.kt index d5bf52edcc..964bbdbd65 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinFactories.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KotlinFactories.kt @@ -20,6 +20,7 @@ package com.google.devtools.ksp.gradle import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.file.Directory import org.gradle.api.file.FileCollection import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty @@ -187,6 +188,12 @@ interface KspTask : Task { @get:Internal val incrementalChangesTransformers: ListProperty<(SourcesChanges) -> List> + + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + val resourceRoots: ListProperty } @CacheableTask @@ -227,7 +234,9 @@ abstract class KspTaskJvm @Inject constructor( super.callCompilerAsync(args, inputChanges, taskOutputsBackup) } - override fun skipCondition(): Boolean = sources.isEmpty && javaSources.isEmpty + override fun skipCondition(): Boolean = sources.isEmpty && + javaSources.isEmpty && + resourceRoots.get().all { r -> r.asFileTree.isEmpty } @get:InputFiles @get:SkipWhenEmpty diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt index b9cc427e74..2e193769d7 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspAATask.kt @@ -22,7 +22,9 @@ import com.google.devtools.ksp.processing.* import org.gradle.api.DefaultTask import org.gradle.api.artifacts.Configuration import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory import org.gradle.api.logging.LogLevel +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty @@ -152,6 +154,7 @@ abstract class KspAATask @Inject constructor( cfg.moduleName.value(project.name) val kotlinOutputDir = KspGradleSubplugin.getKspKotlinOutputDir(project, sourceSetName, target) val javaOutputDir = KspGradleSubplugin.getKspJavaOutputDir(project, sourceSetName, target) + val resourceOutputDir = KspGradleSubplugin.getKspResourceOutputDir(project, sourceSetName, target) val filteredTasks = kspExtension.excludedSources.buildDependencies.getDependencies(null).map { it.name } kotlinCompilation.allKotlinSourceSetsObservable.forAll { sourceSet -> @@ -164,9 +167,21 @@ abstract class KspAATask @Inject constructor( } cfg.sourceRoots.from(filtered) cfg.javaSourceRoots.from(filtered) + cfg.resourceRoots.addAll( + sourceSet.resources.srcDirs + .filter { + !resourceOutputDir.isParentOf(it) && it !in kspExtension.excludedSources + } + .map { + project.layout.dir(project.provider { it }).get() + } + ) kspAATask.dependsOn( sourceSet.kotlin.nonSelfDeps(kspTaskName).filter { it.name !in filteredTasks } ) + kspAATask.dependsOn( + sourceSet.resources.nonSelfDeps(kspTaskName).filter { it.name !in filteredTasks } + ) } if (kotlinCompilation is KotlinCommonCompilation) { cfg.commonSourceRoots.from(kotlinCompilation.defaultSourceSet.kotlin) @@ -305,6 +320,12 @@ abstract class KspGradleConfig @Inject constructor() { @get:PathSensitive(PathSensitivity.RELATIVE) abstract val javaSourceRoots: ConfigurableFileCollection + @get:InputFiles + @get:SkipWhenEmpty + @get:IgnoreEmptyDirectories + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val resourceRoots: ListProperty + @get:Incremental @get:Classpath abstract val libraries: ConfigurableFileCollection @@ -441,6 +462,7 @@ abstract class KspAAWorkerAction : WorkAction { moduleName = gradleCfg.moduleName.get() sourceRoots = gradleCfg.sourceRoots.files.toList() commonSourceRoots = gradleCfg.commonSourceRoots.files.toList() + resourceRoots = gradleCfg.resourceRoots.get().map { it.asFile } libraries = gradleCfg.libraries.files.toList() projectBaseDir = gradleCfg.projectBaseDir.get() outputBaseDir = gradleCfg.outputBaseDir.get() diff --git a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt index 6a8560b431..5ad4e2abf0 100644 --- a/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt +++ b/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspSubplugin.kt @@ -103,7 +103,7 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool private fun getSubpluginOptions( project: Project, kspExtension: KspExtension, - sourceSetName: String, + sourceSet: KotlinSourceSet, target: String, isIncremental: Boolean, allWarningsAsErrors: Provider, @@ -112,25 +112,38 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool ): Provider> { val options = project.objects.listProperty(SubpluginOption::class.java) options.add( - InternalSubpluginOption("classOutputDir", getKspClassOutputDir(project, sourceSetName, target).path) + InternalSubpluginOption("classOutputDir", getKspClassOutputDir(project, sourceSet.name, target).path) ) options.add( - InternalSubpluginOption("javaOutputDir", getKspJavaOutputDir(project, sourceSetName, target).path) + InternalSubpluginOption("javaOutputDir", getKspJavaOutputDir(project, sourceSet.name, target).path) ) options.add( - InternalSubpluginOption("kotlinOutputDir", getKspKotlinOutputDir(project, sourceSetName, target).path) + InternalSubpluginOption("kotlinOutputDir", getKspKotlinOutputDir(project, sourceSet.name, target).path) + ) + val resourceOutputDir = getKspResourceOutputDir(project, sourceSet.name, target) + options.addAll( + project.provider { + sourceSet.resources.srcDirs + .filter { !resourceOutputDir.isParentOf(it) && it !in kspExtension.excludedSources } + .map { resource -> + InternalSubpluginOption( + "resourceRoot", + resource.path + ) + } + } ) options.add( InternalSubpluginOption( "resourceOutputDir", - getKspResourceOutputDir(project, sourceSetName, target).path + resourceOutputDir.path ) ) options.add( InternalSubpluginOption("cachesDir", getKspCachesDir(project, sourceSetName, target).path) ) options.add( - InternalSubpluginOption("kspOutputDir", getKspOutputDir(project, sourceSetName, target).path) + InternalSubpluginOption("kspOutputDir", getKspOutputDir(project, sourceSet.name, target).path) ) options.add( SubpluginOption("incremental", isIncremental.toString()) @@ -245,12 +258,12 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool } val target = kotlinCompilation.target.name - val sourceSetName = kotlinCompilation.defaultSourceSet.name - val classOutputDir = getKspClassOutputDir(project, sourceSetName, target) - val javaOutputDir = getKspJavaOutputDir(project, sourceSetName, target) - val kotlinOutputDir = getKspKotlinOutputDir(project, sourceSetName, target) - val resourceOutputDir = getKspResourceOutputDir(project, sourceSetName, target) - val kspOutputDir = getKspOutputDir(project, sourceSetName, target) + val sourceSet = kotlinCompilation.defaultSourceSet + val classOutputDir = getKspClassOutputDir(project, sourceSet.name, target) + val javaOutputDir = getKspJavaOutputDir(project, sourceSet.name, target) + val kotlinOutputDir = getKspKotlinOutputDir(project, sourceSet.name, target) + val resourceOutputDir = getKspResourceOutputDir(project, sourceSet.name, target) + val kspOutputDir = getKspOutputDir(project, sourceSet.name, target) val kspClasspathCfg = project.configurations.maybeCreate( KSP_PLUGIN_CLASSPATH_CONFIGURATION_NAME @@ -296,12 +309,20 @@ class KspGradleSubplugin @Inject internal constructor(private val registry: Tool // depends on the processor; if the processor changes, it needs to be reprocessed. kspTask.dependsOn(processorClasspath.buildDependencies) kspTask.commandLineArgumentProviders.addAll(kspExtension.commandLineArgumentProviders) - + kspTask.resourceRoots.addAll( + sourceSet.resources.srcDirs + .filter { + !resourceOutputDir.isParentOf(it) && it !in kspExtension.excludedSources + } + .map { + project.layout.dir(project.provider { it }).get() + } + ) kspTask.options.addAll( getSubpluginOptions( project = project, kspExtension = kspExtension, - sourceSetName = sourceSetName, + sourceSet = sourceSet, target = target, isIncremental = isIncremental, allWarningsAsErrors = project.provider { kspExtension.allWarningsAsErrors }, diff --git a/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/ResourcesIT.kt b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/ResourcesIT.kt new file mode 100644 index 0000000000..cbab6b826a --- /dev/null +++ b/integration-tests/src/test/kotlin/com/google/devtools/ksp/test/ResourcesIT.kt @@ -0,0 +1,74 @@ +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.jetbrains.kotlin.incremental.deleteRecursivelyOrThrow +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class ResourcesIT(val useKSP2: Boolean) { + @Rule + @JvmField + val project: TemporaryTestProject = TemporaryTestProject("resources", useKSP2 = useKSP2) + + @Test + fun testResources() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + gradleRunner.withArguments(":workload:assemble").build().let { result -> + result.assertOutputContains("foo.txt: Hello") + result.assertOutputContains("foo/bar/baz/quux: World") + } + } + + @Test + fun testUpToDate() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + gradleRunner.withArguments(":workload:assemble").build().let { result -> + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:kspKotlin")?.outcome) + } + gradleRunner.withArguments(":workload:assemble").build().let { result -> + Assert.assertEquals(TaskOutcome.UP_TO_DATE, result.task(":workload:kspKotlin")?.outcome) + } + } + + @Test + fun skipsWhenEmpty() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + project.root.resolve("workload/src/main/resources").deleteRecursivelyOrThrow() + gradleRunner.withArguments(":workload:assemble").build().let { result -> + Assert.assertEquals(TaskOutcome.NO_SOURCE, result.task(":workload:kspKotlin")?.outcome) + } + } + + @Test + fun incremental() { + val gradleRunner = GradleRunner.create().withProjectDir(project.root) + gradleRunner.withArguments(":workload:assemble").build().let { result -> + result.assertOutputContains("foo.txt: Hello") + result.assertOutputContains("foo/bar/baz/quux: World") + } + project.root.resolve("workload/src/main/resources/foo.txt").writeText("Goodbye") + gradleRunner.withArguments(":workload:assemble").build().let { result -> + result.assertOutputContains("foo.txt: Goodbye") + result.assertOutputContains("foo/bar/baz/quux: World") + } + } + + private fun BuildResult.assertOutputContains(expected: String) { + Assert.assertTrue( + "Expected output to contain $expected but did not. Output:\n$output", + expected in output + ) + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "KSP2={0}") + fun params() = listOf(arrayOf(true), arrayOf(false)) + } +} diff --git a/integration-tests/src/test/resources/resources/build.gradle.kts b/integration-tests/src/test/resources/resources/build.gradle.kts new file mode 100644 index 0000000000..c5737a2e0d --- /dev/null +++ b/integration-tests/src/test/resources/resources/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + kotlin("jvm") +} + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap/") +} diff --git a/integration-tests/src/test/resources/resources/settings.gradle.kts b/integration-tests/src/test/resources/resources/settings.gradle.kts new file mode 100644 index 0000000000..15a6495b80 --- /dev/null +++ b/integration-tests/src/test/resources/resources/settings.gradle.kts @@ -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 = "resources" + +include(":workload") +include(":test-processor") diff --git a/integration-tests/src/test/resources/resources/test-processor/build.gradle.kts b/integration-tests/src/test/resources/resources/test-processor/build.gradle.kts new file mode 100644 index 0000000000..75c6c7c5ab --- /dev/null +++ b/integration-tests/src/test/resources/resources/test-processor/build.gradle.kts @@ -0,0 +1,25 @@ +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.squareup:javapoet:1.12.1") + implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion") +} + +sourceSets.main { + java.srcDirs("src/main/kotlin") +} diff --git a/integration-tests/src/test/resources/resources/test-processor/src/main/kotlin/TestProcessor.kt b/integration-tests/src/test/resources/resources/test-processor/src/main/kotlin/TestProcessor.kt new file mode 100644 index 0000000000..cad47f65ff --- /dev/null +++ b/integration-tests/src/test/resources/resources/test-processor/src/main/kotlin/TestProcessor.kt @@ -0,0 +1,19 @@ +import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.symbol.* + +class TestProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger +): SymbolProcessor { + override fun process(resolver: Resolver): List { + logger.warn("Processing resources") + for (resource in resolver.getAllResources()) { + logger.warn("$resource: ${resolver.getResource(resource)!!.bufferedReader().use { it.readText() }}") + } + return emptyList() + } +} + +class TestProcessorProvider : SymbolProcessorProvider { + override fun create(env: SymbolProcessorEnvironment): SymbolProcessor = TestProcessor(env.codeGenerator, env.logger) +} diff --git a/integration-tests/src/test/resources/resources/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/integration-tests/src/test/resources/resources/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..c91e3e9e0b --- /dev/null +++ b/integration-tests/src/test/resources/resources/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +TestProcessorProvider diff --git a/integration-tests/src/test/resources/resources/workload/build.gradle.kts b/integration-tests/src/test/resources/resources/workload/build.gradle.kts new file mode 100644 index 0000000000..d79ac26e5d --- /dev/null +++ b/integration-tests/src/test/resources/resources/workload/build.gradle.kts @@ -0,0 +1,20 @@ +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool + +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 { + ksp(project(":test-processor")) +} diff --git a/integration-tests/src/test/resources/resources/workload/src/main/resources/foo.txt b/integration-tests/src/test/resources/resources/workload/src/main/resources/foo.txt new file mode 100644 index 0000000000..5ab2f8a432 --- /dev/null +++ b/integration-tests/src/test/resources/resources/workload/src/main/resources/foo.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/integration-tests/src/test/resources/resources/workload/src/main/resources/foo/bar/baz/quux b/integration-tests/src/test/resources/resources/workload/src/main/resources/foo/bar/baz/quux new file mode 100644 index 0000000000..beef906c3e --- /dev/null +++ b/integration-tests/src/test/resources/resources/workload/src/main/resources/foo/bar/baz/quux @@ -0,0 +1 @@ +World \ No newline at end of file diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt index 3b4aad2dca..7fdf4a9b09 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/KotlinSymbolProcessing.kt @@ -493,6 +493,7 @@ class KotlinSymbolProcessing( var allDirtyKSFiles = incrementalContext.calcDirtyFiles(allKSFiles).toList() var newKSFiles = allDirtyKSFiles val initialDirtySet = allDirtyKSFiles.toSet() + val resourceRoots = kspConfig.resourceRoots val targetPlatform = ResolverAAImpl.ktModule.platform val symbolProcessorEnvironment = SymbolProcessorEnvironment( @@ -527,6 +528,7 @@ class KotlinSymbolProcessing( val resolver = ResolverAAImpl( allDirtyKSFiles, newKSFiles, + resourceRoots, deferredSymbols, project, incrementalContext, diff --git a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt index 66f24b5c9e..f392b3aaeb 100644 --- a/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt +++ b/kotlin-analysis-api/src/main/kotlin/com/google/devtools/ksp/impl/ResolverAAImpl.kt @@ -19,25 +19,17 @@ package com.google.devtools.ksp.impl import com.google.devtools.ksp.* -import com.google.devtools.ksp.common.JVM_DEFAULT_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_DEFAULT_WITHOUT_COMPATIBILITY_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_STATIC_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_STRICTFP_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_SYNCHRONIZED_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_TRANSIENT_ANNOTATION_FQN -import com.google.devtools.ksp.common.JVM_VOLATILE_ANNOTATION_FQN -import com.google.devtools.ksp.common.extractThrowsAnnotation +import com.google.devtools.ksp.common.* import com.google.devtools.ksp.common.impl.KSNameImpl import com.google.devtools.ksp.common.impl.KSTypeReferenceSyntheticImpl import com.google.devtools.ksp.common.impl.RefPosition import com.google.devtools.ksp.common.impl.findOuterMostRef import com.google.devtools.ksp.common.impl.findRefPosition import com.google.devtools.ksp.common.impl.isReturnTypeOfAnnotationMethod -import com.google.devtools.ksp.common.javaModifiers -import com.google.devtools.ksp.common.memoized import com.google.devtools.ksp.common.visitor.CollectAnnotatedSymbolsVisitor import com.google.devtools.ksp.impl.symbol.java.KSAnnotationJavaImpl import com.google.devtools.ksp.impl.symbol.kotlin.* +import com.google.devtools.ksp.impl.symbol.kotlin.findParentOfType import com.google.devtools.ksp.impl.symbol.util.BinaryClassInfoCache import com.google.devtools.ksp.impl.symbol.util.DeclarationOrdering import com.google.devtools.ksp.impl.symbol.util.extractThrowsFromClassFile @@ -80,11 +72,14 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.org.objectweb.asm.Opcodes +import java.io.File +import java.io.InputStream @OptIn(KspExperimental::class) class ResolverAAImpl( val allKSFiles: List, val newKSFiles: List, + val resourceRoots: List, val deferredSymbols: Map>, val project: Project, val incrementalContext: IncrementalContextAA, @@ -293,6 +288,10 @@ class ResolverAAImpl( return allKSFiles.asSequence() } + override fun getResource(path: String): InputStream? = resourceRoots.getResource(path) + + override fun getAllResources(): Sequence = resourceRoots.getAllResources() + override fun getClassDeclarationByName(name: KSName): KSClassDeclaration? { fun findClass(name: KSName): KtSymbol? { if (name.asString() == "") { diff --git a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt index 13ba744c49..1395d2fa82 100644 --- a/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt +++ b/kotlin-analysis-api/src/test/kotlin/com/google/devtools/ksp/test/AbstractKSPAATest.kt @@ -46,6 +46,9 @@ abstract class AbstractKSPAATest : AbstractKSPTest(FrontendKinds.FIR) { val TestModule.kotlinSrc get() = File(testRoot, "kotlinSrc") + val TestModule.resources + get() = File(testRoot, "resources") + fun TestModule.writeKtFiles() { kotlinSrc.mkdirs() files.filter { it.isKtFile }.forEach { file -> @@ -56,6 +59,16 @@ abstract class AbstractKSPAATest : AbstractKSPTest(FrontendKinds.FIR) { } } + fun TestModule.writeResourceFiles() { + resources.mkdirs() + files.filter { it.isAdditional }.forEach { file -> + File(resources, file.relativePath).let { + it.parentFile.mkdirs() + it.writeText(file.originalContent) + } + } + } + private fun compileKotlin(dependencies: List, sourcesPath: String, javaSourcePath: String, outDir: File) { val classpath = mutableListOf() classpath.addAll(dependencies.map { it.canonicalPath }) @@ -84,6 +97,7 @@ abstract class AbstractKSPAATest : AbstractKSPTest(FrontendKinds.FIR) { override fun compileModule(module: TestModule, testServices: TestServices) { module.writeKtFiles() + module.writeResourceFiles() val javaFiles = module.writeJavaFiles() val dependencies = module.allDependencies.map { outDirForModule(it.moduleName) } compileKotlin(dependencies, module.kotlinSrc.path, module.javaDir.path, module.outDir) @@ -111,6 +125,7 @@ abstract class AbstractKSPAATest : AbstractKSPTest(FrontendKinds.FIR) { // Therefore, this doesn't work: // val ktFiles = mainModule.loadKtFiles(kotlinCoreEnvironment.project) mainModule.writeKtFiles() + mainModule.writeResourceFiles() val testRoot = mainModule.testRoot @@ -118,6 +133,7 @@ abstract class AbstractKSPAATest : AbstractKSPTest(FrontendKinds.FIR) { moduleName = mainModule.name sourceRoots = listOf(mainModule.kotlinSrc) javaSourceRoots = compilerConfiguration.javaSourceRoots.map { File(it) }.toList() + resourceRoots = listOf(mainModule.resources) jdkHome = compilerConfiguration.get(JVMConfigurationKeys.JDK_HOME) jvmTarget = compilerConfiguration.get(JVMConfigurationKeys.JVM_TARGET)!!.description languageVersion = compilerConfiguration.languageVersionSettings.languageVersion.versionString