Skip to content

Commit

Permalink
Merge pull request #56 from akefirad/GH-55-assert-spock-mocks
Browse files Browse the repository at this point in the history
GH-55 add assert hint for spock mocks
  • Loading branch information
akefirad authored Jan 25, 2025
2 parents fffc1c3 + 4ae3305 commit 4c20606
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 35 deletions.
30 changes: 18 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,29 @@
# - Create a draft release.

name: Build

on:
push:
branches: [ main ]
branches: [main]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read # This is required for actions/checkout
pull-requests: write

jobs:
build:
name: Build
name: Build Code
runs-on: ubuntu-latest
outputs:
version: ${{ steps.properties.outputs.version }}
changelog: ${{ steps.properties.outputs.changelog }}
pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }}
steps:

- name: Checkout repository
uses: actions/checkout@v4

Expand All @@ -40,6 +44,8 @@ jobs:

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
add-job-summary-as-pr-comment: on-failure

- name: Export properties
id: properties
Expand Down Expand Up @@ -77,10 +83,9 @@ jobs:

test-code:
name: Test Code
needs: [ build ]
needs: [build]
runs-on: ubuntu-latest
steps:

- name: Checkout repository
uses: actions/checkout@v4

Expand All @@ -92,6 +97,8 @@ jobs:

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
add-job-summary-as-pr-comment: on-failure

- name: Run tests
run: ./gradlew check
Expand All @@ -111,14 +118,13 @@ jobs:

inspect-code:
name: Inspect Code
needs: [ build ]
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: write
checks: write
pull-requests: write
steps:

- name: Maximize build space
uses: jlumbroso/free-disk-space@main
with:
Expand All @@ -141,16 +147,15 @@ jobs:
uses: JetBrains/[email protected]
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_205267377 }}
QODANA_ENDPOINT: 'https://qodana.cloud'
QODANA_ENDPOINT: "https://qodana.cloud"
with:
cache-default-branch-only: true

verify-plugin:
name: Verify Plugin
needs: [ build ]
needs: [build]
runs-on: ubuntu-latest
steps:

- name: Maximize build space
uses: jlumbroso/free-disk-space@main
with:
Expand All @@ -168,6 +173,8 @@ jobs:

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
add-job-summary-as-pr-comment: on-failure

- name: Setup plugin verifier IDEs cache
uses: actions/cache@v4
Expand All @@ -188,12 +195,11 @@ jobs:
release-draft:
name: Release Draft
if: github.event_name != 'pull_request' # TODO: run only on main?
needs: [ build, test-code, inspect-code, verify-plugin ]
needs: [build, test-code, inspect-code, verify-plugin]
runs-on: ubuntu-latest
permissions:
contents: write
steps:

- name: Checkout repository
uses: actions/checkout@v4

Expand Down
9 changes: 9 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
plugins {
id("com.gradle.develocity") version "3.18.1"
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
}

rootProject.name = "groom"

