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

GH-55 add assert hint for spock mocks #56

Merged
merged 1 commit into from
Jan 25, 2025
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
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) {
collectFromExpectationBlockChildren(c.lastChild, sink)
}
} else if (current.isExpectation) {
} else if (current.isExpectation()) {
if (current.hasTitle == false) {
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
Loading