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

Give useful exceptions when a requested psi body is null #267

Merged
merged 1 commit into from
Dec 30, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.psi.psiUtil.parents
import java.io.File
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.contracts.contract
import kotlin.io.path.Path

@Serializable
Expand Down Expand Up @@ -118,29 +119,35 @@ internal class NamedSamples(
val content =
when (val namedDeclaration = cache[request.fqName]) {
is KtClassOrObject -> if (request.bodyOnly) {
namedDeclaration.body!!.textInScope()
namedDeclaration.body
.requireBodyNotNull(namedDeclaration, request.fqName) {
"A type declaration must have a body"
}
.textInScope()
} else {
namedDeclaration.text.trimIndentAfterFirstLine()
}

is KtProperty -> if (request.bodyOnly) {
namedDeclaration.initializer
.requireNotNull {
"${namedDeclaration.containingKtFile} > " +
"A property must have an initializer when using 'bodyOnly = true'."
.requireBodyNotNull(namedDeclaration, request.fqName) {
"A property must have an initializer"
}
.let { (it as? KtStringTemplateExpression) ?: it.getChildOfType() }
.requireNotNull {
"${namedDeclaration.containingKtFile} > " +
"A property initializer must be a string template."
.requireBodyNotNull(namedDeclaration, request.fqName) {
"A property initializer must be a string template"
}
.textInScope()
} else {
namedDeclaration.text
}

is KtNamedFunction -> if (request.bodyOnly) {
namedDeclaration.bodyBlockExpression!!.textInScope()
namedDeclaration.bodyBlockExpression
.requireBodyNotNull(namedDeclaration, request.fqName) {
"A function must use body syntax"
}
.textInScope()
} else {
namedDeclaration.text.trimIndentAfterFirstLine()
}
Expand All @@ -156,6 +163,26 @@ internal class NamedSamples(
}
}

inline fun <T : Any> T?.requireBodyNotNull(
psi: KtElement,
fqName: String,
message: () -> String
): T {
contract {
returns() implies (this@requireBodyNotNull != null)
}
return requireNotNull(this) {
"""
|${psi.containingKtFile.absolutePath()} > ${message()} when using 'bodyOnly = true'.
|requested name: $fqName
|matched code:
|---
|${psi.text.trimIndentAfterFirstLine()}
|---
""".trimMargin()
}
}

private fun createDeclarationCache(
ktFiles: Sequence<KtFile>,
requests: List<SampleRequest>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,13 @@ internal fun String.remove(vararg regex: Regex): String = regex.fold(this) { acc

internal fun String.trimIndentAfterFirstLine(): String {
val split = lines()

// If there's only one line, there's nothing to trim,
// and the remaining logic would just append an extra newline.
if (split.size == 1) return this

val first = split.first()
val remaining = split.drop(1).joinToString("\n").trimIndent()
val remaining = split.drop(1).joinToString("\n")
.trimIndent()
return "$first\n$remaining"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.rickbusarow.doks.internal.psi

import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -239,6 +240,95 @@ class NamedSamplesTest {
""".trimIndent()
}

@Test
fun `an class without a body and bodyOnly = true`() {

// test for: https://github.com/rickbusarow/Doks/issues/223

val exception = shouldThrow<IllegalArgumentException> {
parse(
fqName = "com.test.MyClass",
bodyOnly = true,
"""
package com.test

class MyClass
"""
)
}

exception.message shouldBe """
src/Source_0.kt > A type declaration must have a body when using 'bodyOnly = true'.
requested name: com.test.MyClass
matched code:
---
class MyClass
---
""".trimIndent()
}

@Test
fun `an expression syntax function that invokes a lambda and bodyOnly = true`() {

// test for: https://github.com/rickbusarow/Doks/issues/223

val exception = shouldThrow<IllegalArgumentException> {
parse(
fqName = "com.test.MyClass.myFunction",
bodyOnly = true,
"""
package com.test

class MyClass {
fun myFunction() = runBlocking {
println("boo!")
}
}
"""
)
}

exception.message shouldBe """
src/Source_0.kt > A function must use body syntax when using 'bodyOnly = true'.
requested name: com.test.MyClass.myFunction
matched code:
---
fun myFunction() = runBlocking {
println("boo!")
}
---
""".trimIndent()
}

@Test
fun `an expression syntax function that does not invoke lambda and bodyOnly = true`() {

// test for: https://github.com/rickbusarow/Doks/issues/223

val exception = shouldThrow<IllegalArgumentException> {
parse(
fqName = "com.test.MyClass.myFunction",
bodyOnly = true,
"""
package com.test

class MyClass {
fun myFunction() = Unit
}
"""
)
}

exception.message shouldBe """
src/Source_0.kt > A function must use body syntax when using 'bodyOnly = true'.
requested name: com.test.MyClass.myFunction
matched code:
---
fun myFunction() = Unit
---
""".trimIndent()
}

@Test
fun `a variable inside a nested function can be resolved`() {

Expand Down Expand Up @@ -370,7 +460,6 @@ class NamedSamplesTest {
"""
) shouldBe """
|object Foo
|
""".trimMargin()
}

Expand All @@ -379,7 +468,7 @@ class NamedSamplesTest {
DoksPsiFileFactory()
.createKotlin(
name = "Source_$index.kt",
path = "Source_$index.kt",
path = "src/Source_$index.kt",
content = code.trimIndent()
)
}
Expand Down
Loading