develocity {
buildScan {
termsOfUseUrl = "https://gradle.com/terms-of-service"
termsOfUseAgree = "yes"
publishing.onlyIf { it.buildResult.failures.isNotEmpty() }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.akefirad.groom.spock

import com.akefirad.groom.intellij.PsiElementExtensions.startOffset
import com.akefirad.groom.spock.SpockSpecUtils.hasAnySpecification
import com.akefirad.groom.spock.SpockSpecUtils.isSpecification
import com.akefirad.groom.spock.SpockSpecUtils.isSpeckLabel
import com.intellij.codeInsight.hints.declarative.InlayHintsProvider
import com.intellij.codeInsight.hints.declarative.InlayTreeSink
Expand All @@ -11,27 +13,29 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.PossiblyDumbAware
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod
import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.arithmetic.GrMultiplicativeExpressionImpl
import org.jetbrains.plugins.groovy.lang.psi.util.isWhiteSpaceOrNewLine

class AssertInlayHintsProvider : InlayHintsProvider, PossiblyDumbAware {
override fun createCollector(file: PsiFile, editor: Editor): AssertInlayHintsCollector? {
// TODO: check if it's an actual test file!
return if (file is GroovyFile) AssertInlayHintsCollector() else null
return if (file.hasAnySpecification()) AssertInlayHintsCollector() else null
}
}

class AssertInlayHintsCollector : SharedBypassCollector {

override fun collectFromElement(element: PsiElement, sink: InlayTreeSink) {
if (element is GrMethod) // TODO: check if it's a test method!
// TODO: also check if it's a test method!
if (element is GrMethod && element.isSpecification()) {
collectFromMethod(element, sink)
}
}

private fun collectFromMethod(m: GrMethod, sink: InlayTreeSink) {
Expand All @@ -41,6 +45,8 @@ class AssertInlayHintsCollector : SharedBypassCollector {
}

private fun collectFromBlock(b: GrOpenBlock, sink: InlayTreeSink) {
fun SpecLabelElement?.isExpectation() = this?.name?.isExpectation == true

var label: SpecLabelElement? = null
for (c in b.children) {
if (c.isWhiteSpaceOrNewLine()) {
Expand All @@ -49,47 +55,66 @@ class AssertInlayHintsCollector : SharedBypassCollector {

if (c.isSpeckLabel()) {
val current = SpecLabelElement.ofLabel(c)
if (current.isContinuation && label?.isExpectation == true) {
if (current.isContinuation && label.isExpectation()) {
if (current.hasTitle == false) {

Check notice on line 59 in src/main/kotlin/com/akefirad/groom/spock/AssertInlayHintsProvider.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Boolean expression can be simplified

Boolean expression can be simplified
collectFromExpectationBlockChildren(c.lastChild, sink)
}
} else if (current.isExpectation) {
} else if (current.isExpectation()) {
if (current.hasTitle == false) {

Check notice on line 63 in src/main/kotlin/com/akefirad/groom/spock/AssertInlayHintsProvider.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Boolean expression can be simplified

Boolean expression can be simplified
collectFromExpectationBlockChildren(c.lastChild, sink)
}
label = current
} else {
label = null
}
} else if (label?.isExpectation == true) {
} else if (label.isExpectation()) {
collectFromExpectationBlockChildren(c, sink)
}

}
}

private fun collectFromExpectationBlockChildren(e: PsiElement, sink: InlayTreeSink) {
class AssertInlayHint : (PresentationTreeBuilder) -> Unit {
override fun invoke(builder: PresentationTreeBuilder) = builder.text("assert")
}

if (e !is GrExpression || e is GrAssignmentExpression) return
if (e.isSpockInteractionExpression()) return

if (e.isVerifyAllExpression() || e.isWithExpression()) {
val closure = e.lastChild as? GrClosableBlock ?: return
closure.children.forEach { collectFromExpectationBlockChildren(it, sink) }
if (e.isSpockMockExpression()) {
e.addAssertInlayHint(sink)
e.lastChild?.let { collectFromClosableBlock(it.lastChild, sink) }
} else if (e.isVerifyAllExpression()) {
collectFromClosableBlock(e.lastChild, sink)
} else if (e.isWithExpression()) {
collectFromClosableBlock(e.lastChild, sink)
} else {
val position = InlineInlayPosition(e.startOffset, relatedToPrevious = false)
sink.addPresentation(position, hasBackground = true, builder = AssertInlayHint())
e.addAssertInlayHint(sink)
}
}

private fun GrExpression.isSpockInteractionExpression() = isMethodCallExpression("interaction")
private fun collectFromClosableBlock(e: PsiElement?, sink: InlayTreeSink) {
val closure = e as? GrClosableBlock ?: return
closure.children.forEach { collectFromExpectationBlockChildren(it, sink) }
}

private fun GrExpression.isVerifyAllExpression() = isMethodCallExpression("verifyAll")
private fun GrExpression.isWithExpression() = isMethodCallExpression("with")

private fun GrExpression.isSpockInteractionExpression() = isMethodCallExpression("interaction")
private fun GrExpression.isSpockMockExpression(): Boolean {
return this is GrMultiplicativeExpressionImpl // TODO: use GrBinaryExpression and check the operator!
&& rightOperand is GrMethodCallExpression // TODO: anything else to check?
&& leftOperand.text.let { it == "_" || it.matches("\\d+".toRegex()) } // FIXME: do it properly!
}

// TODO: this is too naive, we need to handle all edge cases!
private fun GrExpression.isMethodCallExpression(method: String) =
this is GrMethodCallExpression && callReference?.methodName == method

private fun GrExpression.addAssertInlayHint(sink: InlayTreeSink) {
class AssertInlayHint : (PresentationTreeBuilder) -> Unit {
override fun invoke(builder: PresentationTreeBuilder) = builder.text("assert")
}

val position = InlineInlayPosition(startOffset, relatedToPrevious = false)
sink.addPresentation(position, hasBackground = true, builder = AssertInlayHint())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ data class SpecLabelElement(val element: GrLabeledStatement) {
val title = element.title
val hasTitle = title != null
val isContinuation = name.isContinuation
val isExpectation = name.isExpectation

override fun toString() = "$name${if (hasTitle) ": '$title'" else ""}"

Expand Down
8 changes: 5 additions & 3 deletions src/main/kotlin/com/akefirad/groom/spock/SpockSpecUtils.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.akefirad.groom.spock

import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrLabeledStatement
import kotlin.contracts.contract

object SpockSpecUtils {
/*
private const val SPOCK_SPEC_CLASS: String = "spock.lang.Specification"

@JvmStatic
fun PsiElement.hasAnySpecification(): Boolean {
fun PsiFile.hasAnySpecification(): Boolean {
return this is GroovyFile && children
.filterIsInstance<PsiClass>()
.any(SpockSpecUtils::hasSpecificationSuperClass)
Expand All @@ -23,7 +26,6 @@ object SpockSpecUtils {
private fun hasSpecificationSuperClass(clazz: PsiClass) =
generateSequence(clazz) { it.superClass }
.any { it.qualifiedName == SPOCK_SPEC_CLASS }
*/

fun PsiElement.isSpeckLabel(): Boolean {
contract {
Expand Down
Loading

0 comments on commit 4c20606

Please sign in to comment.