Giter Club home page Giter Club logo

reflekt's Introduction

JetBrains Research Gradle Build Run deteKT Run diKTat

Reflekt

Reflekt is a compile-time reflection library that leverages the flows of the standard reflection approach and can find classes, objects (singleton classes) or functions by some conditions in compile-time.

Instead of relying on JVM reflection, Reflekt performs compile-time resolution of reflection queries using Kotlin compiler analysis, providing a convenient reflection API without actually using reflection.

Reflekt is a joint project of JetBrains Research and the Kotless team. The main reason for its creation was the necessity of GraalVM support in modern Java applications, especially on Serverless workloads. With the help of the Reflekt project, Kotless will be able to provide access to GraalVM to users of historically reflection-based frameworks such as Spring or their own Kotless DSL.

We have implemented two approaches - searching classes\objects or functions via a limited DSL and by custom user condition via an extended DSL. The first one will be called Reflekt, and the second SmartReflekt.

Restrictions. Reflekt analyses only .kt files (in the project and in the libraries); uses Kotlin 1.7.0. Reflekt does not currently support incremental compilation.

Note, we use Intermediate Representation of code in this plugin. It means, that Reflekt can be used for all available platforms: JVM, Native and JavaScript.


Table of contents

Getting started

Note, currently we support the following Reflekt and Kotlin versions: 1.7.0 (only for local usage), 1.5.30, 1.5.21, 1.5.20, 1.5.10, 1.5.0

Reflekt uses Gradle. If you have a Gradle project, you only need to do three things.

Firstly, set up the Reflekt plugin. You need to apply the plugin. In the build.gradle.kts file, add the following lines in the plugins section:

plugins {
    // Version of Kotlin should be 1.5.0+ that supports IR backend
    kotlin("jvm") version "1.7.0"

    // Please, use the same version with the Kotlin version in your project
    id("org.jetbrains.reflekt") version "1.7.0"

    // Necessary only for this example, for Kotless library
    id("io.kotless") version "0.1.6"
}

At the same time, add to the settings.gradle.kts file the following snippet:

pluginManagement {
    repositories {
        //add the dependency to Reflekt Maven repository
        maven(url = uri("https://packages.jetbrains.team/maven/p/reflekt/reflekt"))

        // Necessary only for this example, for Kotless library
        maven(url = uri("https://plugins.gradle.org/m2/"))
    }
}

Secondly, add the Reflekt DSL as a library to your application. In the build.gradle.kts file, add the following lines in the dependencies section:

dependencies {
    // The version here and the version in the plugins sections should be equal
    implementation("org.jetbrains.reflekt", "reflekt-dsl", "1.7.0")

    // Necessary for this example
    compileOnly("io.kotless", "kotless-lang", "0.1.6")
}

At the same time, add the following lines in the repositories section:

repositories {
    //... Any other repositories
    // add Reflekt repository for libraries resolving
    maven(url = uri("https://packages.jetbrains.team/maven/p/reflekt/reflekt"))
}

Thirdly, customize the Reflekt plugin. In the build.gradle.kts file, add the reflekt object:

reflekt {
    // Enable or disable Reflekt plugin
    enabled = true
}

Please note that the Reflekt can also analyze the files from the external libraries. Reflekt can handle only libraries from the dependencies section and DependencyHandlers of them should be canBeResolve = True, e.g.:

val reflektConfiguration by configurations.creating {
    isCanBeResolved = true
}

configurations["implementation"].extendsFrom(reflektConfiguration)

Also, this library should contain a special file ReflektMeta. The last point means the library should use Reflekt with the following configuration:

reflekt {
    // Enable or disable Reflekt plugin
    enabled = true
    // Create ReflektMeta file
    toSaveMetadata = true
}

To avoid some bugs and enable IR, please add the following compilation settings for Java and Kotlin in the build.gradle.kts file:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

kotlin.jvmToolchain(11)

tasks.withType<KotlinCompile> {
    kotlinOptions {
        // Current Reflekt version does not support incremental compilation process
        incremental = false
    }
}

Note: Please note that the current version of Reflekt and SmartReflekt does not support incremental compilation process

This gives you access to the limited Reflekt DSL interfaces.

This gives you access to the extended SmartReflekt DSL, which allow filtering classes/objects\functions by user condition.

Now you can use the Reflekt plugin to find objects, classes, and functions in your project:

val objects = Reflekt.objects().withSupertype<AInterface>()
    .withAnnotations<AInterface>(FirstAnnotation::class, SecondAnnotation::class).toList()

val classes = Reflekt.classes().withSupertype<BInterface>().toSet()

