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

Migrate to Analysis API #56

Merged
merged 6 commits into from
Sep 21, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Kobweb IntelliJ Plugin

## [0.1.4]
Copy link
Contributor

Choose a reason for hiding this comment

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

Very minor question: What do you think about 0.2.0? This seems like a bigger change than normal.


### Added

- Support for Kotlin K2 mode
- Updated the compatibility range to 2024.2.1 - 2024.3

## [0.1.3]

### Added
Expand Down
10 changes: 4 additions & 6 deletions plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ dependencies {
intellijPlatform {
pluginModule(implementation(project(":kobweb-model")))
// Interesting statistics: https://plugins.jetbrains.com/docs/marketplace/product-versions-in-use-statistics.html
// We target 2023.3 for:
// - ProjectActivity (available since 2023.1)
// - Kotlin 1.9 support
intellijIdeaCommunity("2023.3")
// We target 2024.2.1 as it is the earliest version supporting K2 mode / the Analysis API
intellijIdeaCommunity("2024.2.1")

bundledPlugins(
"org.jetbrains.kotlin",
Expand Down Expand Up @@ -72,7 +70,7 @@ intellijPlatform {

ideaVersion {
//sinceBuild derived from intellij.version
untilBuild = "242.*" // Include EAP
untilBuild = "243.*" // Include EAP
}

changeNotes = provider {
Expand Down Expand Up @@ -149,7 +147,7 @@ changelog {

fun Project.isSnapshot() = version.toString().endsWith("-SNAPSHOT")

val jvmTarget = JvmTarget.JVM_17
val jvmTarget = JvmTarget.JVM_21
tasks.withType<JavaCompile>().configureEach {
sourceCompatibility = jvmTarget.target
targetCompatibility = jvmTarget.target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.varabyte.kobweb.intellij.util.kobweb.isInKobwebSource
import com.varabyte.kobweb.intellij.util.kobweb.isInReadableKobwebProject
import org.jetbrains.kotlin.idea.base.psi.kotlinFqName
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.base.KaConstantValue
import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull
import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.js.translate.declaration.hasCustomGetter
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import java.awt.Color
import kotlin.math.abs
import kotlin.math.roundToInt
Expand All @@ -39,8 +42,6 @@ import kotlin.math.roundToInt
*/
private const val MAX_SEARCH_DEPTH = 15

private const val KOBWEB_COLOR_COMPANION_FQ_NAME = "com.varabyte.kobweb.compose.ui.graphics.Color.Companion"

/**
* Enables showing small rectangular gutter icons that preview Kobweb colors
*/
Expand Down Expand Up @@ -105,142 +106,115 @@ private fun Float.toColorInt(): Int {
return (this * 255f).roundToInt().coerceIn(0, 255)
}

private object ColorFunctions {
private val KOBWEB_COLOR_COMPANION_ID =
ClassId.fromString("com/varabyte/kobweb/compose/ui/graphics/Color.Companion")
val rgb = CallableId(KOBWEB_COLOR_COMPANION_ID, Name.identifier("rgb"))
val rgba = CallableId(KOBWEB_COLOR_COMPANION_ID, Name.identifier("rgba"))
val argb = CallableId(KOBWEB_COLOR_COMPANION_ID, Name.identifier("argb"))
val hsl = CallableId(KOBWEB_COLOR_COMPANION_ID, Name.identifier("hsl"))
val hsla = CallableId(KOBWEB_COLOR_COMPANION_ID, Name.identifier("hsla"))

val entries = listOf(rgb, rgba, argb, hsl, hsla)
val names = entries.map { it.callableName }
}

private fun KaConstantValue.asIntOrNull(): Int? = (this as? KaConstantValue.IntValue)?.value
private fun KaConstantValue.asFloatOrNull(): Float? = (this as? KaConstantValue.FloatValue)?.value
private fun KaConstantValue.asLongOrNull(): Long? = (this as? KaConstantValue.LongValue)?.value

/**
* Checks if a call expression represents a Kobweb color function call and if so, try extracting the color from it.
*
* @return The specified color, if it could be parsed and the callee is a Kobweb color function, otherwise null
*/
private fun KtCallExpression.tryParseKobwebColorFunctionColor(): Color? {
"$KOBWEB_COLOR_COMPANION_FQ_NAME.rgb".let { rgbFqn ->
this.extractConstantArguments1<Int>(rgbFqn)?.let { (rgb) ->
return tryCreateRgbColor(rgb)
}

this.extractConstantArguments1<Long>(rgbFqn)?.let { (rgb) ->
return tryCreateRgbColor(rgb.toInt())
}

this.extractConstantArguments3<Int, Int, Int>(rgbFqn)?.let { (r, g, b) ->
return tryCreateRgbColor(r, g, b)
}

this.extractConstantArguments3<Float, Float, Float>(rgbFqn)?.let { (r, g, b) ->
return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
val ktExpression = (this.calleeExpression as? KtNameReferenceExpression)
?.takeIf { it.getReferencedNameAsName() in ColorFunctions.names }
?: return null
analyze(ktExpression) {
val callableId = (ktExpression.mainReference.resolveToSymbol() as? KaFunctionSymbol)
?.callableId
?.takeIf { it in ColorFunctions.entries }
?: return@analyze
val functionArgs = ktExpression.resolveToCall()?.successfulFunctionCallOrNull()?.argumentMapping
?: return@analyze

when (callableId) {
ColorFunctions.rgb -> when (functionArgs.size) {
1 -> {
val constantValue = functionArgs.entries.single().key.evaluate()
val rgb = constantValue?.asIntOrNull() ?: constantValue?.asLongOrNull()?.toInt() ?: return@analyze
return tryCreateRgbColor(rgb)
}

3 -> {
val (r, g, b) = functionArgs.mapNotNull {
val constantValue = it.key.evaluate()
constantValue?.asIntOrNull() ?: constantValue?.asFloatOrNull()?.toColorInt()
}.takeIf { it.size == 3 } ?: return@analyze
return tryCreateRgbColor(r, g, b)
}
}

ColorFunctions.rgba -> when (functionArgs.size) {
1 -> {
val constantValue = functionArgs.entries.single().key.evaluate()
val rgb = constantValue?.asIntOrNull() ?: constantValue?.asLongOrNull()?.toInt() ?: return@analyze
return tryCreateRgbColor(rgb shr 8)
}

4 -> {
val (r, g, b) = functionArgs.entries.take(3).mapNotNull {
val constantValue = it.key.evaluate()
constantValue?.asIntOrNull() ?: constantValue?.asFloatOrNull()?.toColorInt()
}.takeIf { it.size == 3 } ?: return@analyze
return tryCreateRgbColor(r, g, b)
}
}

ColorFunctions.argb -> when (functionArgs.size) {
1 -> {
val constantValue = functionArgs.entries.single().key.evaluate()
val rgb = constantValue?.asIntOrNull() ?: constantValue?.asLongOrNull()?.toInt() ?: return@analyze
return tryCreateRgbColor(rgb and 0x00_FF_FF_FF)
}

4 -> {
val (r, g, b) = functionArgs.entries.drop(1).mapNotNull {
val constantValue = it.key.evaluate()
constantValue?.asIntOrNull() ?: constantValue?.asFloatOrNull()?.toColorInt()
}.takeIf { it.size == 3 } ?: return@analyze
return tryCreateRgbColor(r, g, b)
}
}

ColorFunctions.hsl -> {
val h = functionArgs.entries.first().key.evaluate()
.let { it?.asIntOrNull() ?: it?.asFloatOrNull()?.roundToInt() }
?: return@analyze
val (s, l) = functionArgs.entries.drop(1)
.mapNotNull { it.key.evaluate()?.asFloatOrNull() }
.takeIf { it.size == 2 }
?: return@analyze
return tryCreateHslColor(h, s, l)
}

ColorFunctions.hsla -> {
val h = functionArgs.entries.first().key.evaluate()
.let { it?.asIntOrNull() ?: it?.asFloatOrNull()?.roundToInt() }
?: return@analyze
val (s, l) = functionArgs.entries.drop(1).dropLast(1)
.mapNotNull { it.key.evaluate()?.asFloatOrNull() }
.takeIf { it.size == 2 }
?: return@analyze
return tryCreateHslColor(h, s, l)
}
}
}

"$KOBWEB_COLOR_COMPANION_FQ_NAME.rgba".let { rgbaFqn ->
this.extractConstantArguments1<Int>(rgbaFqn)?.let { (rgb) ->
return tryCreateRgbColor(rgb shr 8)
}

this.extractConstantArguments1<Long>(rgbaFqn)?.let { (rgb) ->
return tryCreateRgbColor(rgb.toInt() shr 8)
}

this.extractConstantArguments4<Int, Int, Int, Int>(rgbaFqn)?.let { (r, g, b) ->
return tryCreateRgbColor(r, g, b)
}

this.extractConstantArguments4<Int, Int, Int, Float>(rgbaFqn)?.let { (r, g, b) ->
return tryCreateRgbColor(r, g, b)
}

this.extractConstantArguments4<Float, Float, Float, Float>(rgbaFqn)?.let { (r, g, b) ->
return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
}
}

"$KOBWEB_COLOR_COMPANION_FQ_NAME.argb".let { argbFqn ->
this.extractConstantArguments1<Int>(argbFqn)?.let { (rgb) ->
return tryCreateRgbColor(rgb and 0x00_FF_FF_FF)
}

this.extractConstantArguments1<Long>(argbFqn)?.let { (rgb) ->
return tryCreateRgbColor(rgb.toInt() and 0x00_FF_FF_FF)
}

this.extractConstantArguments4<Int, Int, Int, Int>(argbFqn)?.let { (_, r, g, b) ->
return tryCreateRgbColor(r, g, b)
}

this.extractConstantArguments4<Float, Int, Int, Int>(argbFqn)?.let { (_, r, g, b) ->
return tryCreateRgbColor(r, g, b)
}

this.extractConstantArguments4<Float, Float, Float, Float>(argbFqn)?.let { (_, r, g, b) ->
return tryCreateRgbColor(r.toColorInt(), g.toColorInt(), b.toColorInt())
}
}

"$KOBWEB_COLOR_COMPANION_FQ_NAME.hsl".let { hslFqn ->
this.extractConstantArguments3<Int, Float, Float>(hslFqn)?.let { (h, s, l) ->
return tryCreateHslColor(h, s, l)
}

this.extractConstantArguments3<Float, Float, Float>(hslFqn)?.let { (h, s, l) ->
return tryCreateHslColor(h.roundToInt(), s, l)
}
}

"$KOBWEB_COLOR_COMPANION_FQ_NAME.hsla".let { hslaFqn ->
this.extractConstantArguments4<Int, Float, Float, Float>(hslaFqn)?.let { (h, s, l) ->
return tryCreateHslColor(h, s, l)
}

this.extractConstantArguments4<Float, Float, Float, Float>(hslaFqn)?.let { (h, s, l) ->
return tryCreateHslColor(h.roundToInt(), s, l)
}
}

return null
}

private data class Values<T1, T2, T3, T4>(
val v1: T1,
val v2: T2,
val v3: T3,
val v4: T4
)

private inline fun <reified T> KtValueArgument.extractConstantValue(): T? {
val constantExpression = getArgumentExpression() as? KtConstantExpression ?: return null
val bindingContext = constantExpression.analyze(BodyResolveMode.PARTIAL)
val constant = bindingContext.get(BindingContext.COMPILE_TIME_VALUE, constantExpression) ?: return null
val type = bindingContext.getType(constantExpression) ?: return null
return constant.getValue(type) as? T
}

private fun KtCallExpression.valueArgumentsIf(fqn: String, requiredSize: Int): List<KtValueArgument>? {
val calleeExpression = calleeExpression as? KtNameReferenceExpression ?: return null
val callee = calleeExpression.findDeclaration() as? KtNamedFunction ?: return null
if (callee.kotlinFqName?.asString() != fqn) return null
return valueArguments.takeIf { it.size == requiredSize }
}

private inline fun <reified I> KtCallExpression.extractConstantArguments1(fqn: String): Values<I, Unit, Unit, Unit>? {
val valueArguments = valueArgumentsIf(fqn, 1) ?: return null
val v1: I? = valueArguments[0].extractConstantValue()
return if (v1 != null) Values(v1, Unit, Unit, Unit) else null
}

private inline fun <reified I1, reified I2, reified I3> KtCallExpression.extractConstantArguments3(fqn: String): Values<I1, I2, I3, Unit>? {
val valueArguments = valueArgumentsIf(fqn, 3) ?: return null
val v1: I1? = valueArguments[0].extractConstantValue()
val v2: I2? = valueArguments[1].extractConstantValue()
val v3: I3? = valueArguments[2].extractConstantValue()
return if (v1 != null && v2 != null && v3 != null) Values(v1, v2, v3, Unit) else null
}

private inline fun <reified I1, reified I2, reified I3, reified I4> KtCallExpression.extractConstantArguments4(fqn: String): Values<I1, I2, I3, I4>? {
val valueArguments = valueArgumentsIf(fqn, 4) ?: return null
val v1: I1? = valueArguments[0].extractConstantValue()
val v2: I2? = valueArguments[1].extractConstantValue()
val v3: I3? = valueArguments[2].extractConstantValue()
val v4: I4? = valueArguments[3].extractConstantValue()
return if (v1 != null && v2 != null && v3 != null && v4 != null) Values(v1, v2, v3, v4) else null
}

private fun tryCreateRgbColor(r: Int, g: Int, b: Int) =
runCatching { Color(r, g, b) }.getOrNull()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import com.intellij.psi.util.CachedValue
import com.varabyte.kobweb.intellij.util.kobweb.isInKobwebSource
import com.varabyte.kobweb.intellij.util.kobweb.isInReadableKobwebProject
import com.varabyte.kobweb.intellij.util.psi.hasAnyAnnotation
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.psi.KtNamedFunction

private val IS_COMPOSABLE_KEY = Key<CachedValue<Boolean>>("IS_COMPOSABLE")
private val COMPOSABLE_ANNOTATION_ID = ClassId.fromString("androidx/compose/runtime/Composable")

/**
* Suppress the "Function name should start with a lowercase letter" inspection.
Expand All @@ -21,7 +23,7 @@ class FunctionNameInspectionSuppressor : InspectionSuppressor {
if (!element.isInReadableKobwebProject() && !element.isInKobwebSource()) return false
val ktFunction = element.parent as? KtNamedFunction ?: return false

return ktFunction.hasAnyAnnotation(IS_COMPOSABLE_KEY, "androidx.compose.runtime.Composable")
return ktFunction.hasAnyAnnotation(IS_COMPOSABLE_KEY, COMPOSABLE_ANNOTATION_ID)
}

override fun getSuppressActions(element: PsiElement?, toolId: String) = emptyArray<SuppressQuickFix>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.util.CachedValue
import com.varabyte.kobweb.intellij.util.kobweb.isInReadableKobwebProject
import com.varabyte.kobweb.intellij.util.psi.hasAnyAnnotation
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.psi.KtNamedFunction

private val ANNOTATION_GENERATES_CODE_KEY = Key<CachedValue<Boolean>>("ANNOTATION_GENERATES_CODE")

private val SUPPRESS_UNUSED_WHEN_ANNOTATED_WITH = arrayOf(
"com.varabyte.kobweb.api.Api",
"com.varabyte.kobweb.api.init.InitApi",
"com.varabyte.kobweb.core.App",
"com.varabyte.kobweb.core.Page",
"com.varabyte.kobweb.core.init.InitKobweb",
"com.varabyte.kobweb.silk.init.InitSilk",
ClassId.fromString("com/varabyte/kobweb/api/Api"),
ClassId.fromString("com/varabyte/kobweb/api/init/InitApi"),
ClassId.fromString("com/varabyte/kobweb/core/App"),
ClassId.fromString("com/varabyte/kobweb/core/Page"),
ClassId.fromString("com/varabyte/kobweb/core/init/InitKobweb"),
ClassId.fromString("com/varabyte/kobweb/silk/init/InitSilk"),
)

/**
Expand All @@ -27,7 +28,10 @@ class UnusedInspectionSuppressor : InspectionSuppressor {
override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean {
if (toolId != "unused") return false
if (!element.isInReadableKobwebProject()) return false
val ktFunction = element.parent as? KtNamedFunction ?: return false
// Originally, only `element.parent` was checked, but at some point it became necessary to check `element` too
val ktFunction = element.parent as? KtNamedFunction
?: element as? KtNamedFunction
?: return false

return ktFunction.hasAnyAnnotation(ANNOTATION_GENERATES_CODE_KEY, *SUPPRESS_UNUSED_WHEN_ANNOTATED_WITH)
}
Expand Down
Loading