JetBrains-Research / reflekt

A plugin for Kotlin compiler for compile-time reflection
Apache License 2.0
363 stars 11 forks source link

Suggestion: Use Gradle dependency attributes to mark libraries that Reflekt will introspect #104

Open aSemy opened 2 years ago

aSemy commented 2 years ago

Hi, I saw this note in the source code about marking which dependencies should be introspected.

https://github.com/JetBrains-Research/reflekt/blob/4e07ffd7897d2aabd5e6de878e4382ce3706b832/gradle-plugin/src/main/kotlin/org/jetbrains/reflekt/plugin/ReflektSubPlugin.kt#L121

I think a better way would be to use Gradle variant attributes.

The end result would, to users, look something like this:

    implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.6.10")  {
        reflekt(introspect = true)
    }

And in the plugin, it would require less config, and leverage Gradle task-avoidance more (faster, less unncessary work).

The steps would be

  1. Create a custom Attribute https://docs.gradle.org/current/userguide/variant_attributes.html#sec:declaring_attributes
  2. Create a helper extension function, to apply the attribute to dependencies
  3. Create a Configuration for gathering the dependencies https://docs.gradle.org/current/userguide/cross_project_publications.html#sec:variant-aware-sharing
  4. Update ReflektSubPlugin.kt to resolve the location of the libraries using the Configuration

Create a custom attribute

// ReflektSubPlugin.kt
    companion object {
        val REFLEKT_INTROSPECTION_ATTRIBUTE = Attribute.of("org.jetbrains.reflekt.introspect", Boolean::class.javaObjectType)
    }

Manually this attribute could be applied

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.6.10")  {
        attributes {
            attributes.attribute(Attribute.of("org.jetbrains.reflekt.introspect", Boolean::class.javaObjectType), true)
        }
    }
}

Create 'apply introspect Attribute' extension

But that's a little clunky. I think an extension function, in reflekt-gradle-plugin, like this would work

// ReflektGradleExtension.kt
/** Designate this dependency as one Reflekt will analyse. */
fun ModuleDependency.reflekt(introspect: Boolean = true) {
    attributes.attribute(REFLEKT_INTROSPECTION_ATTRIBUTE, introspect)
}

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.6.10")  {
        reflekt(introspect = true)
    }
}

Create a Configuration

Now Reflekt can create its own bucket of dependencies, called reflektIntrospect.

    // RefelktSubPlugin.kt
    companion object {
        const val REFLEKT_INTROSPECT_CONFIGURATION_NAME = "reflektIntrospect"
        val REFLEKT_INTROSPECTION_ATTRIBUTE = Attribute.of("org.jetbrains.reflekt.introspect", Boolean::class.javaObjectType)
    }

    private fun Project.createReflektIntrospectConfiguration(): Configuration {
        return configurations.create(REFLEKT_INTROSPECT_CONFIGURATION_NAME).apply {
            description = "These dependencies will be introspected by Reflekt. "

            // mark this as an 'inbound' collector. It is not a provider of dependencies.
            isCanBeResolved = true
            isCanBeConsumed = false

            // filter to only include dependencies that are explicitly marked
            attributes.attribute(REFLEKT_INTROSPECTION_ATTRIBUTE, true)
            isTransitive = false

            // extending from compileClasspath means this 'reflekt' configuration will also receive 
            // any dependencies that 'compileClasspath' receives.
            // https://docs.gradle.org/current/userguide/java_plugin.html#tab:configurations
            extendsFrom(configurations[JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME])
        }
    }

This configuration will be available for customisation in any project where the Reflekt Gradle Plugin is applied, so users can customise it. (For example, including testCompileClasspath, or files from a local directory.)

Create the Configuration in ReflektSubPlugin

I think that this new configuration should be applied ASAP, and creating it in override fun applyToCompilation(...) might be too late? But I'm not sure.

// ReflektSubPlugin.kt
    override fun apply(target: Project) {
        target.createReflektIntrospectConfiguration()
    }

Resolve the Configuration in ReflektSubPlugin

And then in applyToCompilation the configuration can be resolved. This will trigger Gradle tasks - so it's best to either use the correct Gradle methods that will return and map providers...

// get a provider for the config
val introspectConfigurationProvider: NamedDomainObjectProvider<Configuration> =
            project.configurations.named(REFLEKT_INTROSPECT_CONFIGURATION_NAME)

val librariesToIntrospect: Provider<List<SubpluginOption>> =
    introspectConfigurationProvider
        .map { introspectLibs: Configuration ->
            introspectLibs
                .incoming
                .artifactView { it.lenient(true) }
                .files
                .map { lib: File ->
                    SubpluginOption(key = LIBRARY_TO_INTROSPECT.name, value = lib.canonicalPath)
                }
        }
// (note: Provider.map is different to List<>.map)

Or do all the work inside a provider

return project.provider {

            val introspectConfiguration: Configuration = project.configurations[REFLEKT_INTROSPECT_CONFIGURATION_NAME]
            val librariesToIntrospect = introspectConfiguration.incoming
                .artifactView { it.lenient(true) }
                .files
                .map { lib: File ->
                    SubpluginOption(key = LIBRARY_TO_INTROSPECT.name, value = lib.canonicalPath)
                }

            librariesToIntrospect + reflektMetaFilesOptions + dependencyJars +
                SubpluginOption(key = ENABLED_OPTION_INFO.name, value = extension.enabled.toString()) +
                SubpluginOption(key = OUTPUT_DIR_OPTION_INFO.name, value = generationPath) +
                SubpluginOption(key = SAVE_METADATA_OPTION_INFO.name, value = extension.toSaveMetadata.toString()) +
                SubpluginOption(key = REFLEKT_META_FILE_PATH.name, value = createReflektMeta(project.getResourcesPath()).absolutePath)
        }