val functions = Reflekt.functions().withAnnotations<() -> Unit>().toList()

And the SmartReflekt plugin:

val objects = SmartReflekt.objects<AInterface>().filter { TODO("some user's condition") }.resolve()

val classes = SmartReflekt.classes<BInterface>().filter { TODO("some user's condition") }.resolve()

val functions =
    SmartReflekt.functions<() -> Unit>().filter { TODO("some user's condition") }.toList()

Local start

You can use any unpublished Reflekt version. You should do the following steps:

  • Clone the Reflekt project (the official repo, any fork, branch, etc.).
  • Build the project ./gradlew build
  • Publish the project to maven local ./gradlew publishToMavenLocal
  • Add mavenLocal() in the repositories section in the build.gradle.kts file in your project:
repositories {
    mavenLocal()
}

Please note that if you build a Reflekt version with a customized version number, write this version in the plugins and dependencies sections.

Supported features

  • Compile-time reflection by Reflekt DSL for multi-module projects:
    • project's files
    • external libraries
  • Compile-time reflection by custom users' filters for multi-module projects by SmartReflekt DSL
    • project's files
    • external libraries
  • Bytecode generation -> IR generation
  • Incremental compilation process
  • Search in all modules of the project
  • Code generation.

Note: We analyze modules independently of each other. If an object\class\function is in module A, and you run Reflekt in module B, then the object\class\function will not be found. You can find this example in the examples folder.

Examples

Any explanation becomes much better with a proper example.

In the repository's examples folder, you can find an example project that uses the Reflekt plugin by Reflekt DSL and by SmartReflekt DSL.

You can also find many examples of searching algorithm work in the test folder.

By default, the examples project uses Reflekt from the local maven repository. If you would like to use a released version, please, uncomment the corresponding lines in the setting.gradle.kts and build.gradle.kts files in the examples project.

Want to know more?

The Reflekt code itself is widely documented, and you can take a look into its interfaces to get to know Reflekt better.

You may ask questions and participate in discussions on repository issues.

Contribution

Please be sure to review project's contributing guidelines to learn how to help the project.

reflekt's People

Contributors

akuleshov7 avatar asemy avatar commandertvis avatar elena-lyulina avatar elenaerratic avatar isomethane avatar nbirillo avatar tanvd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

reflekt's Issues

Filter all instances (classes and objects) according to their type in the SmartReflekt

For two cases:

    val smartClasses = SmartReflekt.classes<BInterface>().filter { it.isData() }.resolve()
    println(smartClasses)

    val smartObjects = SmartReflekt.objects<BInterface>().filter { it.isCompanion() }.resolve()
    println(smartObjects)

we got this error:
class io.reflekt.example.TestFunctions$Companion cannot be cast to class io.reflekt.example.BInterface

But we should not consider this class

Consider replacing `KClass` with custom information class as DSL result

The idea is that WithX.toList should return not a KClass, which assumes further runtime reflection, but an information class storing all the information about the class available to Reflekt plus the old KClass reference.

This is particularly useful to implement #71, because actually KClass has the sealedSubclasses property that does that thing; however, this data can be collected at compile time, and it's helpful in cases where run-time reflection isn't available or too slow.

Rough API:

class ReflektClass<T> {
  val kotlin: KClass<T>
  val qualifiledName: String
  val simpleName: String
  val sealedSubclasses: List<ReflektClass<*>>
  val isOpen: Boolean
  ...
}

Possible usage:

Reflekt.classes().withSuperType<B1>().toList().map { /* it: ReflektClass<B1> */ it.qualifiedName }.forEach { println(it) }

Also, to not bloat the ReflektImpl file, there must be a registry of such classes to have them available in many DSL calls simultaneously (basically, val classData: HashMap<String, ReflektClass<*>>).

Update and correct Gradle config

Hi, I've been looking at using this project and I see that the Gradle config could be improved. I like fixing Gradle, so I'm happy to make a PR.

Here's a non-complete list:

  1. remove unused or unrequired plugins & dependencies
  • com.github.gmazzo.buildconfig, unused

I'll look into making some additional improvements, like using the Kover for code coverage.

Support the plugin in Intellij IDEA

Create an Intellij IDEA plugin to make it easier to use the Reflekt plugin: add an inspection for searching the Reflekt and SmartReflekt calls and describe what of the part will be replaced at the compile time.

The main reason for it: it can be difficult for users to understand clearly what of the part from the query will be run at the run-time in the composite queries, e.g.:

SmartReflekt.functions<() -> Boolean>().filter { it.isTopLevel && it.name == "fooBoolean" }.resolve().onEach { it() }
        .map { it.toString() }.toSet()

