Skip to content

Commit

Permalink
Detect extracted JSON plugin for Platform 2024.3+ (#1173)
Browse files Browse the repository at this point in the history
* Extract bytecode manipulation in tests to base class
* Test access to unavailable classes
* Allow to build core plugins in the 'lib' directory
* Support artifact name in mock plugin along with product-info.json and module-descriptors.jar
* Add dump of a sample class from `com.intellij` package
* Provide at least 1 class in the `com.intellij` package in mock IDEs
* Add a new problem for undeclared dependencies
* Verify declaration of JSON plugin in the 243+ Platforms
* Extract product info-based resolver to a separate class
* Introduce named class resolver with a delegate
* Introduce class resolver for product-info.json-based IDEs
* Create mock IDE version
* Support 'depends' in the plugin specification
* Provide assertion for no compatibility
* Introduce IDE that contains type-safe product-info.json model
* Generate type-safe IDE when parsed product info is available
* Plugin classpath is built with dependencies overlapping bundled plugins in the IDE
* Use classpath resolution logic for nonexistent layout elements
* When discovering plugin dependencies, module may expose itself as a plugin
* Pickup dependency tree calculation implementation
* Support ByteBuddy in structure tests
* Support classpath filtering for transitive dependencies
  • Loading branch information
novotnyr authored Nov 22, 2024
1 parent a0ff975 commit 23d265f
Show file tree
Hide file tree
Showing 59 changed files with 3,331 additions and 372 deletions.
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[versions]
kotlin = "1.8.21"

byteBuddy = "1.15.10"
caffeine = "3.1.8"
commons-io = "2.16.1"
gson = "2.10.1"
Expand All @@ -22,6 +23,7 @@ spullara-cliParser = "1.1.6"
xz = "1.9"

[libraries]
byteBuddy = { group = "net.bytebuddy", name = "byte-buddy", version.ref = "byteBuddy" }
caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeine" }
commons-io = { group = "commons-io", name = "commons-io", version.ref = "commons-io" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@

package com.jetbrains.plugin.structure.base.utils.contentBuilder

import com.jetbrains.plugin.structure.base.utils.*
import java.io.File
import com.jetbrains.plugin.structure.base.utils.createDir
import com.jetbrains.plugin.structure.base.utils.isDirectory
import com.jetbrains.plugin.structure.base.utils.isFile
import com.jetbrains.plugin.structure.base.utils.listFiles
import com.jetbrains.plugin.structure.base.utils.readBytes
import com.jetbrains.plugin.structure.base.utils.simpleName
import java.nio.file.Path

interface ContentBuilder {
Expand All @@ -16,6 +20,7 @@ interface ContentBuilder {
fun file(name: String, localFile: Path)
fun dir(name: String, localDirectory: Path)
fun dir(name: String, content: ContentBuilder.() -> Unit)
fun dirs(name: String, content: ContentBuilder.() -> Unit)
fun zip(name: String, content: ContentBuilder.() -> Unit)
}

Expand Down Expand Up @@ -69,6 +74,23 @@ private class ContentBuilderImpl(private val result: ChildrenOwnerSpec) : Conten
addChild(name, directorySpec)
}

override fun dirs(name: String, content: ContentBuilder.() -> Unit) {
val pathElements = name.split("/")
when (pathElements.size) {
0 -> throw IllegalArgumentException("Cannot have empty name")
1 -> dir(pathElements.first(), content)
2 -> dir(pathElements.first()) {
dir(pathElements[1], content)
}

else -> {
val firstElement = pathElements.first()
val rest = pathElements.drop(1)
addChild(firstElement, resolveDirs(rest, content))
}
}
}

override fun dir(name: String, localDirectory: Path) {
check(localDirectory.isDirectory) { "Not a directory: $localDirectory" }
dir(name) {
Expand All @@ -90,4 +112,40 @@ private class ContentBuilderImpl(private val result: ChildrenOwnerSpec) : Conten
private fun addChild(name: String, spec: ContentSpec) {
result.addChild(name, spec)
}

private fun resolveDirs(pathElements: List<String>, content: ContentBuilder.() -> Unit): SingleChildSpec {
val parentAndChildPairs = pathElements.windowedPairs()
val directorySpecs = parentAndChildPairs.mapIndexed { i, (parent, child) ->
val childContent = if (i < parentAndChildPairs.lastIndex) DirectorySpec() else buildDirectoryContent(content)
SingleChildSpec(parent, child, childContent)
}
return buildHierarchy(directorySpecs)
}

private fun buildHierarchy(specs: List<SingleChildSpec>): SingleChildSpec = with(ArrayDeque(specs)) {
while (size > 1) {
val child = removeLast()
val parent = removeLast()
addLast(parent.copy(child = child))
}
first()
}

private fun List<String>.windowedPairs() = windowed(2) { ParentAndChild(it.first(), it.last()) }

private data class ParentAndChild(val parent: String, val child: String)

private data class SingleChildSpec(val name: String, val child: ContentSpec) : ContentSpec {
constructor(name: String, child: String, childContent: ContentSpec) : this(name, SingleChildSpec(child, childContent))

override fun generate(target: Path) {
target.createDir()
val childFile = target.resolve(name)
child.generate(childFile)
}

override fun toString(): String {
return "$name/$child"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,14 @@ class DirectorySpec : ChildrenOwnerSpec {
spec.generate(childFile)
}
}

override fun toString(): String {
return children.map { (name, content) ->
if (content is DirectorySpec) {
"$name: $content"
} else {
name
}
}.joinToString(prefix = "[", postfix = "]")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import com.jetbrains.plugin.structure.base.utils.closeAll
import org.objectweb.asm.tree.ClassNode
import java.util.*


private const val DEFAULT_COMPOSITE_RESOLVER_NAME = "Unnamed Composite Resolver"
/**
* [Resolver] that combines several [resolvers] with the Java classpath search strategy.
*/
class CompositeResolver private constructor(
private val resolvers: List<Resolver>,
override val readMode: ReadMode
override val readMode: ReadMode,
val name: String
) : Resolver() {

private val packageToResolvers: MutableMap<String, MutableList<Resolver>> = hashMapOf()
Expand Down Expand Up @@ -93,7 +96,7 @@ class CompositeResolver private constructor(
resolvers.closeAll()
}

override fun toString() = "Union of ${resolvers.size} resolver" + (if (resolvers.size != 1) "s" else "")
override fun toString() = "$name is a union of ${resolvers.size} resolver" + (if (resolvers.size != 1) "s" else "")

companion object {

Expand All @@ -102,17 +105,22 @@ class CompositeResolver private constructor(

@JvmStatic
fun create(resolvers: Iterable<Resolver>): Resolver {
return create(resolvers, DEFAULT_COMPOSITE_RESOLVER_NAME)
}

@JvmStatic
fun create(resolvers: Iterable<Resolver>, resolverName: String): Resolver {
val list = resolvers.toList()
return when(list.size) {
0 -> EmptyResolver
0 -> EmptyNamedResolver(resolverName)
1 -> list.first()
else -> {
val readMode = if (list.all { it.readMode == ReadMode.FULL }) {
ReadMode.FULL
} else {
ReadMode.SIGNATURES
}
CompositeResolver(list, readMode)
CompositeResolver(list, readMode, resolverName)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.jetbrains.plugin.structure.classes.resolvers

import org.objectweb.asm.tree.ClassNode
import java.util.*

class EmptyNamedResolver(val name: String) : Resolver() {
override val readMode
get() = ReadMode.FULL

override fun processAllClasses(processor: (ResolutionResult<ClassNode>) -> Boolean) = true

override fun resolveClass(className: String) = ResolutionResult.NotFound

override fun resolveExactPropertyResourceBundle(baseName: String, locale: Locale) = ResolutionResult.NotFound

override fun containsClass(className: String) = false

override fun containsPackage(packageName: String) = false

override val allClasses get() = emptySet<String>()

override val allPackages get() = emptySet<String>()

override val allBundleNameSet: ResourceBundleNameSet get() = ResourceBundleNameSet(emptyMap())

override fun toString() = "$name (empty resolver)"

override fun close() = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.jetbrains.plugin.structure.ide.IdeManagerImpl.Companion.isCompiledCom
import com.jetbrains.plugin.structure.ide.IdeManagerImpl.Companion.isCompiledUltimate
import com.jetbrains.plugin.structure.ide.IdeManagerImpl.Companion.isDistributionIde
import com.jetbrains.plugin.structure.ide.InvalidIdeException
import com.jetbrains.plugin.structure.ide.classes.resolver.ProductInfoClassResolver
import com.jetbrains.plugin.structure.ide.getRepositoryLibrariesJars
import java.nio.file.Path

Expand All @@ -33,7 +34,7 @@ object IdeResolverCreator {
fun createIdeResolver(readMode: Resolver.ReadMode, ide: Ide): Resolver {
val idePath = ide.idePath
return when {
isDistributionIde(idePath) -> getJarsResolver(idePath.resolve("lib"), readMode, IdeFileOrigin.IdeLibDirectory(ide))
isDistributionIde(idePath) -> getIdeResolverFromDistribution(ide, readMode)
isCompiledCommunity(idePath) || isCompiledUltimate(idePath) -> getIdeResolverFromCompiledSources(idePath, readMode, ide)
else -> throw InvalidIdeException(idePath, "Invalid IDE $ide at $idePath")
}
Expand All @@ -54,6 +55,14 @@ object IdeResolverCreator {
return CompositeResolver.create(buildJarOrZipFileResolvers(jars + antJars + moduleJars, readMode, parentOrigin))
}

private fun getIdeResolverFromDistribution(ide: Ide, readMode: Resolver.ReadMode): Resolver = with(ide) {
return if (ProductInfoClassResolver.supports(idePath)) {
ProductInfoClassResolver.of(ide, readMode)
} else {
getJarsResolver(idePath.resolve("lib"), readMode, IdeFileOrigin.IdeLibDirectory(ide))
}
}

//TODO: Resolver created this way contains all libraries declared in the project,
// including those that don't go to IDE distribution. So such a created resolver may
// resolve classes differently than they are resolved when running IDE.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.jetbrains.plugin.structure.ide.classes.resolver

import com.jetbrains.plugin.structure.classes.resolvers.ResolutionResult
import com.jetbrains.plugin.structure.classes.resolvers.Resolver
import org.objectweb.asm.tree.ClassNode
import java.util.*

class NamedResolver(val name: String, val delegateResolver: Resolver) : Resolver() {

override val readMode get() = delegateResolver.readMode

override val allClasses get() = delegateResolver.allClasses

override val allPackages get() = delegateResolver.allPackages

override val allBundleNameSet get() = delegateResolver.allBundleNameSet

override fun resolveClass(className: String) = delegateResolver.resolveClass(className)

override fun resolveExactPropertyResourceBundle(baseName: String, locale: Locale) =
delegateResolver.resolveExactPropertyResourceBundle(baseName, locale)

override fun containsClass(className: String) = delegateResolver.containsClass(className)

override fun containsPackage(packageName: String) = delegateResolver.containsPackage(packageName)

override fun processAllClasses(processor: (ResolutionResult<ClassNode>) -> Boolean) =
delegateResolver.processAllClasses(processor)

override fun close() = delegateResolver.close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.jetbrains.plugin.structure.ide.classes.resolver

import com.jetbrains.plugin.structure.classes.resolvers.CompositeResolver
import com.jetbrains.plugin.structure.classes.resolvers.ResolutionResult
import com.jetbrains.plugin.structure.classes.resolvers.Resolver
import com.jetbrains.plugin.structure.intellij.plugin.IdePlugin
import com.jetbrains.plugin.structure.intellij.plugin.dependencies.DefaultDependenciesProvider
import com.jetbrains.plugin.structure.intellij.plugin.dependencies.DependenciesProvider
import org.objectweb.asm.tree.ClassNode
import java.util.*

/**
* A classpath resolver that matches plugin dependencies against bundled plugins of an `product-info.json`-based IDE.
*
* This resolver functions as a classpath filter.
* It takes classes and resource bundles from the IDE
* and includes only those that are declared as dependencies in the plugin.
*/
class PluginDependencyFilteredResolver(
plugin: IdePlugin,
productInfoClassResolver: ProductInfoClassResolver,
private val dependenciesProvider: DependenciesProvider = DefaultDependenciesProvider(productInfoClassResolver.ide)
) : Resolver() {
val filteredResolvers: List<NamedResolver> = getResolvers(plugin, productInfoClassResolver)

private fun getResolvers(plugin: IdePlugin, productInfoClassResolver: ProductInfoClassResolver): List<NamedResolver> {
return dependenciesProvider
.getDependencies(plugin)
.map { dependency ->
productInfoClassResolver.layoutComponentResolvers.firstOrNull { component -> dependency.matches(component.name) }
?: productInfoClassResolver.bootClasspathResolver
}
}

private val delegateResolver = filteredResolvers.asResolver()

override val readMode get() = delegateResolver.readMode

override val allClasses get() = delegateResolver.allClasses

override val allPackages get() = delegateResolver.allPackages

override val allBundleNameSet get() = delegateResolver.allBundleNameSet

override fun resolveClass(className: String) = delegateResolver.resolveClass(className)

override fun resolveExactPropertyResourceBundle(baseName: String, locale: Locale) =
delegateResolver.resolveExactPropertyResourceBundle(baseName, locale)

override fun containsClass(className: String) = delegateResolver.containsClass(className)

override fun containsPackage(packageName: String) = delegateResolver.containsPackage(packageName)

override fun processAllClasses(processor: (ResolutionResult<ClassNode>) -> Boolean) =
delegateResolver.processAllClasses(processor)

override fun close() = delegateResolver.close()

private fun List<Resolver>.asResolver(): Resolver {
return CompositeResolver.create(this)
}
}
Loading

0 comments on commit 23d265f

Please sign in to comment.