This example will be replaced into something like this:

listOf(list_of_functions).onEach { it() }.map { it.toString() }.toSet()

And the last part will be executed at the run-time

Readme instructions

Couldn't run readme example with kotless dependency due to the error "Could not resolve io.kotless:kotless-dsl:0.1.6"

A problem occurred configuring root project 'reflekt-example'.
> Failed to notify project evaluation listener.
   > Could not resolve all files for configuration ':compileClasspath'.
      > Could not find io.kotless:kotless-dsl:0.1.6.
        Searched in the following locations:
          - https://repo.maven.apache.org/maven2/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
          - https://jcenter.bintray.com/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
          - https://plugins.gradle.org/m2/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
          - https://packages.jetbrains.team/maven/p/reflekt/reflekt/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
        Required by:
            project :
   > Could not resolve all files for configuration ':compileClasspath'.
      > Could not find io.kotless:kotless-dsl:0.1.6.
        Searched in the following locations:
          - https://repo.maven.apache.org/maven2/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
          - https://jcenter.bintray.com/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
          - https://plugins.gradle.org/m2/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
          - https://packages.jetbrains.team/maven/p/reflekt/reflekt/io/kotless/kotless-dsl/0.1.6/kotless-dsl-0.1.6.pom
        Required by:
            project :

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

Return entities with their annotations

Currently, Reflekt searches for entities (classes/objects/functions) be their annotations and returns as a list or a set.

Sometimes, e.g. in Kotless, users want to use not only the lists with results, but use the information from annotations. In this case, we can find entities, but to get the annotations from them we should use run-time reflection.

Thus, we should add an able of storing not only the list/set of the entities but and additional info (from the query params)

Support incremental compilation

Currently, Reflekt does not support the incremental compilation process. We have to get all compiled instances per module and merge old results with new ones

Java support

Support search entities in Java files from the project and the libraries. We can use special JavaDescriptors to search the entities (only for Reflekt part, since for SmartReflekt we have to adapt all Kotlin-specific properties to Java)

In this case, we also can support the usage of the Reflekt queries in Java files, but we can not replace IR since we don't have IR in Java. To keep our approach multiplatform, we can use ReflektImpl approach -- generate ReflektImpl implementation not only for the libraries but and for the current project (only for Java queries)

Test failures due to mismatched line endings, `\r\n` vs `\n`

I've checked out master and when I run the tests I get failures due to mismatched line endings.

image

"Contents have differences only in line separators".

I've tried implementing a function to normalize the line endings to \n

/**
 * Read all lines in a file, with line-endings normalized to `\n`. The result is [trim]-ed.
 *
 * This helps ensure consistent test results across Windows and Unix platforms.
 */
fun File.readTextNormalized(): String = useLines(Charsets.UTF_8) { it.joinToString("\n").trim() }

However this doesn't resolve the issue, and for some reason the 'actual' isn't loaded correctly

image


I see that in .editorconfig the line endings are specified as lf, but because there's no .gitattributes file, very few of the files are formatted as lf.

The fix is probably to add .gitattributes and reformat all files to have lf endings.

Usage with Maven

Hello,

Can we use this lib with Maven or without Gradle?

Thanks in advance.

Describe all invariants

Describe all invariants in the documentation, e.g. what we consider as a subtype for the functions signatures, how we handle extension functions, what entities we are looking for (e.g. entities with the public modifier, top-level functions)

Check wrong cases

These cases have errors for the current version with bytecode. We need to check them in the current version, also add tests for them

  1. Function:
fun foo(): Boolean {
    println("public second example foo")
    return true
}

Query:

SmartReflekt.functions<() -> Boolean>().filter { it.isTopLevel && it.name == "foo" }.resolve()
  1. Function:
fun <T> foo(): List<T>

Query:

SmartReflekt.functions<Function0<List<*>>>().filter { it.isTopLevel && it.name == "foo" }.resolve()
  1. Enum case

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

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

// TODO: check if it's okay to add files based on libraries' names in their paths

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)
        }

Information wrapper for object instances returned by the DSL

Possible algorithm of implementation:

  1. To interface ReflektClass<T> add val objectInstance: T?
  2. Add interface ReflektObject<T : Any> : ReflektClass<T> where objectInstance is overriden by type T
  3. Add value class ReflektObjectDelegate(val reflektClass: ReflektClass<T>) : ReflektObject<T> { ... }
  4. In objects DSL results resulting ReflektClass for objects should be converted to ReflektObjectDelegate

Maybe the interface ReflektObject isn't necessary at all.

Add VERSION.md

Add VERSION.md file and replace gathering version in the project everywhere

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.