From f932cd969c61dd3f7944a64e9e70d01e4abf151f Mon Sep 17 00:00:00 2001 From: zhislin Date: Tue, 12 Nov 2024 20:51:03 +0800 Subject: [PATCH 01/24] before test --- build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + .../actions/RunCaseAnnoationCheckAction.kt | 31 +++++++ .../annotations/AnnotationFieldConfig.kt | 8 ++ .../annotations/AnnotationFieldType.kt | 7 ++ .../annotations/AnnotationParser.kt | 47 ++++++++++ .../annotations/AnnotationSchema.kt | 73 ++++++++++++++++ .../annotations/AnnotationValidator.kt | 63 ++++++++++++++ .../annotations/UnittestAnnotationConfig.kt | 14 +++ .../casecheck/CheckAnnotationCommand.kt | 69 +++++++++++++++ .../casecheck/CheckMethodDataCommand.kt | 30 +++++++ .../casecheck/UnittestCaseCheckCommand.kt | 76 ++++++++++++++++ .../{ => pitest}/BuildPitestCommandCommand.kt | 3 +- .../{ => pitest}/HandlePitestResultCommand.kt | 3 +- .../commands/{ => pitest}/PitestCommand.kt | 3 +- .../{ => pitest}/PrepareEnvironmentCommand.kt | 4 +- .../commands/{ => pitest}/RunPitestCommand.kt | 3 +- .../{ => pitest}/StoreHistoryCommand.kt | 4 +- .../context/CaseCheckContext.kt | 29 +++++++ .../pitestintellij/context/TestPoints.kt | 32 +++++++ .../pitestintellij/context/UnittestCase.kt | 9 ++ .../context/UnittestCaseInfoContext.kt | 20 +++++ .../context/UnittestCaseStatus.kt | 5 ++ .../context/UnittestMethodContext.kt | 8 ++ .../processor/TestPointProcessor.kt | 24 ++++++ .../processor/UnittestCaseInfoProcessor.kt | 52 +++++++++++ .../processor/UnittestMethodProcessor.kt | 23 +++++ .../services/AnnotationConfigService.kt | 33 +++++++ .../pitestintellij/services/PitestService.kt | 1 + .../ui/AnnotationConfigurable.kt | 86 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 4 + 31 files changed, 754 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/UnittestAnnotationConfig.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckMethodDataCommand.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{ => pitest}/BuildPitestCommandCommand.kt (96%) rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{ => pitest}/HandlePitestResultCommand.kt (95%) rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{ => pitest}/PitestCommand.kt (97%) rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{ => pitest}/PrepareEnvironmentCommand.kt (98%) rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{ => pitest}/RunPitestCommand.kt (91%) rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{ => pitest}/StoreHistoryCommand.kt (67%) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/context/TestPoints.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCase.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseInfoContext.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseStatus.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestMethodContext.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/TestPointProcessor.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestMethodProcessor.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt diff --git a/build.gradle.kts b/build.gradle.kts index 6c3311e..80182d3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.changelog) // Gradle Changelog Plugin alias(libs.plugins.qodana) // Gradle Qodana Plugin alias(libs.plugins.kover) // Gradle Kover Plugin + alias(libs.plugins.serialize) // Gradle Serializer Plugin } group = providers.gradleProperty("pluginGroup").get() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7782a23..6293ac1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,3 +20,4 @@ gradleIntelliJPlugin = { id = "org.jetbrains.intellij", version.ref = "gradleInt kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } +serialize = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt new file mode 100644 index 0000000..0e70cd9 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt @@ -0,0 +1,31 @@ +package com.github.jaksonlin.pitestintellij.actions + +import com.github.jaksonlin.pitestintellij.commands.casecheck.CheckAnnotationCommand +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil + +class RunCaseAnnoationCheckAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + + val psiMethod = findMethodAtCaret(e) ?: return + val context = CaseCheckContext.create(psiMethod) ?: return + CheckAnnotationCommand(e.project!!, context).execute() + } + + + + private fun findMethodAtCaret(e: AnActionEvent): PsiMethod? { + val project = e.project ?: return null + val editor = e.dataContext.getData(CommonDataKeys.EDITOR) ?: return null + val caret = e.dataContext.getData(CommonDataKeys.CARET) ?: return null + val elementAtCaret = PsiDocumentManager.getInstance(project) + .getPsiFile(editor.document)?.findElementAt(caret.offset) ?: return null + return PsiTreeUtil.getParentOfType(elementAtCaret, PsiMethod::class.java) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt new file mode 100644 index 0000000..385f7a1 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt @@ -0,0 +1,8 @@ +package com.github.jaksonlin.pitestintellij.annotations + +data class AnnotationFieldConfig( + val name: String, + val type: AnnotationFieldType, + val required: Boolean = false, + val defaultValue: Any? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt new file mode 100644 index 0000000..6c3afe5 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt @@ -0,0 +1,7 @@ +package com.github.jaksonlin.pitestintellij.annotations + +enum class AnnotationFieldType { + STRING, + STRING_LIST, + STATUS +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt new file mode 100644 index 0000000..f947321 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt @@ -0,0 +1,47 @@ +package com.github.jaksonlin.pitestintellij.annotations + +import com.github.jaksonlin.pitestintellij.context.UnittestCase + + +class AnnotationParser(private val schema: AnnotationSchema) { + private val validator = AnnotationValidator(schema) + + fun parseAnnotation(annotationValues: Map): UnittestCase { + when (val result = validator.validate(annotationValues)) { + is AnnotationValidator.ValidationResult.Valid -> { + val parsedValues = schema.fields.associate { field -> + if (field.required) { + if (!annotationValues.containsKey(field.name)) { + throw IllegalArgumentException("Missing required field: ${field.name}") + } + if (annotationValues[field.name] == null) { + throw IllegalArgumentException("Required field cannot be null: ${field.name}") + } + } else { + if (!annotationValues.containsKey(field.name) || annotationValues[field.name] == null) { + return@associate field.name to field.defaultValue + } + } + val rawValue = annotationValues[field.name] + field.name to convertValue(rawValue, field) + } + return UnittestCase(parsedValues) + } + is AnnotationValidator.ValidationResult.Invalid -> { + throw IllegalArgumentException( + "Invalid annotation values:\n${result.errors.joinToString("\n")}" + ) + } + } + } + + private fun convertValue(value: Any?, field: AnnotationFieldConfig): Any? { + if (value == null) return field.defaultValue + + return when (field.type) { + AnnotationFieldType.STRING -> value as? String ?: field.defaultValue + AnnotationFieldType.STRING_LIST -> (value as? List<*>)?.mapNotNull { it as? String } ?: emptyList() + AnnotationFieldType.STATUS -> (value as? String)?.uppercase() ?: field.defaultValue + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt new file mode 100644 index 0000000..e3612a1 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -0,0 +1,73 @@ +package com.github.jaksonlin.pitestintellij.annotations + + +data class AnnotationSchema( + val annotationClassName: String, + val fields: List +) { + companion object { + // Default schema matching UnittestCaseInfoContext + val DEFAULT_SCHEMA = """ + { + "annotationClassName": "UnittestCaseInfo", + "fields": [ + { + "name": "author", + "type": "STRING", + "required": true + }, + { + "name": "title", + "type": "STRING", + "required": true + }, + { + "name": "targetClass", + "type": "STRING", + "required": true + }, + { + "name": "targetMethod", + "type": "STRING", + "required": true + }, + { + "name": "testPoints", + "type": "STRING_LIST", + "required": false, + "defaultValue": [] + }, + { + "name": "status", + "type": "STATUS", + "required": false, + "defaultValue": "TODO" + }, + { + "name": "description", + "type": "STRING", + "required": false + }, + { + "name": "tags", + "type": "STRING_LIST", + "required": false, + "defaultValue": [] + }, + { + "name": "relatedRequirements", + "type": "STRING_LIST", + "required": false, + "defaultValue": [] + }, + { + "name": "relatedDefects", + "type": "STRING_LIST", + "required": false, + "defaultValue": [] + } + ] + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt new file mode 100644 index 0000000..08c9a98 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt @@ -0,0 +1,63 @@ +package com.github.jaksonlin.pitestintellij.annotations + +class AnnotationValidator(private val schema: AnnotationSchema) { + sealed class ValidationResult { + object Valid : ValidationResult() + data class Invalid(val errors: List) : ValidationResult() + } + + fun validate(annotationValues: Map): ValidationResult { + val errors = mutableListOf() + + // Check required fields + schema.fields + .filter { it.required } + .forEach { field -> + if (!annotationValues.containsKey(field.name) || annotationValues[field.name] == null) { + errors.add("Missing required field: ${field.name}") + } + } + + // Validate field types + annotationValues.forEach { (name, value) -> + schema.fields.find { it.name == name }?.let { field -> + validateFieldType(field, value)?.let { error -> + errors.add(error) + } + } + } + + return if (errors.isEmpty()) ValidationResult.Valid else ValidationResult.Invalid(errors) + } + + private fun validateFieldType(field: AnnotationFieldConfig, value: Any?): String? { + if (value == null) return null + + return when (field.type) { + AnnotationFieldType.STRING -> { + if (value !is String) "Field ${field.name} must be a String" + else null + } + AnnotationFieldType.STRING_LIST -> { + when (value) { + is List<*> -> { + if (value.any { it !is String }) { + "Field ${field.name} must be a list of Strings" + } else null + } + else -> "Field ${field.name} must be a List" + } + } + AnnotationFieldType.STATUS -> { + if (value !is String) { + "Field ${field.name} must be a String" + } else { + val validStatus = setOf("TODO", "IN_PROGRESS", "DONE", "DEPRECATED", "BROKEN") // Add your valid statuses + if (!validStatus.contains(value.uppercase())) { + "Invalid status value for ${field.name}: $value" + } else null + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/UnittestAnnotationConfig.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/UnittestAnnotationConfig.kt new file mode 100644 index 0000000..cc2ef03 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/UnittestAnnotationConfig.kt @@ -0,0 +1,14 @@ +package com.github.jaksonlin.pitestintellij.annotations + +data class UnittestAnnotationConfig( + val authorField: String = "author", + val titleField: String = "title", + val targetClassField: String = "targetClass", + val targetMethodField: String = "targetMethod", + val testPointsField: String = "testPoints", + val statusField: String = "status", + val descriptionField: String = "description", + val tagsField: String = "tags", + val relatedRequirementsField: String = "relatedRequirements", + val relatedDefectsField: String = "relatedDefects" +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt new file mode 100644 index 0000000..39887aa --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt @@ -0,0 +1,69 @@ +package com.github.jaksonlin.pitestintellij.commands.casecheck + +import com.github.jaksonlin.pitestintellij.annotations.AnnotationFieldType +import com.github.jaksonlin.pitestintellij.annotations.AnnotationParser +import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.github.jaksonlin.pitestintellij.context.UnittestCase +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiArrayInitializerMemberValue +import com.intellij.psi.PsiMethod + +class CheckAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { + override fun execute() { + val annotation = findTargetAnnotation(context.psiMethod, context.schema) + if (annotation == null) { + showNoAnnotationMessage(project, context.schema.annotationClassName) + return + } + processAnnotation(annotation, context.parser) + return + } + + + + private fun processAnnotation( + annotation: PsiAnnotation, + parser: AnnotationParser + ) { + try { + val annotationValues = parseAnnotationValues(annotation) + val testCase = parser.parseAnnotation(annotationValues) + val message = formatTestCaseMessage(testCase, context.schema) + showSuccessMessage(project, message) + } catch (e: Exception) { + showErrorMessage(project, e.message ?: "Unknown error") + } + } + + private fun parseAnnotationValues(annotation: PsiAnnotation): Map { + return annotation.parameterList.attributes.associate { attr -> + attr.attributeName to when (val value = attr.value) { + is PsiArrayInitializerMemberValue -> + value.initializers.map { it.text.trim('"') } + else -> value?.text?.trim('"') ?: "" + } + } + } + + private fun formatTestCaseMessage( + testCase: UnittestCase, + schema: AnnotationSchema + ): String = buildString { + appendLine("Test Case Details:") + schema.fields.forEach { field -> + append(field.name) + append(": ") + when (field.type) { + AnnotationFieldType.STRING -> + appendLine(testCase.getString(field.name)) + AnnotationFieldType.STRING_LIST -> + appendLine(testCase.getStringList(field.name).joinToString(", ")) + AnnotationFieldType.STATUS -> + appendLine(testCase.getStatus(field.name)) + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckMethodDataCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckMethodDataCommand.kt new file mode 100644 index 0000000..3e27ca6 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckMethodDataCommand.kt @@ -0,0 +1,30 @@ +package com.github.jaksonlin.pitestintellij.commands.casecheck + +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil + +class CheckMethodDataCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { + override fun execute() { + val annotation = findTargetAnnotation(context.psiMethod, context.schema) + if (annotation == null) { + showNoAnnotationMessage(project, context.schema.annotationClassName) + return + } + val comments = extractCommentsFromMethodBody(context.psiMethod) + if (comments.isEmpty()) { + showErrorMessage(project, "No comments found in the method body") + return + } + } + + private fun extractCommentsFromMethodBody(psiMethod: PsiMethod): List { + val comments = mutableListOf() + PsiTreeUtil.findChildrenOfType(psiMethod, PsiComment::class.java).forEach { + comments.add(it.text) + } + return comments + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt new file mode 100644 index 0000000..b0cf55e --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt @@ -0,0 +1,76 @@ +package com.github.jaksonlin.pitestintellij.commands.casecheck + +import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.github.jaksonlin.pitestintellij.context.UnittestCaseInfoContext +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiArrayInitializerMemberValue +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiMethod + +abstract class UnittestCaseCheckCommand(protected val project: Project, protected val context: CaseCheckContext) { + abstract fun execute() + fun showSuccessMessage(project: Project, message: String) { + Messages.showMessageDialog( + project, + message, + "Test Annotation Details", + Messages.getInformationIcon() + ) + } + + fun showErrorMessage(project: Project, message: String) { + Messages.showMessageDialog( + project, + "Error parsing annotation: $message", + "Test File Action", + Messages.getErrorIcon() + ) + } + + fun showNoAnnotationMessage(project: Project, annotationName: String) { + Messages.showMessageDialog( + project, + "No $annotationName annotation found on this method", + "Test File Action", + Messages.getWarningIcon() + ) + } + + fun findTargetAnnotation( + psiMethod: PsiMethod, + schema: AnnotationSchema + ): PsiAnnotation? { + return psiMethod.annotations.find { annotation -> + annotation.qualifiedName?.contains(schema.annotationClassName) == true + } + } + + fun extractStringArrayValue(annotation: PsiAnnotation, attributeName: String): List { + val attributeValue = annotation.findAttributeValue(attributeName) + return if (attributeValue is PsiArrayInitializerMemberValue) { + // retrieve the array element into the tags + //attributeValue.initializers.map { it.text } + attributeValue.initializers.mapNotNull { (it as? PsiLiteralExpression)?.value as? String } + } else { + emptyList() + } + } + + fun showValidationErrors(project: Project, errors: List) { + val message = buildString { + appendLine("Annotation validation failed:") + errors.forEach { error -> + appendLine("- $error") + } + } + Messages.showMessageDialog( + project, + message, + "Validation Errors", + Messages.getErrorIcon() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/BuildPitestCommandCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/BuildPitestCommandCommand.kt similarity index 96% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/BuildPitestCommandCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/BuildPitestCommandCommand.kt index 9b2d271..969a7a3 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/BuildPitestCommandCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/BuildPitestCommandCommand.kt @@ -1,6 +1,5 @@ -package com.github.jaksonlin.pitestintellij.commands +package com.github.jaksonlin.pitestintellij.commands.pitest -import PitestCommand import com.github.jaksonlin.pitestintellij.context.PitestContext import com.intellij.openapi.project.Project diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/HandlePitestResultCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/HandlePitestResultCommand.kt similarity index 95% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/HandlePitestResultCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/HandlePitestResultCommand.kt index a00b743..de19bbe 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/HandlePitestResultCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/HandlePitestResultCommand.kt @@ -1,6 +1,5 @@ -package com.github.jaksonlin.pitestintellij.commands +package com.github.jaksonlin.pitestintellij.commands.pitest -import PitestCommand import com.github.jaksonlin.pitestintellij.context.PitestContext import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/PitestCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PitestCommand.kt similarity index 97% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/PitestCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PitestCommand.kt index bf3efdd..ea7d341 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/PitestCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PitestCommand.kt @@ -1,3 +1,5 @@ +package com.github.jaksonlin.pitestintellij.commands.pitest + import com.github.jaksonlin.pitestintellij.commands.CommandCancellationException import com.github.jaksonlin.pitestintellij.ui.PitestOutputDialog import com.intellij.openapi.application.ApplicationManager @@ -5,7 +7,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.github.jaksonlin.pitestintellij.context.PitestContext import com.github.jaksonlin.pitestintellij.context.dumpPitestContext -import com.github.jaksonlin.pitestintellij.services.PitestService import com.github.jaksonlin.pitestintellij.services.RunHistoryManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.components.service diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/PrepareEnvironmentCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PrepareEnvironmentCommand.kt similarity index 98% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/PrepareEnvironmentCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PrepareEnvironmentCommand.kt index a7aad0f..1c8eb22 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/PrepareEnvironmentCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PrepareEnvironmentCommand.kt @@ -1,6 +1,6 @@ -package com.github.jaksonlin.pitestintellij.commands +package com.github.jaksonlin.pitestintellij.commands.pitest -import PitestCommand +import com.github.jaksonlin.pitestintellij.commands.CommandCancellationException import com.github.jaksonlin.pitestintellij.context.PitestContext import com.github.jaksonlin.pitestintellij.util.FileUtils import com.github.jaksonlin.pitestintellij.util.GradleUtils diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/RunPitestCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/RunPitestCommand.kt similarity index 91% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/RunPitestCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/RunPitestCommand.kt index ff6dc0e..645e2a7 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/RunPitestCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/RunPitestCommand.kt @@ -1,6 +1,5 @@ -package com.github.jaksonlin.pitestintellij.commands +package com.github.jaksonlin.pitestintellij.commands.pitest -import PitestCommand import com.github.jaksonlin.pitestintellij.context.PitestContext import com.github.jaksonlin.pitestintellij.util.ProcessExecutor import com.intellij.openapi.diagnostic.thisLogger diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/StoreHistoryCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/StoreHistoryCommand.kt similarity index 67% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/StoreHistoryCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/StoreHistoryCommand.kt index 857ff19..997604a 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/StoreHistoryCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/StoreHistoryCommand.kt @@ -1,8 +1,6 @@ -package com.github.jaksonlin.pitestintellij.commands +package com.github.jaksonlin.pitestintellij.commands.pitest -import PitestCommand import com.github.jaksonlin.pitestintellij.context.PitestContext -import com.github.jaksonlin.pitestintellij.services.RunHistoryManager import com.intellij.openapi.project.Project class StoreHistoryCommand (project: Project, context: PitestContext) : PitestCommand(project, context) { diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt new file mode 100644 index 0000000..1286a15 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt @@ -0,0 +1,29 @@ +package com.github.jaksonlin.pitestintellij.context + +import AnnotationConfigService +import com.github.jaksonlin.pitestintellij.actions.RunCaseAnnoationCheckAction +import com.github.jaksonlin.pitestintellij.annotations.AnnotationParser +import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiMethod + +data class CaseCheckContext( + val psiMethod: PsiMethod, + val schema: AnnotationSchema, + val parser: AnnotationParser +) { + companion object { + fun create(psiMethod: PsiMethod): CaseCheckContext? { + val configService = service() + val schema = configService.getSchema() + return CaseCheckContext( + psiMethod = psiMethod, + schema = schema, + parser = AnnotationParser(schema) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/TestPoints.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/TestPoints.kt new file mode 100644 index 0000000..81e2eac --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/TestPoints.kt @@ -0,0 +1,32 @@ +package com.github.jaksonlin.pitestintellij.context + + +object TestPoints { + const val NONE_EMPTY_CHECK = "NonEmptyCheck" + const val BOUNDARY_VALUE_CHECK = "BoundaryValueCheck" + const val ERROR_HANDLING = "ErrorHandling" + const val INPUT_VALIDATION = "InputValidation" + const val POSITIVE_BRANCH = "PositiveBranch" + const val NEGATIVE_BRANCH = "NegativeBranch" + const val FUNCTIONALITY_CHECK = "FunctionalityCheck" + const val BUSINESS_LOGIC_VALIDATION = "BusinessLogicValidation" + const val BUSINESS_INPUT_CHECK = "BusinessInputOutputCheck" + const val SIDE_EFFECT_CHECK = "SideEffectsCheck" + const val STATE_TRANSITION_CHECK = "StateTransitionCheck" + const val BUSINESS_CALCULATION_CHECK = "BusinessCalculationCheck" + + val builtinTestPoints = listOf( + NONE_EMPTY_CHECK, + BOUNDARY_VALUE_CHECK, + ERROR_HANDLING, + INPUT_VALIDATION, + POSITIVE_BRANCH, + NEGATIVE_BRANCH, + FUNCTIONALITY_CHECK, + BUSINESS_LOGIC_VALIDATION, + BUSINESS_INPUT_CHECK, + SIDE_EFFECT_CHECK, + STATE_TRANSITION_CHECK, + BUSINESS_CALCULATION_CHECK + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCase.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCase.kt new file mode 100644 index 0000000..72a0a9a --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCase.kt @@ -0,0 +1,9 @@ +package com.github.jaksonlin.pitestintellij.context + +data class UnittestCase( + val values: Map +) { + fun getString(key: String): String = values[key] as? String ?: "" + fun getStringList(key: String): List = values[key] as? List ?: emptyList() + fun getStatus(key: String): String = values[key] as? String ?: "TODO" +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseInfoContext.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseInfoContext.kt new file mode 100644 index 0000000..b323aee --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseInfoContext.kt @@ -0,0 +1,20 @@ +package com.github.jaksonlin.pitestintellij.context + +import com.github.jaksonlin.pitestintellij.annotations.UnittestAnnotationConfig + +data class UnittestCaseInfoContext( + val annotationValues: Map, + private val config: UnittestAnnotationConfig = UnittestAnnotationConfig() +) { + val author: String get() = annotationValues[config.authorField] as? String ?: "" + val title: String get() = annotationValues[config.titleField] as? String ?: "" + val targetClass: String get() = annotationValues[config.targetClassField] as? String ?: "" + val targetMethod: String get() = annotationValues[config.targetMethodField] as? String ?: "" + val testPoints: List get() = (annotationValues[config.testPointsField] as? List<*>)?.mapNotNull { it as? String } ?: emptyList() + var status: UnittestCaseStatus = UnittestCaseStatus.TODO + var description: String = annotationValues[config.descriptionField] as? String ?: "" + var tags: List = (annotationValues[config.tagsField] as? List<*>)?.mapNotNull { it as? String } ?: emptyList() + var relatedRequirements: List = (annotationValues[config.relatedRequirementsField] as? List<*>)?.mapNotNull { it as? String } ?: emptyList() + var relatedDefects: List = (annotationValues[config.relatedDefectsField] as? List<*>)?.mapNotNull { it as? String } ?: emptyList() +} + diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseStatus.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseStatus.kt new file mode 100644 index 0000000..6663865 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestCaseStatus.kt @@ -0,0 +1,5 @@ +package com.github.jaksonlin.pitestintellij.context + +enum class UnittestCaseStatus { + TODO, DONE, BROKEN, DEPRECATED +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestMethodContext.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestMethodContext.kt new file mode 100644 index 0000000..eb30fef --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/UnittestMethodContext.kt @@ -0,0 +1,8 @@ +package com.github.jaksonlin.pitestintellij.context + +data class UnittestMethodContext ( + + val methodName: String, + val comments: List + +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/TestPointProcessor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/TestPointProcessor.kt new file mode 100644 index 0000000..4ccf655 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/TestPointProcessor.kt @@ -0,0 +1,24 @@ +package com.github.jaksonlin.pitestintellij.processor + +import com.github.jaksonlin.pitestintellij.context.TestPoints +import com.intellij.ide.util.PropertiesComponent + + +class TestPointProcessor { + companion object { + private var validTestPoints = TestPoints.builtinTestPoints + + fun isValidTestPoint(testPoint: String): Boolean = validTestPoints.contains(testPoint) + + fun loadCustomizedTestPoints() { + val propertiesComponent = PropertiesComponent.getInstance() + val defaultTestPoints = propertiesComponent.getValue( + "defaultTestPoints", + TestPoints.builtinTestPoints.joinToString(",") + ) + validTestPoints = defaultTestPoints.split(",") + } + + fun getValidTestPoints(): List = validTestPoints + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt new file mode 100644 index 0000000..b4fe41a --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt @@ -0,0 +1,52 @@ +package com.github.jaksonlin.pitestintellij.processor + +import com.github.jaksonlin.pitestintellij.context.UnittestCaseInfoContext +import com.github.jaksonlin.pitestintellij.context.UnittestCaseStatus +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiArrayInitializerMemberValue +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiReferenceExpression + +class UnittestCaseInfoProcessor { + companion object { + fun fromPsiAnnotation(annotation: PsiAnnotation): UnittestCaseInfoContext { + val annotationResult = UnittestCaseInfoContext( + author = annotation.findAttributeValue("author")?.text ?: "", + title = annotation.findAttributeValue("title")?.text ?: "", + targetClass = annotation.findAttributeValue("targetClass")?.text ?: "", + targetMethod = annotation.findAttributeValue("targetMethod")?.text ?: "", + testPoints = extractStringArrayValue(annotation, "testPoints") + ) + + // process tags + annotationResult.tags = extractStringArrayValue(annotation, "tags") + // process relatedRequirement + annotationResult.relatedRequirements = extractStringArrayValue(annotation, "relatedRequirements") + // process relatedDefect + annotationResult.relatedDefects = extractStringArrayValue(annotation, "relatedDefects") + + // process status enum + val statusValue = annotation.findAttributeValue("status") + if (statusValue is PsiReferenceExpression) { + annotationResult.status = UnittestCaseStatus.valueOf(statusValue.text.substringAfterLast(".")) + } + + // process description + val descriptionValue = annotation.findAttributeValue("description")?.text + if (descriptionValue != null) { + annotationResult.description = descriptionValue + } + + return annotationResult + } + + private fun extractStringArrayValue(annotation: PsiAnnotation, attributeName: String): List { + val attributeValue = annotation.findAttributeValue(attributeName) + return if (attributeValue is PsiArrayInitializerMemberValue) { + attributeValue.initializers.mapNotNull { (it as? PsiLiteralExpression)?.value as? String } + } else { + emptyList() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestMethodProcessor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestMethodProcessor.kt new file mode 100644 index 0000000..3196023 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestMethodProcessor.kt @@ -0,0 +1,23 @@ +package com.github.jaksonlin.pitestintellij.processor + +import com.github.jaksonlin.pitestintellij.context.UnittestMethodContext +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil + +class UnittestMethodProcessor { + companion object { + fun fromPsiMethod(psiMethod: PsiMethod): UnittestMethodContext { + val comments = extractCommentsFromMethodBody(psiMethod) + return UnittestMethodContext(psiMethod.name, comments) + } + + private fun extractCommentsFromMethodBody(psiMethod: PsiMethod): List { + val comments = mutableListOf() + PsiTreeUtil.findChildrenOfType(psiMethod, PsiComment::class.java).forEach { + comments.add(it.text) + } + return comments + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt new file mode 100644 index 0000000..e4ae4d4 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt @@ -0,0 +1,33 @@ +import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Service(Service.Level.APP) +class AnnotationConfigService : PersistentStateComponent { + data class State( + var schemaJson: String = AnnotationSchema.DEFAULT_SCHEMA + ) + + private var myState = State() + + override fun getState(): State = myState + + override fun loadState(state: State) { + myState = state + } + + fun getSchema(): AnnotationSchema { + return try { + Json.decodeFromString(myState.schemaJson) + } catch (e: Exception) { + Json.decodeFromString(AnnotationSchema.DEFAULT_SCHEMA) + } + } + + fun updateSchema(schema: AnnotationSchema) { + myState.schemaJson = Json.encodeToString(schema) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/PitestService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/PitestService.kt index 586383e..ebaa4f9 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/PitestService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/PitestService.kt @@ -1,6 +1,7 @@ package com.github.jaksonlin.pitestintellij.services import com.github.jaksonlin.pitestintellij.commands.* +import com.github.jaksonlin.pitestintellij.commands.pitest.* import com.github.jaksonlin.pitestintellij.context.PitestContext import com.github.jaksonlin.pitestintellij.context.dumpPitestContext import com.intellij.openapi.application.ApplicationManager diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt new file mode 100644 index 0000000..50dc0b2 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt @@ -0,0 +1,86 @@ +package com.github.jaksonlin.pitestintellij.ui + +import AnnotationConfigService +import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema +import com.intellij.json.JsonFileType +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.project.ProjectManager +import com.intellij.ui.EditorTextField +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBScrollPane +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import java.awt.BorderLayout +import javax.swing.JComponent +import javax.swing.JPanel + +class AnnotationConfigurable : Configurable { + private var editor: EditorTextField? = null + private val configService = service() + + override fun getDisplayName(): String = "Test Annotation Configuration" + + override fun createComponent(): JComponent { + val project = ProjectManager.getInstance().defaultProject + val editorFactory = EditorFactory.getInstance() + + editor = EditorTextField( + EditorFactory.getInstance().createDocument(configService.state.schemaJson), + project, + JsonFileType.INSTANCE, + true, + false + ).apply { + setOneLineMode(false) + setPreferredWidth(400) + addSettingsProvider { editor -> + editor.settings.apply { + isLineNumbersShown = true + isWhitespacesShown = true + } + } + } + + return JPanel(BorderLayout()).apply { + add(JBScrollPane(editor), BorderLayout.CENTER) + add(createHelpPanel(), BorderLayout.SOUTH) + } + } + + private fun createHelpPanel(): JComponent { + return JPanel(BorderLayout()).apply { + add( + JBLabel(""" + Define your test annotation schema in JSON format. + Available field types: STRING, STRING_LIST, STATUS + Example schema is shown by default. + """.trimIndent()), BorderLayout.CENTER) + } + } + + override fun isModified(): Boolean { + return editor?.text != configService.state.schemaJson + } + + override fun apply() { + val jsonText = editor?.text ?: return + try { + // Validate JSON format and schema + val schema = Json.decodeFromString(jsonText) + configService.state.schemaJson = jsonText + } catch (e: Exception) { + throw ConfigurationException("Invalid JSON schema: ${e.message}") + } + } + + override fun reset() { + editor?.text = configService.state.schemaJson + } + + override fun disposeUIResources() { + editor = null + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 333a5b9..499bd88 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -36,6 +36,10 @@ + From 58099b6d08436e5072364be47ca7493c04bb23ac Mon Sep 17 00:00:00 2001 From: zhislin Date: Tue, 12 Nov 2024 21:21:19 +0800 Subject: [PATCH 02/24] add annotation rule engines --- build.gradle.kts | 1 + .../annotations/AnnotationFieldConfig.kt | 23 +++++- .../annotations/AnnotationFieldType.kt | 3 + .../annotations/AnnotationParser.kt | 8 +- .../annotations/AnnotationSchema.kt | 44 +++++++--- .../processor/UnittestCaseInfoProcessor.kt | 82 +++++++++---------- .../services/AnnotationConfigService.kt | 6 ++ src/main/resources/META-INF/plugin.xml | 4 + 8 files changed, 117 insertions(+), 54 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 80182d3..30c0b66 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.18.0") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.0") implementation("com.fasterxml.jackson.core:jackson-databind:2.18.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") // Use latest version } // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt index 385f7a1..9bfd8cc 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt @@ -1,8 +1,29 @@ package com.github.jaksonlin.pitestintellij.annotations +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed class DefaultValue { + @Serializable + @SerialName("StringValue") + data class StringValue(val value: String) : DefaultValue() + + @Serializable + @SerialName("StringListValue") + data class StringListValue(val value: List) : DefaultValue() + + @Serializable + @SerialName("NullValue") + data object NullValue : DefaultValue() +} + + +@Serializable data class AnnotationFieldConfig( val name: String, val type: AnnotationFieldType, val required: Boolean = false, - val defaultValue: Any? = null + val defaultValue: DefaultValue = DefaultValue.NullValue ) \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt index 6c3afe5..89d4244 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt @@ -1,5 +1,8 @@ package com.github.jaksonlin.pitestintellij.annotations +import kotlinx.serialization.Serializable + +@Serializable enum class AnnotationFieldType { STRING, STRING_LIST, diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt index f947321..b5b996f 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt @@ -36,7 +36,13 @@ class AnnotationParser(private val schema: AnnotationSchema) { } private fun convertValue(value: Any?, field: AnnotationFieldConfig): Any? { - if (value == null) return field.defaultValue + if (value == null) { + return when (val defaultValue = field.defaultValue) { + is DefaultValue.StringValue -> defaultValue.value + is DefaultValue.StringListValue -> defaultValue.value + DefaultValue.NullValue -> null + } + } return when (field.type) { AnnotationFieldType.STRING -> value as? String ?: field.defaultValue diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index e3612a1..4387439 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -1,6 +1,8 @@ package com.github.jaksonlin.pitestintellij.annotations +import kotlinx.serialization.Serializable +@Serializable data class AnnotationSchema( val annotationClassName: String, val fields: List @@ -9,62 +11,82 @@ data class AnnotationSchema( // Default schema matching UnittestCaseInfoContext val DEFAULT_SCHEMA = """ { - "annotationClassName": "UnittestCaseInfo", + "annotationClassName": "UnitTest", "fields": [ { "name": "author", "type": "STRING", - "required": true + "required": false, + "defaultValue": {"type": "NullValue"} }, { "name": "title", "type": "STRING", - "required": true + "required": true, + "defaultValue": {"type": "NullValue"} }, { "name": "targetClass", "type": "STRING", - "required": true + "required": true, + "defaultValue": {"type": "NullValue"} }, { "name": "targetMethod", "type": "STRING", - "required": true + "required": true, + "defaultValue": {"type": "NullValue"} }, { "name": "testPoints", "type": "STRING_LIST", "required": false, - "defaultValue": [] + "defaultValue": { + "type": "StringListValue", + "value": [] + } }, { "name": "status", "type": "STATUS", "required": false, - "defaultValue": "TODO" + "defaultValue": { + "type": "StringValue", + "value": "TODO" + } }, { "name": "description", "type": "STRING", - "required": false + "required": false, + "defaultValue": {"type": "NullValue"} }, { "name": "tags", "type": "STRING_LIST", "required": false, - "defaultValue": [] + "defaultValue": { + "type": "StringListValue", + "value": [] + } }, { "name": "relatedRequirements", "type": "STRING_LIST", "required": false, - "defaultValue": [] + "defaultValue": { + "type": "StringListValue", + "value": [] + } }, { "name": "relatedDefects", "type": "STRING_LIST", "required": false, - "defaultValue": [] + "defaultValue": { + "type": "StringListValue", + "value": [] + } } ] } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt index b4fe41a..2ccf483 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/processor/UnittestCaseInfoProcessor.kt @@ -8,45 +8,45 @@ import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiReferenceExpression class UnittestCaseInfoProcessor { - companion object { - fun fromPsiAnnotation(annotation: PsiAnnotation): UnittestCaseInfoContext { - val annotationResult = UnittestCaseInfoContext( - author = annotation.findAttributeValue("author")?.text ?: "", - title = annotation.findAttributeValue("title")?.text ?: "", - targetClass = annotation.findAttributeValue("targetClass")?.text ?: "", - targetMethod = annotation.findAttributeValue("targetMethod")?.text ?: "", - testPoints = extractStringArrayValue(annotation, "testPoints") - ) - - // process tags - annotationResult.tags = extractStringArrayValue(annotation, "tags") - // process relatedRequirement - annotationResult.relatedRequirements = extractStringArrayValue(annotation, "relatedRequirements") - // process relatedDefect - annotationResult.relatedDefects = extractStringArrayValue(annotation, "relatedDefects") - - // process status enum - val statusValue = annotation.findAttributeValue("status") - if (statusValue is PsiReferenceExpression) { - annotationResult.status = UnittestCaseStatus.valueOf(statusValue.text.substringAfterLast(".")) - } - - // process description - val descriptionValue = annotation.findAttributeValue("description")?.text - if (descriptionValue != null) { - annotationResult.description = descriptionValue - } - - return annotationResult - } - - private fun extractStringArrayValue(annotation: PsiAnnotation, attributeName: String): List { - val attributeValue = annotation.findAttributeValue(attributeName) - return if (attributeValue is PsiArrayInitializerMemberValue) { - attributeValue.initializers.mapNotNull { (it as? PsiLiteralExpression)?.value as? String } - } else { - emptyList() - } - } - } +// companion object { +// fun fromPsiAnnotation(annotation: PsiAnnotation): UnittestCaseInfoContext { +// val annotationResult = UnittestCaseInfoContext( +// author = annotation.findAttributeValue("author")?.text ?: "", +// title = annotation.findAttributeValue("title")?.text ?: "", +// targetClass = annotation.findAttributeValue("targetClass")?.text ?: "", +// targetMethod = annotation.findAttributeValue("targetMethod")?.text ?: "", +// testPoints = extractStringArrayValue(annotation, "testPoints") +// ) +// +// // process tags +// annotationResult.tags = extractStringArrayValue(annotation, "tags") +// // process relatedRequirement +// annotationResult.relatedRequirements = extractStringArrayValue(annotation, "relatedRequirements") +// // process relatedDefect +// annotationResult.relatedDefects = extractStringArrayValue(annotation, "relatedDefects") +// +// // process status enum +// val statusValue = annotation.findAttributeValue("status") +// if (statusValue is PsiReferenceExpression) { +// annotationResult.status = UnittestCaseStatus.valueOf(statusValue.text.substringAfterLast(".")) +// } +// +// // process description +// val descriptionValue = annotation.findAttributeValue("description")?.text +// if (descriptionValue != null) { +// annotationResult.description = descriptionValue +// } +// +// return annotationResult +// } +// +// private fun extractStringArrayValue(annotation: PsiAnnotation, attributeName: String): List { +// val attributeValue = annotation.findAttributeValue(attributeName) +// return if (attributeValue is PsiArrayInitializerMemberValue) { +// attributeValue.initializers.mapNotNull { (it as? PsiLiteralExpression)?.value as? String } +// } else { +// emptyList() +// } +// } +// } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt index e4ae4d4..6cc59a0 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt @@ -1,11 +1,17 @@ import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @Service(Service.Level.APP) +@State( + name = "AnnotationConfig", + storages = [Storage("pitestAnnotationConfig.xml")] +) class AnnotationConfigService : PersistentStateComponent { data class State( var schemaJson: String = AnnotationSchema.DEFAULT_SCHEMA diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 499bd88..f6aef3a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -62,6 +62,10 @@ id="RunPitestAction" text="Run Mutation Test"> + + From 4ba4bca488f73bde5784e63c30c0d946ccf8d5c0 Mon Sep 17 00:00:00 2001 From: zhislin Date: Tue, 12 Nov 2024 21:42:00 +0800 Subject: [PATCH 03/24] update default schema --- .../jaksonlin/pitestintellij/annotations/AnnotationSchema.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 4387439..5875b1f 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -11,7 +11,7 @@ data class AnnotationSchema( // Default schema matching UnittestCaseInfoContext val DEFAULT_SCHEMA = """ { - "annotationClassName": "UnitTest", + "annotationClassName": "Unittest", "fields": [ { "name": "author", From 04527092f483d73768997d386d92d108bf2e877b Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 13 Nov 2024 16:26:32 +0800 Subject: [PATCH 04/24] add check for annotation --- .../actions/RunCaseAnnoationCheckAction.kt | 6 +- .../RunTestFileAnnoationCheckAction.kt | 42 ++++ .../annotations/AnnotationFieldConfig.kt | 5 +- .../annotations/AnnotationSchema.kt | 32 +++ .../annotations/AnnotationValidator.kt | 88 +++++++- .../annotations/FieldValidation.kt | 11 + .../casecheck/CheckAnnotationCommand.kt | 17 +- .../casecheck/UnittestCaseCheckCommand.kt | 16 ++ .../casecheck/UnittestFileInspectorCommand.kt | 46 ++++ .../context/CaseCheckContext.kt | 2 +- .../inspectors/UnittestInspector.kt | 82 +++++++ .../services/AnnotationConfigService.kt | 5 + .../services/TestClassCacheService.kt | 15 ++ .../ui/AnnotationConfigurable.kt | 28 ++- src/main/resources/META-INF/plugin.xml | 12 +- .../UnittestCaseAnnotationInspection.html | 27 +++ .../annotations/AnnotationValidatorTest.kt | 212 ++++++++++++++++++ .../UnittestInspectorPerformanceTest.kt | 44 ++++ 18 files changed, 655 insertions(+), 35 deletions(-) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestFileInspectorCommand.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/services/TestClassCacheService.kt create mode 100644 src/main/resources/inspectionDescriptions/UnittestCaseAnnotationInspection.html create mode 100644 src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt create mode 100644 src/test/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspectorPerformanceTest.kt diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt index 0e70cd9..992cec6 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt @@ -10,15 +10,13 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.util.PsiTreeUtil class RunCaseAnnoationCheckAction : AnAction() { - override fun actionPerformed(e: AnActionEvent) { + override fun actionPerformed(e: AnActionEvent) { val psiMethod = findMethodAtCaret(e) ?: return - val context = CaseCheckContext.create(psiMethod) ?: return + val context = CaseCheckContext.create(psiMethod) CheckAnnotationCommand(e.project!!, context).execute() } - - private fun findMethodAtCaret(e: AnActionEvent): PsiMethod? { val project = e.project ?: return null val editor = e.dataContext.getData(CommonDataKeys.EDITOR) ?: return null diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt new file mode 100644 index 0000000..50cb9d2 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt @@ -0,0 +1,42 @@ +package com.github.jaksonlin.pitestintellij.actions + +import com.github.jaksonlin.pitestintellij.commands.casecheck.CheckAnnotationCommand +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.JavaRecursiveElementVisitor +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil + +class RunTestFileAnnoationCheckAction: AnAction() { + + override fun actionPerformed(e: AnActionEvent) { + batchCheckAnnotation(e) + } + + private fun batchCheckAnnotation(e: AnActionEvent){ + val psiFile = e.dataContext.getData(CommonDataKeys.PSI_FILE) + val psiJavaFile = psiFile as PsiJavaFile + + psiJavaFile.accept(object : JavaRecursiveElementVisitor() { + override fun visitMethod(method: PsiMethod) { + super.visitMethod(method) + // inspect the method annotations + val annotations = method.annotations + for (annotation in annotations) { + // inspect the annotation + val annotationName = annotation.qualifiedName + if (annotationName!=null && // to support junit 4 & 5, do not use regexp, as it will also match some beforeTest/afterTest annotations + (annotationName == "org.junit.Test" || annotationName == "org.junit.jupiter.api.Test" || annotationName == "Test")) { + val context = CaseCheckContext.create(method) + CheckAnnotationCommand(e.project!!, context).execute() + break + } + } + } + }) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt index 9bfd8cc..23d2cca 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt @@ -1,8 +1,10 @@ package com.github.jaksonlin.pitestintellij.annotations import kotlinx.serialization.Contextual +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator @Serializable sealed class DefaultValue { @@ -25,5 +27,6 @@ data class AnnotationFieldConfig( val name: String, val type: AnnotationFieldType, val required: Boolean = false, - val defaultValue: DefaultValue = DefaultValue.NullValue + val defaultValue: DefaultValue = DefaultValue.NullValue, + val validation: FieldValidation? = null ) \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 5875b1f..2dd47fa 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -44,6 +44,19 @@ data class AnnotationSchema( "defaultValue": { "type": "StringListValue", "value": [] + }, + "validation": { + "validValues": [ + "Boundary Value", + "Equivalence Class", + "Error Handling", + "Performance", + "Security", + "Integration", + "Edge Case" + ], + "allowCustomValues": true, + "minLength": 1 } }, { @@ -53,6 +66,16 @@ data class AnnotationSchema( "defaultValue": { "type": "StringValue", "value": "TODO" + }, + "validation": { + "validValues": [ + "TODO", + "IN_PROGRESS", + "DONE", + "DEPRECATED", + "BROKEN" + ], + "allowCustomValues": false } }, { @@ -68,6 +91,9 @@ data class AnnotationSchema( "defaultValue": { "type": "StringListValue", "value": [] + }, + "validation": { + "minLength": 1 } }, { @@ -77,6 +103,9 @@ data class AnnotationSchema( "defaultValue": { "type": "StringListValue", "value": [] + }, + "validation": { + "minLength": 1 } }, { @@ -86,6 +115,9 @@ data class AnnotationSchema( "defaultValue": { "type": "StringListValue", "value": [] + }, + "validation": { + "minLength": 1 } } ] diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt index 08c9a98..5bd8a43 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt @@ -21,7 +21,7 @@ class AnnotationValidator(private val schema: AnnotationSchema) { // Validate field types annotationValues.forEach { (name, value) -> schema.fields.find { it.name == name }?.let { field -> - validateFieldType(field, value)?.let { error -> + validateField(field, value)?.let { error -> errors.add(error) } } @@ -30,9 +30,37 @@ class AnnotationValidator(private val schema: AnnotationSchema) { return if (errors.isEmpty()) ValidationResult.Valid else ValidationResult.Invalid(errors) } - private fun validateFieldType(field: AnnotationFieldConfig, value: Any?): String? { + private fun validateField(field: AnnotationFieldConfig, value: Any?): String? { if (value == null) return null + // First validate the type + val typeError = validateType(field, value) + if (typeError != null) return typeError + + // Then validate against the validation rules if they exist + field.validation?.let { validation -> + when (value) { + is String -> { + validateStringValue(field.name, value, validation)?.let { return it } + } + is List<*> -> { + // Validate list content + validateListContent(field.name, value, validation)?.let { return it } + + // Validate list length + validateListLength(field.name, value, validation)?.let { return it } + } + + else -> { + return "Unsupported type for field ${field.name}" + } + } + } + + return null + } + + private fun validateType(field: AnnotationFieldConfig, value: Any): String? { return when (field.type) { AnnotationFieldType.STRING -> { if (value !is String) "Field ${field.name} must be a String" @@ -49,15 +77,55 @@ class AnnotationValidator(private val schema: AnnotationSchema) { } } AnnotationFieldType.STATUS -> { - if (value !is String) { - "Field ${field.name} must be a String" - } else { - val validStatus = setOf("TODO", "IN_PROGRESS", "DONE", "DEPRECATED", "BROKEN") // Add your valid statuses - if (!validStatus.contains(value.uppercase())) { - "Invalid status value for ${field.name}: $value" - } else null - } + if (value !is String) "Field ${field.name} must be a String" + else null + } + } + } + + private fun validateStringValue( + fieldName: String, + value: String, + validation: FieldValidation + ): String? { + if (validation.validValues.isNotEmpty() && + !validation.allowCustomValues && + !validation.validValues.contains(value)) { + return "Invalid value for $fieldName: $value. Valid values are: ${validation.validValues.joinToString()}" + } + return null + } + + private fun validateListContent( + fieldName: String, + value: List<*>, + validation: FieldValidation + ): String? { + if (validation.validValues.isNotEmpty() && !validation.allowCustomValues) { + val invalidValues = value.filterIsInstance() + .filter { !validation.validValues.contains(it) } + if (invalidValues.isNotEmpty()) { + return "Invalid values for $fieldName: ${invalidValues.joinToString()}. Valid values are: ${validation.validValues.joinToString()}" + } + } + return null + } + + private fun validateListLength( + fieldName: String, + value: List<*>, + validation: FieldValidation + ): String? { + validation.minLength?.let { min -> + if (value.size < min) { + return "$fieldName must contain at least $min element${if (min > 1) "s" else ""}" + } + } + validation.maxLength?.let { max -> + if (value.size > max) { + return "$fieldName cannot contain more than $max element${if (max > 1) "s" else ""}" } } + return null } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt new file mode 100644 index 0000000..aab1841 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt @@ -0,0 +1,11 @@ +package com.github.jaksonlin.pitestintellij.annotations + +import kotlinx.serialization.Serializable + +@Serializable +data class FieldValidation( + val validValues: List = emptyList(), + val allowCustomValues: Boolean = true, + val minLength: Int? = null, // Added for list length validation + val maxLength: Int? = null // Optional: add max length constraint +) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt index 39887aa..414f6c3 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt @@ -17,19 +17,17 @@ class CheckAnnotationCommand(project: Project, context: CaseCheckContext):Unitt showNoAnnotationMessage(project, context.schema.annotationClassName) return } - processAnnotation(annotation, context.parser) + processAnnotation(annotation) return } private fun processAnnotation( - annotation: PsiAnnotation, - parser: AnnotationParser + annotation: PsiAnnotation ) { try { - val annotationValues = parseAnnotationValues(annotation) - val testCase = parser.parseAnnotation(annotationValues) + val testCase = parseUnittestCaseFromAnnotations(annotation) val message = formatTestCaseMessage(testCase, context.schema) showSuccessMessage(project, message) } catch (e: Exception) { @@ -37,15 +35,6 @@ class CheckAnnotationCommand(project: Project, context: CaseCheckContext):Unitt } } - private fun parseAnnotationValues(annotation: PsiAnnotation): Map { - return annotation.parameterList.attributes.associate { attr -> - attr.attributeName to when (val value = attr.value) { - is PsiArrayInitializerMemberValue -> - value.initializers.map { it.text.trim('"') } - else -> value?.text?.trim('"') ?: "" - } - } - } private fun formatTestCaseMessage( testCase: UnittestCase, diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt index b0cf55e..0067f8a 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt @@ -2,6 +2,7 @@ package com.github.jaksonlin.pitestintellij.commands.casecheck import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.github.jaksonlin.pitestintellij.context.UnittestCase import com.github.jaksonlin.pitestintellij.context.UnittestCaseInfoContext import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages @@ -73,4 +74,19 @@ abstract class UnittestCaseCheckCommand(protected val project: Project, protecte Messages.getErrorIcon() ) } + + fun parseAnnotationValues(annotation: PsiAnnotation): Map { + return annotation.parameterList.attributes.associate { attr -> + attr.attributeName to when (val value = attr.value) { + is PsiArrayInitializerMemberValue -> + value.initializers.map { it.text.trim('"') } + else -> value?.text?.trim('"') ?: "" + } + } + } + + fun parseUnittestCaseFromAnnotations(annotation: PsiAnnotation):UnittestCase{ + val annotationValues = parseAnnotationValues(annotation) + return context.parser.parseAnnotation(annotationValues) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestFileInspectorCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestFileInspectorCommand.kt new file mode 100644 index 0000000..937e751 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestFileInspectorCommand.kt @@ -0,0 +1,46 @@ +package com.github.jaksonlin.pitestintellij.commands.casecheck + +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil + +class UnittestFileInspectorCommand(private val holder: ProblemsHolder, project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { + override fun execute() { + checkAnnotationSchema(context.psiMethod) + checkIfCommentHasStepAndAssert(context.psiMethod) + } + + private fun checkAnnotationSchema(psiMethod:PsiMethod) { + try { + val annotation = findTargetAnnotation(psiMethod, context.schema) + if (annotation == null) { + holder.registerProblem(context.psiMethod, "No annotation found", ProblemHighlightType.WARNING) + return + } + val testCase = parseUnittestCaseFromAnnotations(annotation) + } catch (e: Exception) { + holder.registerProblem(context.psiMethod, e.message ?: "Unknown error", ProblemHighlightType.ERROR) + } + } + + private fun checkIfCommentHasStepAndAssert(psiMethod: PsiMethod) { + var hasStep = false + var hasAssert = false + PsiTreeUtil.findChildrenOfType(psiMethod, PsiComment::class.java).forEach { + //comments.add(it.text) + if (it.text.contains("step", ignoreCase = true)) { + hasStep = true + } + if (it.text.contains("assert", ignoreCase = true)) { + hasAssert = true + } + } + if (!hasStep || !hasAssert) { + holder.registerProblem(psiMethod, "Method should contains both step and assert comment", ProblemHighlightType.ERROR) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt index 1286a15..10f2903 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt @@ -16,7 +16,7 @@ data class CaseCheckContext( val parser: AnnotationParser ) { companion object { - fun create(psiMethod: PsiMethod): CaseCheckContext? { + fun create(psiMethod: PsiMethod): CaseCheckContext { val configService = service() val schema = configService.getSchema() return CaseCheckContext( diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt new file mode 100644 index 0000000..11e7b35 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt @@ -0,0 +1,82 @@ +package com.github.jaksonlin.pitestintellij.inspectors + +import com.github.jaksonlin.pitestintellij.commands.casecheck.CheckAnnotationCommand +import com.github.jaksonlin.pitestintellij.commands.casecheck.UnittestFileInspectorCommand +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.intellij.codeInspection.* +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import java.util.concurrent.ConcurrentHashMap + +class UnittestInspector : AbstractBaseJavaLocalInspectionTool() { + // Cache test annotation qualified names for faster lookup + private val testAnnotations = setOf( + "org.junit.Test", + "org.junit.jupiter.api.Test", + "Test" + ) + + private val testClassAnnotations = setOf( + "org.junit.runner.RunWith", + "org.junit.jupiter.api.TestInstance", + "org.junit.platform.suite.api.Suite" + ) + + // Cache test class status to avoid repeated checks + private val testClassCache = ConcurrentHashMap() + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + val project = holder.project + return object : JavaElementVisitor() { + override fun visitMethod(psiMethod: PsiMethod) { + // Quick check for test annotation before doing anything else + if (!hasTestAnnotation(psiMethod)) { + return + } + + // Check containing class using cache + val containingClass = psiMethod.containingClass ?: return + val qualifiedName = containingClass.qualifiedName ?: return + + if (!isTestClass(containingClass, qualifiedName)) { + return + } + + val context = CaseCheckContext.create(psiMethod) + UnittestFileInspectorCommand(holder, project, context).execute() + } + + private fun hasTestAnnotation(psiMethod: PsiMethod): Boolean { + return psiMethod.annotations.any { annotation -> + annotation.qualifiedName in testAnnotations + } + } + + private fun isTestClass(psiClass: PsiClass, qualifiedName: String): Boolean { + return testClassCache.getOrPut(qualifiedName) { + // First check class name pattern (fastest check) + val className = psiClass.name + if (className != null && ( + className.endsWith("Test") || + className.endsWith("Tests") || + className.endsWith("TestCase") + )) { + return@getOrPut true + } + + // Then check annotations if needed + psiClass.annotations.any { + it.qualifiedName in testClassAnnotations + } + } + } + } + } + + override fun getID(): String = "PitestUnitTest" + + // Clear cache when plugin is unloaded + fun clearCache() { + testClassCache.clear() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt index 6cc59a0..b85b37c 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt @@ -3,6 +3,7 @@ import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage +import com.intellij.openapi.diagnostic.Logger import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -13,6 +14,8 @@ import kotlinx.serialization.json.Json storages = [Storage("pitestAnnotationConfig.xml")] ) class AnnotationConfigService : PersistentStateComponent { + private val LOG = Logger.getInstance(AnnotationConfigService::class.java) + data class State( var schemaJson: String = AnnotationSchema.DEFAULT_SCHEMA ) @@ -22,6 +25,7 @@ class AnnotationConfigService : PersistentStateComponent() + + fun isTestClass(qualifiedName: String, compute: () -> Boolean): Boolean { + return testClassCache.getOrPut(qualifiedName, compute) + } + + fun clearCache() { + testClassCache.clear() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt index 50dc0b2..51ef51b 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/AnnotationConfigurable.kt @@ -10,10 +10,14 @@ import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.project.ProjectManager import com.intellij.ui.EditorTextField import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.panels.VerticalLayout import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import java.awt.BorderLayout +import java.awt.FlowLayout +import javax.swing.JButton import javax.swing.JComponent import javax.swing.JPanel @@ -31,7 +35,7 @@ class AnnotationConfigurable : Configurable { EditorFactory.getInstance().createDocument(configService.state.schemaJson), project, JsonFileType.INSTANCE, - true, + false, false ).apply { setOneLineMode(false) @@ -40,14 +44,30 @@ class AnnotationConfigurable : Configurable { editor.settings.apply { isLineNumbersShown = true isWhitespacesShown = true + isUseSoftWraps = true } } } - return JPanel(BorderLayout()).apply { - add(JBScrollPane(editor), BorderLayout.CENTER) - add(createHelpPanel(), BorderLayout.SOUTH) + val mainPanel = JBPanel>(VerticalLayout(10)) + + // Add editor with scroll pane + mainPanel.add(JBScrollPane(editor)) + + // Add buttons panel + val buttonsPanel = JPanel(FlowLayout(FlowLayout.LEFT)) + val restoreDefaultsButton = JButton("Restore Defaults").apply { + addActionListener { + editor?.text = AnnotationSchema.DEFAULT_SCHEMA + } } + buttonsPanel.add(restoreDefaultsButton) + mainPanel.add(buttonsPanel) + + // Add help panel + mainPanel.add(createHelpPanel()) + + return mainPanel } private fun createHelpPanel(): JComponent { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f6aef3a..66dc86a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -40,6 +40,16 @@ instance="com.github.jaksonlin.pitestintellij.ui.AnnotationConfigurable" id="com.github.jaksonlin.pitestintellij.settings" displayName="Unittest Annotation Configuration"/> + + @@ -64,7 +74,7 @@ + text="Run Current Method Annotation Check"> diff --git a/src/main/resources/inspectionDescriptions/UnittestCaseAnnotationInspection.html b/src/main/resources/inspectionDescriptions/UnittestCaseAnnotationInspection.html new file mode 100644 index 0000000..5365a23 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/UnittestCaseAnnotationInspection.html @@ -0,0 +1,27 @@ + + +Write your description here. +Start the description with a verb in 3rd person singular, like reports, detects, highlights. +In the first sentence, briefly explain what exactly the inspection helps you detect. +Make sure the sentence is not very long and complicated. +

+ The first sentence must be in a dedicated paragraph separated from the rest of the text. This will make the + description easier to read. + Make sure the description doesn’t just repeat the inspection title. +

+

+ See https://plugins.jetbrains.com/docs/intellij/inspections.html#descriptions for more information. +

+

+ Embed code snippets: +

+

+// automatically highlighted according to inspection registration 'language' attribute
+
+ +

Text after this comment will only be shown in the settings of the inspection.

+ +

To open related settings directly from the description, add a link with `settings://$` optionally followed by `?$` to + pre-select a UI element.

+ + \ No newline at end of file diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt new file mode 100644 index 0000000..a2a13b0 --- /dev/null +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt @@ -0,0 +1,212 @@ +package com.github.jaksonlin.pitestintellij.annotations + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.runner.RunWith + +@RunWith(Enclosed::class) +class AnnotationValidatorTest { + + class ListLengthValidation { + @Test + fun `should validate minimum list length`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "testPoints", + type = AnnotationFieldType.STRING_LIST, + validation = FieldValidation( + minLength = 2 + ) + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test invalid case (too few elements) + val resultInvalid = validator.validate(mapOf( + "testPoints" to listOf("point1") + )) + assertTrue(resultInvalid is AnnotationValidator.ValidationResult.Invalid) + assertEquals( + "testPoints must contain at least 2 elements", + (resultInvalid as AnnotationValidator.ValidationResult.Invalid).errors.first() + ) + + // Test valid case + val resultValid = validator.validate(mapOf( + "testPoints" to listOf("point1", "point2") + )) + assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) + } + + @Test + fun `should validate maximum list length`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "tags", + type = AnnotationFieldType.STRING_LIST, + validation = FieldValidation( + maxLength = 3 + ) + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test invalid case (too many elements) + val resultInvalid = validator.validate(mapOf( + "tags" to listOf("tag1", "tag2", "tag3", "tag4") + )) + assertTrue(resultInvalid is AnnotationValidator.ValidationResult.Invalid) + assertEquals( + "tags cannot contain more than 3 elements", + (resultInvalid as AnnotationValidator.ValidationResult.Invalid).errors.first() + ) + + // Test valid case + val resultValid = validator.validate(mapOf( + "tags" to listOf("tag1", "tag2", "tag3") + )) + assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) + } + } + + class ValidValuesValidation { + @Test + fun `should validate string list allowed values when custom values not allowed`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "testPoints", + type = AnnotationFieldType.STRING_LIST, + validation = FieldValidation( + validValues = listOf("Valid1", "Valid2", "Valid3"), + allowCustomValues = false + ) + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test invalid case (contains invalid value) + val resultInvalid = validator.validate(mapOf( + "testPoints" to listOf("Valid1", "Invalid") + )) + assertTrue(resultInvalid is AnnotationValidator.ValidationResult.Invalid) + assertTrue( + (resultInvalid as AnnotationValidator.ValidationResult.Invalid) + .errors.first() + .contains("Invalid values for testPoints: Invalid") + ) + + // Test valid case + val resultValid = validator.validate(mapOf( + "testPoints" to listOf("Valid1", "Valid2") + )) + assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) + } + + @Test + fun `should allow custom values when configured`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "testPoints", + type = AnnotationFieldType.STRING_LIST, + validation = FieldValidation( + validValues = listOf("Valid1", "Valid2"), + allowCustomValues = true + ) + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test with custom value (should be valid) + val result = validator.validate(mapOf( + "testPoints" to listOf("Valid1", "CustomValue") + )) + assertTrue(result is AnnotationValidator.ValidationResult.Valid) + } + } + + class RequiredFieldValidation { + @Test + fun `should validate required fields`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "required", + type = AnnotationFieldType.STRING, + required = true + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test missing required field + val resultMissing = validator.validate(mapOf()) + assertTrue(resultMissing is AnnotationValidator.ValidationResult.Invalid) + assertEquals( + "Missing required field: required", + (resultMissing as AnnotationValidator.ValidationResult.Invalid).errors.first() + ) + + // Test with required field + val resultValid = validator.validate(mapOf( + "required" to "value" + )) + assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) + } + } + + class TypeValidation { + @Test + fun `should validate field types`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "stringField", + type = AnnotationFieldType.STRING + ), + AnnotationFieldConfig( + name = "listField", + type = AnnotationFieldType.STRING_LIST + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test invalid types + val resultInvalid = validator.validate(mapOf( + "stringField" to listOf("wrong type"), + "listField" to "wrong type" + )) + assertTrue(resultInvalid is AnnotationValidator.ValidationResult.Invalid) + assertEquals(2, (resultInvalid as AnnotationValidator.ValidationResult.Invalid).errors.size) + + // Test valid types + val resultValid = validator.validate(mapOf( + "stringField" to "correct", + "listField" to listOf("correct") + )) + assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspectorPerformanceTest.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspectorPerformanceTest.kt new file mode 100644 index 0000000..3d9b16e --- /dev/null +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspectorPerformanceTest.kt @@ -0,0 +1,44 @@ +package com.github.jaksonlin.pitestintellij.inspectors +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase +import org.junit.Test + +class UnittestInspectorPerformanceTest : LightJavaCodeInsightFixtureTestCase() { + + @Test + fun `test inspection performance`() { + // Create a large test file + val testFileContent = buildString { + append(""" + import org.junit.Test; + + public class LargeTest { + """.trimIndent()) + + // Add 1000 test methods + for (i in 1..1000) { + append(""" + @Test + public void test$i() { + // Some test code + } + """.trimIndent()) + } + + append("}") + } + + myFixture.configureByText("LargeTest.java", testFileContent) + + // Measure performance + val startTime = System.nanoTime() + myFixture.enableInspections(UnittestInspector::class.java) + myFixture.doHighlighting() + val endTime = System.nanoTime() + + val durationMs = (endTime - startTime) / 1_000_000.0 + println("Inspection took $durationMs ms") + + // Assert reasonable performance (adjust threshold as needed) + assertTrue("Inspection took too long: $durationMs ms", durationMs < 3000) + } +} From 7cfa45eec5da57c151d94c65979417479cd7124b Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 13 Nov 2024 17:07:38 +0800 Subject: [PATCH 05/24] add more check on schema rather than in code --- .../annotations/AnnotationSchema.kt | 49 +++++++- .../annotations/AnnotationValidator.kt | 56 ++++++++- .../annotations/FieldValidation.kt | 4 +- .../annotations/ValidationMode.kt | 9 ++ .../annotations/AnnotationValidatorTest.kt | 115 ++++++++++++++++++ 5 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValidationMode.kt diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 2dd47fa..5e91a1b 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -16,8 +16,11 @@ data class AnnotationSchema( { "name": "author", "type": "STRING", - "required": false, - "defaultValue": {"type": "NullValue"} + "required": true, + "defaultValue": {"type": "NullValue"}, + "validation": { + "allowEmpty": false + } }, { "name": "title", @@ -37,10 +40,28 @@ data class AnnotationSchema( "required": true, "defaultValue": {"type": "NullValue"} }, + { + "name": "lastUpdateTime", + "type": "STRING", + "required": true, + "defaultValue": {"type": "NullValue"} + }, + { + "name": "lastUpdateAuthor", + "type": "STRING", + "required": true, + "defaultValue": {"type": "NullValue"} + }, + { + "name": "methodSignature", + "type": "STRING", + "required": true, + "defaultValue": {"type": "NullValue"} + }, { "name": "testPoints", "type": "STRING_LIST", - "required": false, + "required": true, "defaultValue": { "type": "StringListValue", "value": [] @@ -56,13 +77,15 @@ data class AnnotationSchema( "Edge Case" ], "allowCustomValues": true, - "minLength": 1 + "minLength": 1, + "mode": "CONTAINS", + "allowEmpty": false } }, { "name": "status", "type": "STATUS", - "required": false, + "required": true, "defaultValue": { "type": "StringValue", "value": "TODO" @@ -75,7 +98,9 @@ data class AnnotationSchema( "DEPRECATED", "BROKEN" ], - "allowCustomValues": false + "allowCustomValues": false, + "mode": "CONTAINS", + "allowEmpty": false } }, { @@ -108,6 +133,18 @@ data class AnnotationSchema( "minLength": 1 } }, + { + "name": "relatedTestCases", + "type": "STRING_LIST", + "required": false, + "defaultValue": { + "type": "StringListValue", + "value": [] + }, + "validation": { + "minLength": 1 + } + }, { "name": "relatedDefects", "type": "STRING_LIST", diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt index 5bd8a43..9313b53 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt @@ -37,6 +37,10 @@ class AnnotationValidator(private val schema: AnnotationSchema) { val typeError = validateType(field, value) if (typeError != null) return typeError + // Then validate non-empty if required + val emptyError = validateNonEmpty(field, value) + if (emptyError != null) return emptyError + // Then validate against the validation rules if they exist field.validation?.let { validation -> when (value) { @@ -52,7 +56,8 @@ class AnnotationValidator(private val schema: AnnotationSchema) { } else -> { - return "Unsupported type for field ${field.name}" + // Should not happen + return "Unsupported field type for ${field.name}: ${value::class.simpleName}" } } } @@ -60,6 +65,24 @@ class AnnotationValidator(private val schema: AnnotationSchema) { return null } + private fun validateNonEmpty(field: AnnotationFieldConfig, value: Any): String? { + if (!field.validation?.allowEmpty.let { it != null && !it }) { + when (value) { + is String -> { + if (value.isBlank()) { + return "Field ${field.name} cannot be empty" + } + } + is List<*> -> { + if (value.isEmpty()) { + return "Field ${field.name} cannot be empty" + } + } + } + } + return null + } + private fun validateType(field: AnnotationFieldConfig, value: Any): String? { return when (field.type) { AnnotationFieldType.STRING -> { @@ -88,10 +111,19 @@ class AnnotationValidator(private val schema: AnnotationSchema) { value: String, validation: FieldValidation ): String? { - if (validation.validValues.isNotEmpty() && - !validation.allowCustomValues && - !validation.validValues.contains(value)) { - return "Invalid value for $fieldName: $value. Valid values are: ${validation.validValues.joinToString()}" + if (!validation.allowEmpty && value.isBlank()) { + return "Field $fieldName cannot be empty" + } + if (validation.validValues.isNotEmpty() && !validation.allowCustomValues) { + val isValid = when (validation.mode) { + ValidationMode.EXACT -> validation.validValues.contains(value) + ValidationMode.CONTAINS -> validation.validValues.any { candidate -> + value.contains(candidate, ignoreCase = true) + } + } + if (!isValid) { + return "Invalid value for $fieldName: $value. Valid values are: ${validation.validValues.joinToString()}" + } } return null } @@ -103,7 +135,14 @@ class AnnotationValidator(private val schema: AnnotationSchema) { ): String? { if (validation.validValues.isNotEmpty() && !validation.allowCustomValues) { val invalidValues = value.filterIsInstance() - .filter { !validation.validValues.contains(it) } + .filter { item -> + when (validation.mode) { + ValidationMode.EXACT -> !validation.validValues.contains(item) + ValidationMode.CONTAINS -> !validation.validValues.any { candidate -> + item.contains(candidate, ignoreCase = true) + } + } + } if (invalidValues.isNotEmpty()) { return "Invalid values for $fieldName: ${invalidValues.joinToString()}. Valid values are: ${validation.validValues.joinToString()}" } @@ -116,6 +155,11 @@ class AnnotationValidator(private val schema: AnnotationSchema) { value: List<*>, validation: FieldValidation ): String? { + validation.allowEmpty.let { allowEmpty -> + if (!allowEmpty && value.isEmpty()) { + return "Field $fieldName cannot be empty" + } + } validation.minLength?.let { min -> if (value.size < min) { return "$fieldName must contain at least $min element${if (min > 1) "s" else ""}" diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt index aab1841..3b6bd5a 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/FieldValidation.kt @@ -7,5 +7,7 @@ data class FieldValidation( val validValues: List = emptyList(), val allowCustomValues: Boolean = true, val minLength: Int? = null, // Added for list length validation - val maxLength: Int? = null // Optional: add max length constraint + val maxLength: Int? = null, // Optional: add max length constraint + val mode: ValidationMode = ValidationMode.EXACT, + val allowEmpty: Boolean = true // New field to control empty string validation ) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValidationMode.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValidationMode.kt new file mode 100644 index 0000000..2279e56 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValidationMode.kt @@ -0,0 +1,9 @@ +package com.github.jaksonlin.pitestintellij.annotations + +import kotlinx.serialization.Serializable + +@Serializable +enum class ValidationMode { + EXACT, // Exact string match + CONTAINS, // String contains check +} \ No newline at end of file diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt index a2a13b0..95d1745 100644 --- a/src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidatorTest.kt @@ -209,4 +209,119 @@ class AnnotationValidatorTest { assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) } } + + class StringContainsValidation { + @Test + fun `should validate string using contains mode`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "status", + type = AnnotationFieldType.STRING, + validation = FieldValidation( + validValues = listOf("TODO", "DONE"), + allowCustomValues = false, + mode = ValidationMode.CONTAINS + ) + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test valid cases (partial matches) + val resultValid1 = validator.validate(mapOf( + "status" to "TODO_REVIEW" + )) + assertTrue(resultValid1 is AnnotationValidator.ValidationResult.Valid) + + val resultValid2 = validator.validate(mapOf( + "status" to "DONE_WITH_COMMENTS" + )) + assertTrue(resultValid2 is AnnotationValidator.ValidationResult.Valid) + + // Test invalid case + val resultInvalid = validator.validate(mapOf( + "status" to "IN_PROGRESS" + )) + assertTrue(resultInvalid is AnnotationValidator.ValidationResult.Invalid) + } + } + + class EmptyValueValidation { + @Test + fun `should validate non-empty strings when required`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "title", + type = AnnotationFieldType.STRING, + validation = FieldValidation( + allowEmpty = false + ) + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test empty string + val resultEmpty = validator.validate(mapOf( + "title" to "" + )) + assertTrue(resultEmpty is AnnotationValidator.ValidationResult.Invalid) + assertEquals( + "Field title cannot be empty", + (resultEmpty as AnnotationValidator.ValidationResult.Invalid).errors.first() + ) + + // Test blank string + val resultBlank = validator.validate(mapOf( + "title" to " " + )) + assertTrue(resultBlank is AnnotationValidator.ValidationResult.Invalid) + + // Test valid non-empty string + val resultValid = validator.validate(mapOf( + "title" to "Valid Title" + )) + assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) + } + + @Test + fun `should validate non-empty lists when required`() { + val schema = AnnotationSchema( + annotationClassName = "Test", + fields = listOf( + AnnotationFieldConfig( + name = "tags", + type = AnnotationFieldType.STRING_LIST, + validation = FieldValidation( + allowEmpty = false + ) + ) + ) + ) + + val validator = AnnotationValidator(schema) + + // Test empty list + val resultEmpty = validator.validate(mapOf( + "tags" to emptyList() + )) + assertTrue(resultEmpty is AnnotationValidator.ValidationResult.Invalid) + assertEquals( + "Field tags cannot be empty", + (resultEmpty as AnnotationValidator.ValidationResult.Invalid).errors.first() + ) + + // Test valid non-empty list + val resultValid = validator.validate(mapOf( + "tags" to listOf("tag1") + )) + assertTrue(resultValid is AnnotationValidator.ValidationResult.Valid) + } + } } \ No newline at end of file From 4af8ec6225809de2e49e83d01af34d6a9b929fe0 Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 13 Nov 2024 21:05:46 +0800 Subject: [PATCH 06/24] debugging version of completions --- .../AnnotationCompletionContributor.kt | 138 +++++++++++++ .../context/CaseCheckContext.kt | 2 +- .../services/AnnotationConfigService.kt | 5 + .../ui/AnnotationConfigurable.kt | 2 +- ...CustomAnnotationCompletionLookupElement.kt | 32 +++ src/main/resources/META-INF/plugin.xml | 3 + .../completion/AnnotationCompletionTest.kt | 192 ++++++++++++++++++ .../pitestintellij/testutil/TestBase.kt | 19 ++ 8 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt create mode 100644 src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt create mode 100644 src/test/kotlin/com/github/jaksonlin/pitestintellij/testutil/TestBase.kt diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt new file mode 100644 index 0000000..0c88602 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt @@ -0,0 +1,138 @@ +package com.github.jaksonlin.pitestintellij.completion + +import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.patterns.PlatformPatterns +import com.intellij.psi.* +import com.intellij.util.ProcessingContext +import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.patterns.ElementPattern + +class AnnotationCompletionContributor : CompletionContributor() { + private val LOG = Logger.getInstance(AnnotationCompletionContributor::class.java) + + init { + LOG.info("Initializing AnnotationCompletionContributor") + // Start with a very broad pattern +// extend( +// CompletionType.BASIC, +// PlatformPatterns.psiElement(), // Match any element +// AnnotationCompletionProvider() +// ) + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement().inside(PsiAnnotation::class.java), + AnnotationCompletionProvider() + ) + /* Once the first breakpoint is hit, we can gradually narrow it down: + + // Step 1: Match any element inside an annotation + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement() + .withSuperParent(2, PsiAnnotation::class.java), + AnnotationCompletionProvider() + ) + + // Step 2: Match any element that's part of an annotation attribute + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement() + .withParent(PsiNameValuePair::class.java) + .withSuperParent(2, PsiAnnotation::class.java), + AnnotationCompletionProvider() + ) + + // Step 3: Match string literals in annotations + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement() + .withParent(PsiLiteralExpression::class.java) + .withSuperParent(2, PsiNameValuePair::class.java) + .withSuperParent(3, PsiAnnotation::class.java), + AnnotationCompletionProvider() + ) + */ + } + + // Override for debugging + override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { + LOG.info("fillCompletionVariants called") + LOG.info("Position: ${parameters.position}") + LOG.info("Original position: ${parameters.originalPosition}") + LOG.info("Offset: ${parameters.offset}") + LOG.info("Is auto popup: ${parameters.isAutoPopup}") + + // Log the PSI tree for the current position + LOG.info("PSI tree:") + var element: PsiElement? = parameters.position + while (element != null) { + LOG.info(" ${element.javaClass.simpleName}: '${element.text}'") + element = element.parent + } + + // Create a wrapped result set for debugging + + super.fillCompletionVariants(parameters, result) + } + + private class AnnotationCompletionProvider : CompletionProvider() { + private val LOG = Logger.getInstance(AnnotationCompletionContributor::class.java) + + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + LOG.info("addCompletions called") + + val position = parameters.position + LOG.info("Position class: ${position.javaClass.name}") + LOG.info("Position text: ${position.text}") + + val nameValuePair = position.parent?.parent as? PsiNameValuePair + LOG.info("NameValuePair: ${nameValuePair?.text}") + + val annotation = nameValuePair?.parent?.parent as? PsiAnnotation + LOG.info("Annotation: ${annotation?.text}") + + if (nameValuePair == null || annotation == null) { + LOG.info("Required PSI elements not found") + return + } + + val configService = position.project.service() + val schema = configService.getSchema() + LOG.info("Schema annotation class: ${schema.annotationClassName}") + LOG.info("Actual annotation class: ${annotation.qualifiedName}") + + // Check if this is our target annotation + if (annotation.qualifiedName != schema.annotationClassName) { + LOG.info("Annotation mismatch") + return + } + + // Find matching field in schema + val fieldName = nameValuePair.name + LOG.info("Field name: $fieldName") + + val field = schema.fields.find { it.name == fieldName } + if (field == null) { + LOG.info("Field not found in schema") + return + } + + // Add completion items + field.validation?.validValues?.forEach { value -> + LOG.info("Adding completion value: $value") + val element = LookupElementBuilder.create(value) + .withCaseSensitivity(false) + .withTypeText(field.type.toString()) + result.addElement(element) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt index 10f2903..d657ed5 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt @@ -1,6 +1,6 @@ package com.github.jaksonlin.pitestintellij.context -import AnnotationConfigService +import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService import com.github.jaksonlin.pitestintellij.actions.RunCaseAnnoationCheckAction import com.github.jaksonlin.pitestintellij.annotations.AnnotationParser import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt index b85b37c..db70a80 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt @@ -1,3 +1,4 @@ +package com.github.jaksonlin.pitestintellij.services import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service @@ -41,4 +42,8 @@ class AnnotationConfigService : PersistentStateComponent AllIcons.General.Settings + AnnotationFieldType.STRING_LIST -> AllIcons.Nodes.EntryPoints + else -> AllIcons.Nodes.Field + } + + if (isDefaultValue) { + presentation.isItemTextBold = true + presentation.typeText = "Default" + } + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 66dc86a..ccf427b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -49,6 +49,9 @@ groupName="Java" order="LAST" /> + diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt new file mode 100644 index 0000000..1938c2f --- /dev/null +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt @@ -0,0 +1,192 @@ +package com.github.jaksonlin.pitestintellij.completion + +import com.github.jaksonlin.pitestintellij.annotations.* +import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService +import com.github.jaksonlin.pitestintellij.testutil.TestBase +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase +import com.intellij.codeInsight.lookup.LookupElementPresentation +import com.intellij.openapi.Disposable +import com.intellij.openapi.diagnostic.LogLevel +import com.intellij.openapi.diagnostic.Logger +import com.intellij.psi.PsiElement + +class AnnotationCompletionTest : LightJavaCodeInsightFixtureTestCase(), TestBase { + + private lateinit var configService: AnnotationConfigService + + override fun setUp() { + super.setUp() + setupServices(getTestRootDisposable()) // Pass the disposable from parent class + + // Add the UnitTest annotation class + myFixture.addClass(""" + public @interface UnitTest { + String status() default "TODO"; + String[] testPoints() default {}; + } + """.trimIndent()) + + // Get the registered service + configService = project.getService(AnnotationConfigService::class.java) + + // Set up test schema + val testSchema = AnnotationSchema( + annotationClassName = "UnitTest", + fields = listOf( + AnnotationFieldConfig( + name = "status", + type = AnnotationFieldType.STATUS, + validation = FieldValidation( + validValues = listOf("TODO", "IN_PROGRESS", "DONE"), + allowCustomValues = false, + mode = ValidationMode.EXACT + ), + defaultValue = DefaultValue.StringValue("TODO") + ), + AnnotationFieldConfig( + name = "testPoints", + type = AnnotationFieldType.STRING_LIST, + validation = FieldValidation( + validValues = listOf( + "Boundary Value", + "Error Handling", + "Performance", + "Security" + ), + allowCustomValues = true, + mode = ValidationMode.CONTAINS + ), + defaultValue = DefaultValue.StringListValue(emptyList()) + ) + ) + ) + + configService.updateSchema(testSchema) + } + + + + + fun testStatusCompletion() { + myFixture.configureByText("TestClass.java", """ + @UnitTest(status = "") + public class TestClass { + } + """.trimIndent()) + + // Type the opening quote and a character to trigger completion + myFixture.type("\"T") + myFixture.completeBasic() + + // Get all lookup elements + val lookupElements = myFixture.lookupElements + assertNotNull("Should have completions", lookupElements) + + val completions = lookupElements!!.map { it.lookupString } + println("Available completions: ${completions.joinToString()}") + + assertTrue("Should contain TODO", completions.contains("TODO")) + assertTrue("Should contain at least one completion", completions.isNotEmpty()) + } + + fun testTestPointsCompletion() { + myFixture.configureByText("TestClass.java", """ + @UnitTest(testPoints = {""}) + public class TestClass { + } + """.trimIndent()) + + val completions = myFixture.completeBasic() + assertNotNull(completions) + assertTrue(completions.any { it.lookupString == "Boundary Value" }) + assertTrue(completions.any { it.lookupString == "Error Handling" }) + assertTrue(completions.any { it.lookupString == "Performance" }) + assertTrue(completions.any { it.lookupString == "Security" }) + } + + fun testNoCompletionForUnknownAnnotation() { + myFixture.configureByText("TestClass.java", """ + @SomeOtherAnnotation(value = "") + public class TestClass { + } + """.trimIndent()) + + val completions = myFixture.completeBasic() + assertTrue(completions == null || completions.none { + it.lookupString in listOf("TODO", "DONE", "Boundary Value") + }) + } + + fun testNoCompletionForUnknownField() { + myFixture.configureByText("TestClass.java", """ + @UnitTest(unknownField = "") + public class TestClass { + } + """.trimIndent()) + + val completions = myFixture.completeBasic() + assertTrue(completions == null || completions.none { + it.lookupString in listOf("TODO", "DONE", "Boundary Value") + }) + } + + fun testServiceSetup() { + // Verify service is properly registered + val service = project.getService(AnnotationConfigService::class.java) + assertNotNull("AnnotationConfigService should be registered", service) + + // Verify schema is properly loaded + val schema = service.getSchema() + assertEquals("UnitTest", schema.annotationClassName) + // store the schema in a variable + + + // Verify fields are properly configured + val statusField = schema.fields.find { it.name == "status" } + assertNotNull("Status field should exist", statusField) + assertEquals(AnnotationFieldType.STATUS, statusField!!.type) + + val testPointsField = schema.fields.find { it.name == "testPoints" } + assertNotNull("TestPoints field should exist", testPointsField) + assertEquals(AnnotationFieldType.STRING_LIST, testPointsField!!.type) + } + + fun testCompletionDebug() { + val positions = listOf( + """ + @UnitTest(status = ) + """, + """ + @UnitTest(status = "") + """, + """ + @UnitTest(status = "T") + """ + ) + // set the log level to debug + Logger.getInstance(AnnotationCompletionContributor::class.java).setLevel(LogLevel.DEBUG) + + positions.forEachIndexed { index, position -> + println("\nTesting position $index") + myFixture.configureByText("TestClass${index}.java", """ + ${position.trimIndent()} + public class TestClass { + } + """.trimIndent()) + + // Try completion + val completions = myFixture.completeBasic() + println("Completions at position $index: ${completions?.size ?: 0}") + completions?.forEach { + println(" ${it.lookupString}") + } + } + } + + private fun printPsiTree(element: PsiElement, indent: String = "") { + println("$indent${element.javaClass.simpleName}: '${element.text}'") + element.children.forEach { child -> + printPsiTree(child, "$indent ") + } + } +} diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/testutil/TestBase.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/testutil/TestBase.kt new file mode 100644 index 0000000..ede0632 --- /dev/null +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/testutil/TestBase.kt @@ -0,0 +1,19 @@ +package com.github.jaksonlin.pitestintellij.testutil + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService +import com.intellij.openapi.Disposable +import com.intellij.testFramework.replaceService + +interface TestBase { + // Remove the property requirement and use a parameter instead + fun setupServices(disposable: Disposable) { + val application = ApplicationManager.getApplication() + application.replaceService( + AnnotationConfigService::class.java, + AnnotationConfigService(), + disposable + ) + } +} \ No newline at end of file From 227c2f4017e6bb1c8add20e1c4a4ffbcea62822e Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 13 Nov 2024 21:15:34 +0800 Subject: [PATCH 07/24] all test passes --- .../AnnotationCompletionContributor.kt | 169 ++++++------------ .../completion/AnnotationCompletionTest.kt | 2 +- 2 files changed, 56 insertions(+), 115 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt index 0c88602..e4ae241 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt @@ -7,132 +7,73 @@ import com.intellij.psi.* import com.intellij.util.ProcessingContext import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.lang.java.JavaLanguage import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.patterns.ElementPattern +import com.intellij.psi.util.PsiTreeUtil class AnnotationCompletionContributor : CompletionContributor() { private val LOG = Logger.getInstance(AnnotationCompletionContributor::class.java) init { LOG.info("Initializing AnnotationCompletionContributor") - // Start with a very broad pattern -// extend( -// CompletionType.BASIC, -// PlatformPatterns.psiElement(), // Match any element -// AnnotationCompletionProvider() -// ) - extend( - CompletionType.BASIC, - PlatformPatterns.psiElement().inside(PsiAnnotation::class.java), - AnnotationCompletionProvider() - ) - /* Once the first breakpoint is hit, we can gradually narrow it down: - - // Step 1: Match any element inside an annotation - extend( - CompletionType.BASIC, - PlatformPatterns.psiElement() - .withSuperParent(2, PsiAnnotation::class.java), - AnnotationCompletionProvider() - ) - - // Step 2: Match any element that's part of an annotation attribute - extend( - CompletionType.BASIC, - PlatformPatterns.psiElement() - .withParent(PsiNameValuePair::class.java) - .withSuperParent(2, PsiAnnotation::class.java), - AnnotationCompletionProvider() - ) - - // Step 3: Match string literals in annotations extend( CompletionType.BASIC, PlatformPatterns.psiElement() - .withParent(PsiLiteralExpression::class.java) - .withSuperParent(2, PsiNameValuePair::class.java) - .withSuperParent(3, PsiAnnotation::class.java), - AnnotationCompletionProvider() - ) - */ - } - - // Override for debugging - override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { - LOG.info("fillCompletionVariants called") - LOG.info("Position: ${parameters.position}") - LOG.info("Original position: ${parameters.originalPosition}") - LOG.info("Offset: ${parameters.offset}") - LOG.info("Is auto popup: ${parameters.isAutoPopup}") - - // Log the PSI tree for the current position - LOG.info("PSI tree:") - var element: PsiElement? = parameters.position - while (element != null) { - LOG.info(" ${element.javaClass.simpleName}: '${element.text}'") - element = element.parent - } - - // Create a wrapped result set for debugging - - super.fillCompletionVariants(parameters, result) - } - - private class AnnotationCompletionProvider : CompletionProvider() { - private val LOG = Logger.getInstance(AnnotationCompletionContributor::class.java) - - override fun addCompletions( - parameters: CompletionParameters, - context: ProcessingContext, - result: CompletionResultSet - ) { - LOG.info("addCompletions called") - - val position = parameters.position - LOG.info("Position class: ${position.javaClass.name}") - LOG.info("Position text: ${position.text}") - - val nameValuePair = position.parent?.parent as? PsiNameValuePair - LOG.info("NameValuePair: ${nameValuePair?.text}") - - val annotation = nameValuePair?.parent?.parent as? PsiAnnotation - LOG.info("Annotation: ${annotation?.text}") - - if (nameValuePair == null || annotation == null) { - LOG.info("Required PSI elements not found") - return + .inside(PsiAnnotation::class.java) // Inside an annotation + .withLanguage(JavaLanguage.INSTANCE), // Ensure it's Java + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + // Find the containing annotation and name-value pair + val annotation = PsiTreeUtil.getParentOfType(parameters.position, PsiAnnotation::class.java) ?: return + val nameValuePair = PsiTreeUtil.getParentOfType(parameters.position, PsiNameValuePair::class.java) ?: return + + LOG.info("Found annotation: ${annotation.qualifiedName}") + LOG.info("Found attribute: ${nameValuePair.name}") + + // Rest of your completion logic... + if (nameValuePair == null || annotation == null) { + LOG.info("Required PSI elements not found") + return + } + val project = parameters.position.project + val configService = project.service() + val schema = configService.getSchema() + LOG.info("Schema annotation class: ${schema.annotationClassName}") + LOG.info("Actual annotation class: ${annotation.qualifiedName}") + + // Check if this is our target annotation + if (annotation.qualifiedName != schema.annotationClassName) { + LOG.info("Annotation mismatch") + return + } + + // Find matching field in schema + val fieldName = nameValuePair.name + LOG.info("Field name: $fieldName") + + val field = schema.fields.find { it.name == fieldName } + if (field == null) { + LOG.info("Field not found in schema") + return + } + + // Add completion items + field.validation?.validValues?.forEach { value -> + LOG.info("Adding completion value: $value") + val element = LookupElementBuilder.create(value) + .withCaseSensitivity(false) + .withTypeText(field.type.toString()) + result.addElement(element) + } + + } } - - val configService = position.project.service() - val schema = configService.getSchema() - LOG.info("Schema annotation class: ${schema.annotationClassName}") - LOG.info("Actual annotation class: ${annotation.qualifiedName}") - - // Check if this is our target annotation - if (annotation.qualifiedName != schema.annotationClassName) { - LOG.info("Annotation mismatch") - return - } - - // Find matching field in schema - val fieldName = nameValuePair.name - LOG.info("Field name: $fieldName") - - val field = schema.fields.find { it.name == fieldName } - if (field == null) { - LOG.info("Field not found in schema") - return - } - - // Add completion items - field.validation?.validValues?.forEach { value -> - LOG.info("Adding completion value: $value") - val element = LookupElementBuilder.create(value) - .withCaseSensitivity(false) - .withTypeText(field.type.toString()) - result.addElement(element) - } - } + ) } } \ No newline at end of file diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt index 1938c2f..522766e 100644 --- a/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt @@ -75,7 +75,7 @@ class AnnotationCompletionTest : LightJavaCodeInsightFixtureTestCase(), TestBase """.trimIndent()) // Type the opening quote and a character to trigger completion - myFixture.type("\"T") + myFixture.type("T") myFixture.completeBasic() // Get all lookup elements From 43b62bbd1a031015e7cff36834df36886ae57004 Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 13 Nov 2024 22:16:14 +0800 Subject: [PATCH 08/24] finish the insertion --- .../AnnotationCompletionContributor.kt | 43 +++++++++++++++---- ...CustomAnnotationCompletionLookupElement.kt | 17 +++++++- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt index e4ae241..48ee9f7 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt @@ -1,11 +1,14 @@ package com.github.jaksonlin.pitestintellij.completion +import com.github.jaksonlin.pitestintellij.annotations.DefaultValue +import com.github.jaksonlin.pitestintellij.annotations.ValidationMode import com.intellij.codeInsight.completion.* import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.patterns.PlatformPatterns import com.intellij.psi.* import com.intellij.util.ProcessingContext import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService +import com.github.jaksonlin.pitestintellij.ui.CustomAnnotationCompletionLookupElement import com.intellij.codeInsight.lookup.LookupElement import com.intellij.lang.java.JavaLanguage import com.intellij.openapi.components.service @@ -30,10 +33,17 @@ class AnnotationCompletionContributor : CompletionContributor() { result: CompletionResultSet ) { // Find the containing annotation and name-value pair - val annotation = PsiTreeUtil.getParentOfType(parameters.position, PsiAnnotation::class.java) ?: return - val nameValuePair = PsiTreeUtil.getParentOfType(parameters.position, PsiNameValuePair::class.java) ?: return - + val annotation = PsiTreeUtil.getParentOfType(parameters.position, PsiAnnotation::class.java) + if (annotation == null){ + return + } LOG.info("Found annotation: ${annotation.qualifiedName}") + val nameValuePair = PsiTreeUtil.getParentOfType(parameters.position, PsiNameValuePair::class.java) + if (nameValuePair == null){ + return + } + + LOG.info("Found attribute: ${nameValuePair.name}") // Rest of your completion logic... @@ -65,14 +75,31 @@ class AnnotationCompletionContributor : CompletionContributor() { // Add completion items field.validation?.validValues?.forEach { value -> - LOG.info("Adding completion value: $value") - val element = LookupElementBuilder.create(value) - .withCaseSensitivity(false) - .withTypeText(field.type.toString()) - result.addElement(element) + val isDefault = when (field.defaultValue) { + is DefaultValue.StringValue -> field.defaultValue.value == value + is DefaultValue.StringListValue -> value in field.defaultValue.value + else -> false + } + + val element = CustomAnnotationCompletionLookupElement( + value = value, + fieldType = field.type, + isDefaultValue = isDefault + ) + + val prioritized = when { + isDefault -> PrioritizedLookupElement.withPriority(element, 100.0) + field.validation.mode == ValidationMode.EXACT -> + PrioritizedLookupElement.withPriority(element, 50.0) + else -> PrioritizedLookupElement.withPriority(element, 0.0) + } + + result.addElement(prioritized) } } + + } ) } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt index 75f994b..199f737 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt @@ -1,11 +1,12 @@ package com.github.jaksonlin.pitestintellij.ui import com.github.jaksonlin.pitestintellij.annotations.AnnotationFieldType +import com.intellij.codeInsight.completion.InsertionContext import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementPresentation import com.intellij.icons.AllIcons -class CustomAnnotationCompletionLookupElement ( +class CustomAnnotationCompletionLookupElement( private val value: String, private val fieldType: AnnotationFieldType, private val isDefaultValue: Boolean = false @@ -29,4 +30,18 @@ class CustomAnnotationCompletionLookupElement ( presentation.typeText = "Default" } } + + override fun handleInsert(context: InsertionContext) { + val document = context.document + val startOffset = context.startOffset + val tailOffset = context.tailOffset + + // Insert quotes around the value + document.insertString(startOffset, "\"") + document.insertString(tailOffset+1, "\"") + + // Move caret after the closing quote + context.editor.caretModel.moveToOffset(tailOffset + 2) + + } } \ No newline at end of file From 1be8a2c746282c75c182b10d61a4fe080f7cb7ac Mon Sep 17 00:00:00 2001 From: zhislin Date: Thu, 14 Nov 2024 10:08:39 +0800 Subject: [PATCH 09/24] add generate annotation --- .../GenerateAnnotationCommandAction.kt | 30 ++++++++ .../actions/RunCaseAnnoationCheckAction.kt | 2 +- .../RunTestFileAnnoationCheckAction.kt | 4 +- .../annotations/AnnotationFieldType.kt | 3 +- .../annotations/AnnotationParser.kt | 1 - .../annotations/AnnotationSchema.kt | 69 +++++++++++++++---- .../annotations/AnnotationValidator.kt | 6 +- .../CheckAnnotationCommand.kt | 7 +- .../CheckMethodDataCommand.kt | 2 +- .../GenerateAnnotationCommand.kt | 58 ++++++++++++++++ .../UnittestCaseCheckCommand.kt | 12 +++- .../UnittestFileInspectorCommand.kt | 2 +- .../inspectors/UnittestInspector.kt | 4 +- ...CustomAnnotationCompletionLookupElement.kt | 1 - .../MutationTreeMediatorViewModel.kt | 2 +- src/main/resources/META-INF/plugin.xml | 4 ++ .../completion/AnnotationCompletionTest.kt | 4 +- 17 files changed, 170 insertions(+), 41 deletions(-) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{casecheck => unittestannotations}/CheckAnnotationCommand.kt (84%) rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{casecheck => unittestannotations}/CheckMethodDataCommand.kt (93%) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{casecheck => unittestannotations}/UnittestCaseCheckCommand.kt (89%) rename src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/{casecheck => unittestannotations}/UnittestFileInspectorCommand.kt (96%) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt new file mode 100644 index 0000000..3e87e9b --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt @@ -0,0 +1,30 @@ +package com.github.jaksonlin.pitestintellij.actions + +import com.github.jaksonlin.pitestintellij.commands.unittestannotations.GenerateAnnotationCommand +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil + + +class GenerateAnnotationCommandAction : AnAction() { + + override fun actionPerformed(e: AnActionEvent) { + val psiMethod = findMethodAtCaret(e) ?: return + val context = CaseCheckContext.create(psiMethod) + GenerateAnnotationCommand(e.project!!, context).execute() + } + + private fun findMethodAtCaret(e: AnActionEvent): PsiMethod? { + val project = e.project ?: return null + val editor = e.dataContext.getData(CommonDataKeys.EDITOR) ?: return null + val caret = e.dataContext.getData(CommonDataKeys.CARET) ?: return null + val elementAtCaret = PsiDocumentManager.getInstance(project) + .getPsiFile(editor.document)?.findElementAt(caret.offset) ?: return null + return PsiTreeUtil.getParentOfType(elementAtCaret, PsiMethod::class.java) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt index 992cec6..2c4de84 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt @@ -1,6 +1,6 @@ package com.github.jaksonlin.pitestintellij.actions -import com.github.jaksonlin.pitestintellij.commands.casecheck.CheckAnnotationCommand +import com.github.jaksonlin.pitestintellij.commands.unittestannotations.CheckAnnotationCommand import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt index 50cb9d2..aea588a 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt @@ -1,15 +1,13 @@ package com.github.jaksonlin.pitestintellij.actions -import com.github.jaksonlin.pitestintellij.commands.casecheck.CheckAnnotationCommand +import com.github.jaksonlin.pitestintellij.commands.unittestannotations.CheckAnnotationCommand import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.psi.JavaRecursiveElementVisitor -import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiJavaFile import com.intellij.psi.PsiMethod -import com.intellij.psi.util.PsiTreeUtil class RunTestFileAnnoationCheckAction: AnAction() { diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt index 89d4244..a90cce8 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldType.kt @@ -5,6 +5,5 @@ import kotlinx.serialization.Serializable @Serializable enum class AnnotationFieldType { STRING, - STRING_LIST, - STATUS + STRING_LIST } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt index b5b996f..24633e4 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationParser.kt @@ -47,7 +47,6 @@ class AnnotationParser(private val schema: AnnotationSchema) { return when (field.type) { AnnotationFieldType.STRING -> value as? String ?: field.defaultValue AnnotationFieldType.STRING_LIST -> (value as? List<*>)?.mapNotNull { it as? String } ?: emptyList() - AnnotationFieldType.STATUS -> (value as? String)?.uppercase() ?: field.defaultValue } } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 5e91a1b..ed48733 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -17,46 +17,85 @@ data class AnnotationSchema( "name": "author", "type": "STRING", "required": true, - "defaultValue": {"type": "NullValue"}, "validation": { "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" } }, { "name": "title", "type": "STRING", "required": true, - "defaultValue": {"type": "NullValue"} + "validation": { + "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" + } }, { "name": "targetClass", "type": "STRING", "required": true, - "defaultValue": {"type": "NullValue"} + "validation": { + "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" + } }, { "name": "targetMethod", "type": "STRING", "required": true, - "defaultValue": {"type": "NullValue"} + "validation": { + "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" + } }, { "name": "lastUpdateTime", "type": "STRING", "required": true, - "defaultValue": {"type": "NullValue"} + "validation": { + "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" + } }, { "name": "lastUpdateAuthor", "type": "STRING", "required": true, - "defaultValue": {"type": "NullValue"} + "validation": { + "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" + } }, { "name": "methodSignature", "type": "STRING", "required": true, - "defaultValue": {"type": "NullValue"} + "validation": { + "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" + } }, { "name": "testPoints", @@ -84,8 +123,8 @@ data class AnnotationSchema( }, { "name": "status", - "type": "STATUS", - "required": true, + "type": "STRING", + "required": false, "defaultValue": { "type": "StringValue", "value": "TODO" @@ -98,7 +137,7 @@ data class AnnotationSchema( "DEPRECATED", "BROKEN" ], - "allowCustomValues": false, + "allowCustomValues": true, "mode": "CONTAINS", "allowEmpty": false } @@ -106,8 +145,14 @@ data class AnnotationSchema( { "name": "description", "type": "STRING", - "required": false, - "defaultValue": {"type": "NullValue"} + "required": true, + "validation": { + "allowEmpty": false + }, + "defaultValue": { + "type": "StringValue", + "value": "" + } }, { "name": "tags", diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt index 9313b53..e843d4c 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationValidator.kt @@ -66,7 +66,7 @@ class AnnotationValidator(private val schema: AnnotationSchema) { } private fun validateNonEmpty(field: AnnotationFieldConfig, value: Any): String? { - if (!field.validation?.allowEmpty.let { it != null && !it }) { + if (field.validation?.allowEmpty == false) { when (value) { is String -> { if (value.isBlank()) { @@ -99,10 +99,6 @@ class AnnotationValidator(private val schema: AnnotationSchema) { else -> "Field ${field.name} must be a List" } } - AnnotationFieldType.STATUS -> { - if (value !is String) "Field ${field.name} must be a String" - else null - } } } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckAnnotationCommand.kt similarity index 84% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckAnnotationCommand.kt index 414f6c3..e547257 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckAnnotationCommand.kt @@ -1,14 +1,11 @@ -package com.github.jaksonlin.pitestintellij.commands.casecheck +package com.github.jaksonlin.pitestintellij.commands.unittestannotations import com.github.jaksonlin.pitestintellij.annotations.AnnotationFieldType -import com.github.jaksonlin.pitestintellij.annotations.AnnotationParser import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.github.jaksonlin.pitestintellij.context.UnittestCase import com.intellij.openapi.project.Project import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiArrayInitializerMemberValue -import com.intellij.psi.PsiMethod class CheckAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { override fun execute() { @@ -49,8 +46,6 @@ class CheckAnnotationCommand(project: Project, context: CaseCheckContext):Unitt appendLine(testCase.getString(field.name)) AnnotationFieldType.STRING_LIST -> appendLine(testCase.getStringList(field.name).joinToString(", ")) - AnnotationFieldType.STATUS -> - appendLine(testCase.getStatus(field.name)) } } } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckMethodDataCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckMethodDataCommand.kt similarity index 93% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckMethodDataCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckMethodDataCommand.kt index 3e27ca6..cdb3434 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/CheckMethodDataCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckMethodDataCommand.kt @@ -1,4 +1,4 @@ -package com.github.jaksonlin.pitestintellij.commands.casecheck +package com.github.jaksonlin.pitestintellij.commands.unittestannotations import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.openapi.project.Project diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt new file mode 100644 index 0000000..4f6b26e --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt @@ -0,0 +1,58 @@ +package com.github.jaksonlin.pitestintellij.commands.unittestannotations + +import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema +import com.github.jaksonlin.pitestintellij.annotations.DefaultValue +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementFactory +import com.intellij.psi.PsiMethod + +class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { + private val psiElementFactory: PsiElementFactory = JavaPsiFacade.getInstance(project).elementFactory + override fun execute() { + val annotation = findTargetAnnotation(context.psiMethod, context.schema) + if (annotation != null) { + showAnnotationAlreadyExistMessage(project, context.schema.annotationClassName) + return + } + generateAnnotation(context.psiMethod, context.schema) + } + + protected fun generateAnnotation(psiMethod: PsiMethod, schema: AnnotationSchema) { + WriteCommandAction.runWriteCommandAction(project) { + val annotation = buildAnnotation(schema) + psiMethod.modifierList.addAfter(annotation, null) + } + } + + private fun buildAnnotation(schema: AnnotationSchema): PsiAnnotation { + // Build the annotation text with default values + val requiredFields = schema.fields.filter { it.required } + + val fieldValues = requiredFields.joinToString(",\n ") { field -> + val defaultValueStr = when (val default = field.defaultValue) { + is DefaultValue.StringValue -> "\"${default.value}\"" + is DefaultValue.StringListValue -> { + val values = default.value.joinToString("\", \"") { "\"$it\"" } + if (values.isEmpty()) "{}" else "{$values}" + } + is DefaultValue.NullValue -> "null" + null -> "\"\"" // Empty string for required fields without default + } + "${field.name} = $defaultValueStr" + } + + val annotationText = if (fieldValues.isEmpty()) { + "@${schema.annotationClassName}" + } else { + """@${schema.annotationClassName}( + $fieldValues +)""" + } + + return psiElementFactory.createAnnotationFromText(annotationText, null) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestCaseCheckCommand.kt similarity index 89% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestCaseCheckCommand.kt index 0067f8a..4aa9714 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestCaseCheckCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestCaseCheckCommand.kt @@ -1,9 +1,8 @@ -package com.github.jaksonlin.pitestintellij.commands.casecheck +package com.github.jaksonlin.pitestintellij.commands.unittestannotations import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.github.jaksonlin.pitestintellij.context.UnittestCase -import com.github.jaksonlin.pitestintellij.context.UnittestCaseInfoContext import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.psi.PsiAnnotation @@ -40,6 +39,15 @@ abstract class UnittestCaseCheckCommand(protected val project: Project, protecte ) } + fun showAnnotationAlreadyExistMessage(project: Project, annotationName: String) { + Messages.showMessageDialog( + project, + "$annotationName already exist on this method", + "Annotation Generation Action", + Messages.getWarningIcon() + ) + } + fun findTargetAnnotation( psiMethod: PsiMethod, schema: AnnotationSchema diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestFileInspectorCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestFileInspectorCommand.kt similarity index 96% rename from src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestFileInspectorCommand.kt rename to src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestFileInspectorCommand.kt index 937e751..7974f66 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/casecheck/UnittestFileInspectorCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestFileInspectorCommand.kt @@ -1,4 +1,4 @@ -package com.github.jaksonlin.pitestintellij.commands.casecheck +package com.github.jaksonlin.pitestintellij.commands.unittestannotations import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.codeInspection.ProblemHighlightType diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt index 11e7b35..731c1f0 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt @@ -1,11 +1,9 @@ package com.github.jaksonlin.pitestintellij.inspectors -import com.github.jaksonlin.pitestintellij.commands.casecheck.CheckAnnotationCommand -import com.github.jaksonlin.pitestintellij.commands.casecheck.UnittestFileInspectorCommand +import com.github.jaksonlin.pitestintellij.commands.unittestannotations.UnittestFileInspectorCommand import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.codeInspection.* import com.intellij.psi.* -import com.intellij.psi.util.PsiTreeUtil import java.util.concurrent.ConcurrentHashMap class UnittestInspector : AbstractBaseJavaLocalInspectionTool() { diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt index 199f737..d9e1001 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/ui/CustomAnnotationCompletionLookupElement.kt @@ -20,7 +20,6 @@ class CustomAnnotationCompletionLookupElement( // Add appropriate icon based on type presentation.icon = when (fieldType) { - AnnotationFieldType.STATUS -> AllIcons.General.Settings AnnotationFieldType.STRING_LIST -> AllIcons.Nodes.EntryPoints else -> AllIcons.Nodes.Field } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/viewmodels/MutationTreeMediatorViewModel.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/viewmodels/MutationTreeMediatorViewModel.kt index 1cbd32d..dc71e2d 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/viewmodels/MutationTreeMediatorViewModel.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/viewmodels/MutationTreeMediatorViewModel.kt @@ -133,7 +133,7 @@ class MutationTreeMediatorViewModel( override fun hashCode(): Int { var result = icon.hashCode() - result = 31 * result + (tooltipText.hashCode() ?: 0) + result = 31 * result + tooltipText.hashCode() return result } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ccf427b..155dbd5 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -79,6 +79,10 @@ id="RunCaseAnnoationCheckAction" text="Run Current Method Annotation Check"> + + diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt index 522766e..b6fa67c 100644 --- a/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt @@ -35,7 +35,7 @@ class AnnotationCompletionTest : LightJavaCodeInsightFixtureTestCase(), TestBase fields = listOf( AnnotationFieldConfig( name = "status", - type = AnnotationFieldType.STATUS, + type = AnnotationFieldType.STRING, validation = FieldValidation( validValues = listOf("TODO", "IN_PROGRESS", "DONE"), allowCustomValues = false, @@ -144,7 +144,7 @@ class AnnotationCompletionTest : LightJavaCodeInsightFixtureTestCase(), TestBase // Verify fields are properly configured val statusField = schema.fields.find { it.name == "status" } assertNotNull("Status field should exist", statusField) - assertEquals(AnnotationFieldType.STATUS, statusField!!.type) + assertEquals(AnnotationFieldType.STRING, statusField!!.type) val testPointsField = schema.fields.find { it.name == "testPoints" } assertNotNull("TestPoints field should exist", testPointsField) From 7fc0173c150e65a81fb50bf022d86ef82715826a Mon Sep 17 00:00:00 2001 From: zhislin Date: Thu, 14 Nov 2024 10:37:38 +0800 Subject: [PATCH 10/24] fix inspection --- .../annotations/AnnotationSchema.kt | 2 +- .../UnittestFileInspectorCommand.kt | 2 +- .../inspectors/UnittestInspector.kt | 27 ++++--------------- .../messages/MyBundle_en_US.properties | 5 +++- .../messages/MyBundle_zh_CN.properties | 5 +++- 5 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index ed48733..4d8b59d 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -8,7 +8,7 @@ data class AnnotationSchema( val fields: List ) { companion object { - // Default schema matching UnittestCaseInfoContext + // Default schema matching UnittestCaseInfoContextG val DEFAULT_SCHEMA = """ { "annotationClassName": "Unittest", diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestFileInspectorCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestFileInspectorCommand.kt index 7974f66..5e2121d 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestFileInspectorCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestFileInspectorCommand.kt @@ -18,7 +18,7 @@ class UnittestFileInspectorCommand(private val holder: ProblemsHolder, project: try { val annotation = findTargetAnnotation(psiMethod, context.schema) if (annotation == null) { - holder.registerProblem(context.psiMethod, "No annotation found", ProblemHighlightType.WARNING) + holder.registerProblem(context.psiMethod, "No unittest case management annotation found", ProblemHighlightType.WARNING) return } val testCase = parseUnittestCaseFromAnnotations(annotation) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt index 731c1f0..6fd36c7 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt @@ -1,5 +1,6 @@ package com.github.jaksonlin.pitestintellij.inspectors +import com.github.jaksonlin.pitestintellij.MyBundle import com.github.jaksonlin.pitestintellij.commands.unittestannotations.UnittestFileInspectorCommand import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.codeInspection.* @@ -7,6 +8,9 @@ import com.intellij.psi.* import java.util.concurrent.ConcurrentHashMap class UnittestInspector : AbstractBaseJavaLocalInspectionTool() { + override fun getGroupDisplayName(): String = MyBundle.message("inspection.group.name") + override fun getDisplayName(): String = MyBundle.message("inspection.display.name") + override fun getShortName(): String = "UnittestCaseAnnotationInspection" // Cache test annotation qualified names for faster lookup private val testAnnotations = setOf( "org.junit.Test", @@ -35,10 +39,6 @@ class UnittestInspector : AbstractBaseJavaLocalInspectionTool() { // Check containing class using cache val containingClass = psiMethod.containingClass ?: return val qualifiedName = containingClass.qualifiedName ?: return - - if (!isTestClass(containingClass, qualifiedName)) { - return - } val context = CaseCheckContext.create(psiMethod) UnittestFileInspectorCommand(holder, project, context).execute() @@ -50,28 +50,11 @@ class UnittestInspector : AbstractBaseJavaLocalInspectionTool() { } } - private fun isTestClass(psiClass: PsiClass, qualifiedName: String): Boolean { - return testClassCache.getOrPut(qualifiedName) { - // First check class name pattern (fastest check) - val className = psiClass.name - if (className != null && ( - className.endsWith("Test") || - className.endsWith("Tests") || - className.endsWith("TestCase") - )) { - return@getOrPut true - } - // Then check annotations if needed - psiClass.annotations.any { - it.qualifiedName in testClassAnnotations - } - } - } } } - override fun getID(): String = "PitestUnitTest" + override fun getID(): String = "UnittestCaseAnnotationInspection" // Clear cache when plugin is unloaded fun clearCache() { diff --git a/src/main/resources/messages/MyBundle_en_US.properties b/src/main/resources/messages/MyBundle_en_US.properties index 6ae7879..41bf3cb 100644 --- a/src/main/resources/messages/MyBundle_en_US.properties +++ b/src/main/resources/messages/MyBundle_en_US.properties @@ -1,3 +1,6 @@ mutation.tree.root=PiTest Mutation History clear.button=Clear All History -search.placeholder=Search History \ No newline at end of file +search.placeholder=Search History +inspection.group.name=Pitest Unit Test Specification +inspection.display.name=Pitest Unit Test Specification Inspection +inspection.problem.descriptor=Missing or invalid unit test annotation \ No newline at end of file diff --git a/src/main/resources/messages/MyBundle_zh_CN.properties b/src/main/resources/messages/MyBundle_zh_CN.properties index afa9fc4..eb2e760 100644 --- a/src/main/resources/messages/MyBundle_zh_CN.properties +++ b/src/main/resources/messages/MyBundle_zh_CN.properties @@ -1,3 +1,6 @@ mutation.tree.root=PiTest变异测试历史 clear.button=清除所有历史 -search.placeholder=搜索历史 \ No newline at end of file +search.placeholder=搜索历史 +inspection.group.name=Pitest单元测试规范 +inspection.display.name=Pitest单元测试规范检查 +inspection.problem.descriptor=缺少或无效的单元测试注解 \ No newline at end of file From 60cc9593d0c5c01da5907661dfe9867d5092fa66 Mon Sep 17 00:00:00 2001 From: zhislin Date: Thu, 14 Nov 2024 11:09:38 +0800 Subject: [PATCH 11/24] fix publish plugin error --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 30c0b66..b57b759 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -126,6 +126,6 @@ tasks { // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel - channels = providers.gradleProperty("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } + channels = providers.gradleProperty("pluginVersion").map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } } } \ No newline at end of file From 3e1868ea7883ef6cc627a7ae03ba5586b38fcd14 Mon Sep 17 00:00:00 2001 From: zhislin Date: Fri, 15 Nov 2024 10:33:02 +0800 Subject: [PATCH 12/24] update schema --- CHANGELOG.md | 4 ++++ gradle.properties | 2 +- .../jaksonlin/pitestintellij/annotations/AnnotationSchema.kt | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b7aa3a..a51646e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,3 +17,7 @@ ## 1.0.2 - 2024-10-12 - Bugfix: change plugin name introduce a bug in the code. + +## 1.0.3-preview - 2024-11-14 +- Add unit test annotation generation action. +- Add unit test annotation inspection. diff --git a/gradle.properties b/gradle.properties index b4e07b0..ea0a1a5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.jaksonlin.pitestintellij pluginName = pitest-gradle pluginRepositoryUrl = https://github.com/jaksonlin/pitest-gradle # SemVer format -> https://semver.org -pluginVersion = 1.0.2 +pluginVersion = 1.0.3-preview # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 222 diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 4d8b59d..4748ade 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -11,7 +11,7 @@ data class AnnotationSchema( // Default schema matching UnittestCaseInfoContextG val DEFAULT_SCHEMA = """ { - "annotationClassName": "Unittest", + "annotationClassName": "UnittestCaseInfo", "fields": [ { "name": "author", From 1e37eaa56b4a2a3518d2ed00621a346e588e0f70 Mon Sep 17 00:00:00 2001 From: zhislin Date: Fri, 15 Nov 2024 13:29:03 +0800 Subject: [PATCH 13/24] fix the comment info collection --- .../CheckAnnotationCommand.kt | 20 +++++++++++++++++ .../AnnotationCompletionContributor.kt | 22 ++++++++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckAnnotationCommand.kt index e547257..2302b14 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/CheckAnnotationCommand.kt @@ -6,6 +6,9 @@ import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.github.jaksonlin.pitestintellij.context.UnittestCase import com.intellij.openapi.project.Project import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil class CheckAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { override fun execute() { @@ -32,6 +35,22 @@ class CheckAnnotationCommand(project: Project, context: CaseCheckContext):Unitt } } + private fun extractMethodBodyComments(psiMethod: PsiMethod): String { + val stepComments = mutableListOf() + val assertComments = mutableListOf() + + PsiTreeUtil.findChildrenOfType(psiMethod, PsiComment::class.java).forEach { + if (it.text.contains("step", ignoreCase = true)) { + stepComments.add(it.text) + } + if (it.text.contains("assert", ignoreCase = true)) { + assertComments.add(it.text) + } + } + // format as : test_step_1: step_comment, remove the leading // + return stepComments.mapIndexed { index, comment -> "test_step_${index + 1}: ${comment.substring(2)}" }.joinToString("\n") + "\n" + assertComments.mapIndexed { index, comment -> "test_assert_${index + 1}: ${comment.substring(2)}" }.joinToString("\n") + } + private fun formatTestCaseMessage( testCase: UnittestCase, @@ -48,6 +67,7 @@ class CheckAnnotationCommand(project: Project, context: CaseCheckContext):Unitt appendLine(testCase.getStringList(field.name).joinToString(", ")) } } + appendLine(extractMethodBodyComments(context.psiMethod)) } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt index 48ee9f7..6426bf8 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt @@ -58,8 +58,8 @@ class AnnotationCompletionContributor : CompletionContributor() { LOG.info("Actual annotation class: ${annotation.qualifiedName}") // Check if this is our target annotation - if (annotation.qualifiedName != schema.annotationClassName) { - LOG.info("Annotation mismatch") + if (annotation.qualifiedName?.endsWith(schema.annotationClassName) != true) { + LOG.info("Annotation mismatch: ${annotation.qualifiedName} not the same as ${schema.annotationClassName}") return } @@ -86,13 +86,19 @@ class AnnotationCompletionContributor : CompletionContributor() { fieldType = field.type, isDefaultValue = isDefault ) - - val prioritized = when { - isDefault -> PrioritizedLookupElement.withPriority(element, 100.0) - field.validation.mode == ValidationMode.EXACT -> - PrioritizedLookupElement.withPriority(element, 50.0) - else -> PrioritizedLookupElement.withPriority(element, 0.0) + var properitizedValue = 100.0 + when { + // Highest priority for exact matches + result.prefixMatcher.prefixMatches(value) -> properitizedValue = 100.0 + // High priority for default values + isDefault -> properitizedValue = 90.0 + // Medium priority for validated values + field.validation?.validValues?.contains(value) == true -> properitizedValue = 80.0 + // Lower priority for other suggestions + else -> properitizedValue = 70.0 } + val prioritized = PrioritizedLookupElement.withPriority(element,properitizedValue) + LOG.info("Adding element: $value with priority ${properitizedValue}") result.addElement(prioritized) } From ab7b2976b77e7e6ecf5574658841c4d56820111e Mon Sep 17 00:00:00 2001 From: zhislin Date: Fri, 15 Nov 2024 14:28:37 +0800 Subject: [PATCH 14/24] add import --- .../GenerateAnnotationCommand.kt | 28 ++++- .../services/AnnotationConfigService.kt | 19 ++- .../ui/AnnotationConfigurable.kt | 109 ++++++++++++++---- 3 files changed, 128 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt index 4f6b26e..f6523da 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt @@ -3,15 +3,16 @@ package com.github.jaksonlin.pitestintellij.commands.unittestannotations import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.github.jaksonlin.pitestintellij.annotations.DefaultValue import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiElementFactory -import com.intellij.psi.PsiMethod +import com.intellij.psi.* class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { private val psiElementFactory: PsiElementFactory = JavaPsiFacade.getInstance(project).elementFactory + private val configService = project.service() + override fun execute() { val annotation = findTargetAnnotation(context.psiMethod, context.schema) if (annotation != null) { @@ -23,11 +24,30 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni protected fun generateAnnotation(psiMethod: PsiMethod, schema: AnnotationSchema) { WriteCommandAction.runWriteCommandAction(project) { + if (configService.isAutoImport()) { + addImportIfNeeded(psiMethod, schema.annotationClassName) + } val annotation = buildAnnotation(schema) psiMethod.modifierList.addAfter(annotation, null) } } + private fun addImportIfNeeded(psiMethod: PsiMethod, annotationClassName: String) { + val file = psiMethod.containingFile + if (file !is PsiJavaFile) return + + val importList = file.importList ?: return + val qualifiedName = "${configService.getAnnotationPackage()}.$annotationClassName" + + // Check if import already exists + if (importList.importStatements.none { it.qualifiedName == qualifiedName }) { + val importStatement = psiElementFactory.createImportStatement( + JavaPsiFacade.getInstance(project).findClass(qualifiedName, psiMethod.resolveScope) ?: return + ) + importList.add(importStatement) + } + } + private fun buildAnnotation(schema: AnnotationSchema): PsiAnnotation { // Build the annotation text with default values val requiredFields = schema.fields.filter { it.required } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt index db70a80..d4a6c51 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/AnnotationConfigService.kt @@ -18,7 +18,9 @@ class AnnotationConfigService : PersistentStateComponent() override fun getDisplayName(): String = "Test Annotation Configuration" override fun createComponent(): JComponent { - val project = ProjectManager.getInstance().defaultProject - val editorFactory = EditorFactory.getInstance() + val mainPanel = JBPanel>(VerticalLayout(10)) + + // Import Settings Section (now at top) + mainPanel.add(createImportSettingsPanel()) + + // Schema Editor Section + mainPanel.add(JBLabel("Annotation Schema:")) + mainPanel.add(createSchemaEditor()) + + // Buttons Panel + mainPanel.add(createButtonsPanel()) + + // Help Panel + mainPanel.add(createHelpPanel()) + return mainPanel + } + + private fun createSchemaEditor(): JComponent { + val project = ProjectManager.getInstance().defaultProject + editor = EditorTextField( EditorFactory.getInstance().createDocument(configService.state.schemaJson), project, @@ -48,41 +74,70 @@ class AnnotationConfigurable : Configurable { } } } - - val mainPanel = JBPanel>(VerticalLayout(10)) - - // Add editor with scroll pane - mainPanel.add(JBScrollPane(editor)) - // Add buttons panel - val buttonsPanel = JPanel(FlowLayout(FlowLayout.LEFT)) - val restoreDefaultsButton = JButton("Restore Defaults").apply { - addActionListener { - editor?.text = AnnotationSchema.DEFAULT_SCHEMA - } + return JBScrollPane(editor) + } + + private fun createImportSettingsPanel(): JComponent { + val panel = JPanel(GridBagLayout()) + val gbc = GridBagConstraints().apply { + insets = Insets(5, 5, 5, 5) + fill = GridBagConstraints.HORIZONTAL + anchor = GridBagConstraints.BASELINE_LEADING // Align items on the same line } - buttonsPanel.add(restoreDefaultsButton) - mainPanel.add(buttonsPanel) - - // Add help panel - mainPanel.add(createHelpPanel()) - return mainPanel + // Package Label + gbc.gridx = 0 + gbc.gridy = 0 + gbc.weightx = 0.0 + panel.add(JBLabel("Annotation Package:"), gbc) + + // Package TextField + packageTextField = JTextField(configService.getAnnotationPackage()).apply { + preferredSize = Dimension(200, preferredSize.height) + } + gbc.gridx = 1 + gbc.weightx = 1.0 + panel.add(packageTextField, gbc) + + // Auto Import Checkbox + autoImportCheckbox = JCheckBox("Auto Import", configService.isAutoImport()) + gbc.gridx = 2 + gbc.weightx = 0.0 + gbc.insets.left = 20 // Add some space between textfield and checkbox + panel.add(autoImportCheckbox, gbc) + + return panel + } + + private fun createButtonsPanel(): JComponent { + return JPanel(FlowLayout(FlowLayout.LEFT)).apply { + add(JButton("Restore Defaults").apply { + addActionListener { + editor?.text = AnnotationSchema.DEFAULT_SCHEMA + packageTextField?.text = "com.example.unittest.annotations" + autoImportCheckbox?.isSelected = true + } + }) + } } private fun createHelpPanel(): JComponent { return JPanel(BorderLayout()).apply { - add( - JBLabel(""" + add(JBLabel(""" Define your test annotation schema in JSON format. Available field types: STRING, STRING_LIST, STATUS - Example schema is shown by default. + + Package: The base package for your annotations + Auto Import: Automatically add import statements when generating annotations """.trimIndent()), BorderLayout.CENTER) } } override fun isModified(): Boolean { - return editor?.text != configService.state.schemaJson + return editor?.text != configService.state.schemaJson || + packageTextField?.text != configService.getAnnotationPackage() || + autoImportCheckbox?.isSelected != configService.isAutoImport() } override fun apply() { @@ -91,6 +146,10 @@ class AnnotationConfigurable : Configurable { // Validate JSON format and schema val schema = Json.decodeFromString(jsonText) configService.state.schemaJson = jsonText + + // Update import settings + packageTextField?.text?.let { configService.setAnnotationPackage(it) } + autoImportCheckbox?.isSelected?.let { configService.setAutoImport(it) } } catch (e: Exception) { throw ConfigurationException("Invalid JSON schema: ${e.message}") } @@ -98,9 +157,13 @@ class AnnotationConfigurable : Configurable { override fun reset() { editor?.text = configService.state.schemaJson + packageTextField?.text = configService.getAnnotationPackage() + autoImportCheckbox?.isSelected = configService.isAutoImport() } override fun disposeUIResources() { editor = null + packageTextField = null + autoImportCheckbox = null } } \ No newline at end of file From ede99b2d3fce888787a0ce4718c0927a7a7ce4c4 Mon Sep 17 00:00:00 2001 From: zhislin Date: Fri, 15 Nov 2024 15:20:39 +0800 Subject: [PATCH 15/24] fix import statement --- .../GenerateAnnotationCommand.kt | 46 +++++++++++++++---- .../AnnotationCompletionContributor.kt | 2 +- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt index f6523da..dc6663c 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt @@ -6,12 +6,14 @@ import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.psi.* +import com.intellij.psi.search.GlobalSearchScope class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { private val psiElementFactory: PsiElementFactory = JavaPsiFacade.getInstance(project).elementFactory - private val configService = project.service() + private val configService = service() override fun execute() { val annotation = findTargetAnnotation(context.psiMethod, context.schema) @@ -32,19 +34,43 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni } } - private fun addImportIfNeeded(psiMethod: PsiMethod, annotationClassName: String) { - val file = psiMethod.containingFile - if (file !is PsiJavaFile) return + private val LOG = Logger.getInstance(GenerateAnnotationCommand::class.java) + private fun addImportIfNeeded(psiMethod: PsiMethod, annotationClassName: String) { + val file = psiMethod.containingFile as? PsiJavaFile ?: return + LOG.info("Processing file: ${file.name}") + val importList = file.importList ?: return + LOG.info("Current imports: ${importList.importStatements.joinToString { it.qualifiedName ?: "null" }}") + val qualifiedName = "${configService.getAnnotationPackage()}.$annotationClassName" - - // Check if import already exists + LOG.info("Trying to add import for: $qualifiedName") + + // Only add if not already imported if (importList.importStatements.none { it.qualifiedName == qualifiedName }) { - val importStatement = psiElementFactory.createImportStatement( - JavaPsiFacade.getInstance(project).findClass(qualifiedName, psiMethod.resolveScope) ?: return - ) - importList.add(importStatement) + LOG.info("Import not found, attempting to add") + + val project = file.project + val facade = JavaPsiFacade.getInstance(project) + val scope = GlobalSearchScope.allScope(project) + + LOG.info("Searching for class in global scope") + val psiClass = facade.findClass(qualifiedName, scope) + LOG.info("Found class: ${psiClass != null}") + + try { + val importStatement = psiElementFactory.createImportStatement( + psiClass ?: return + ) + LOG.info("Created import statement: ${importStatement.text}") + + importList.add(importStatement) + LOG.info("Import added successfully") + } catch (e: Exception) { + LOG.error("Failed to add import", e) + } + } else { + LOG.info("Import already exists") } } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt index 6426bf8..34138cc 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionContributor.kt @@ -52,7 +52,7 @@ class AnnotationCompletionContributor : CompletionContributor() { return } val project = parameters.position.project - val configService = project.service() + val configService = service() val schema = configService.getSchema() LOG.info("Schema annotation class: ${schema.annotationClassName}") LOG.info("Actual annotation class: ${annotation.qualifiedName}") From 51401716c9ae825d6c897304c6dda1e32a2b13d3 Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 20 Nov 2024 21:41:03 +0800 Subject: [PATCH 16/24] upodate annotation --- .../annotations/AnnotationSchema.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 4748ade..66068be 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -107,13 +107,21 @@ data class AnnotationSchema( }, "validation": { "validValues": [ - "Boundary Value", - "Equivalence Class", - "Error Handling", - "Performance", + "BoundaryValue", + "NonEmpty", + "ErrorHandling", + "InputValidation", + "PositiveScenario", + "NegativeScenario", + "EdgeCase", + "Functionality", + "BusinessLogicValidation", + "BusinessInputOutput", + "SideEffects", + "StateTransition", + "BusinessCalculation", "Security", - "Integration", - "Edge Case" + "Performance" ], "allowCustomValues": true, "minLength": 1, From 4b9850b562c7c78f2e25302f0f0abaf3a9352b91 Mon Sep 17 00:00:00 2001 From: zhislin Date: Sun, 24 Nov 2024 14:34:00 +0800 Subject: [PATCH 17/24] add all annotations --- .../GenerateAnnotationCommandAction.kt | 11 +- .../actions/RunCaseAnnoationCheckAction.kt | 11 +- .../RunTestFileAnnoationCheckAction.kt | 5 +- .../GenerateAnnotationCommand.kt | 170 +++++++++++++++++- .../UnittestCaseCheckCommand.kt | 27 +++ .../context/CaseCheckContext.kt | 5 +- .../inspectors/UnittestInspector.kt | 6 +- .../completion/AnnotationCompletionTest.kt | 4 +- 8 files changed, 221 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt index 3e87e9b..c0cb62c 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/GenerateAnnotationCommandAction.kt @@ -5,6 +5,7 @@ import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiClass import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiMethod import com.intellij.psi.util.PsiTreeUtil @@ -13,18 +14,20 @@ import com.intellij.psi.util.PsiTreeUtil class GenerateAnnotationCommandAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { - val psiMethod = findMethodAtCaret(e) ?: return - val context = CaseCheckContext.create(psiMethod) + val psiMethodInfo = findMethodAtCaret(e) ?: return + val context = CaseCheckContext.create(psiMethodInfo.first, psiMethodInfo.second) GenerateAnnotationCommand(e.project!!, context).execute() } - private fun findMethodAtCaret(e: AnActionEvent): PsiMethod? { + private fun findMethodAtCaret(e: AnActionEvent): Pair? { val project = e.project ?: return null val editor = e.dataContext.getData(CommonDataKeys.EDITOR) ?: return null val caret = e.dataContext.getData(CommonDataKeys.CARET) ?: return null val elementAtCaret = PsiDocumentManager.getInstance(project) .getPsiFile(editor.document)?.findElementAt(caret.offset) ?: return null - return PsiTreeUtil.getParentOfType(elementAtCaret, PsiMethod::class.java) + val method = PsiTreeUtil.getParentOfType(elementAtCaret, PsiMethod::class.java) ?: return null + val containingClass = PsiTreeUtil.getParentOfType(method, PsiClass::class.java) ?: return null + return method to containingClass } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt index 2c4de84..91540c5 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunCaseAnnoationCheckAction.kt @@ -5,6 +5,7 @@ import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.psi.PsiClass import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiMethod import com.intellij.psi.util.PsiTreeUtil @@ -12,18 +13,20 @@ import com.intellij.psi.util.PsiTreeUtil class RunCaseAnnoationCheckAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { - val psiMethod = findMethodAtCaret(e) ?: return - val context = CaseCheckContext.create(psiMethod) + val psiMethodInfo = findMethodAtCaret(e) ?: return + val context = CaseCheckContext.create(psiMethodInfo.first, psiMethodInfo.second) CheckAnnotationCommand(e.project!!, context).execute() } - private fun findMethodAtCaret(e: AnActionEvent): PsiMethod? { + private fun findMethodAtCaret(e: AnActionEvent): Pair? { val project = e.project ?: return null val editor = e.dataContext.getData(CommonDataKeys.EDITOR) ?: return null val caret = e.dataContext.getData(CommonDataKeys.CARET) ?: return null val elementAtCaret = PsiDocumentManager.getInstance(project) .getPsiFile(editor.document)?.findElementAt(caret.offset) ?: return null - return PsiTreeUtil.getParentOfType(elementAtCaret, PsiMethod::class.java) + val method = PsiTreeUtil.getParentOfType(elementAtCaret, PsiMethod::class.java) ?: return null + val containingClass = PsiTreeUtil.getParentOfType(method, PsiClass::class.java) ?: return null + return method to containingClass } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt index aea588a..2da3ca0 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/actions/RunTestFileAnnoationCheckAction.kt @@ -6,8 +6,10 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.psi.JavaRecursiveElementVisitor +import com.intellij.psi.PsiClass import com.intellij.psi.PsiJavaFile import com.intellij.psi.PsiMethod +import com.intellij.psi.util.PsiTreeUtil class RunTestFileAnnoationCheckAction: AnAction() { @@ -29,7 +31,8 @@ class RunTestFileAnnoationCheckAction: AnAction() { val annotationName = annotation.qualifiedName if (annotationName!=null && // to support junit 4 & 5, do not use regexp, as it will also match some beforeTest/afterTest annotations (annotationName == "org.junit.Test" || annotationName == "org.junit.jupiter.api.Test" || annotationName == "Test")) { - val context = CaseCheckContext.create(method) + val psiClass = PsiTreeUtil.getParentOfType(method, PsiClass::class.java) ?: return + val context = CaseCheckContext.create(method, psiClass) CheckAnnotationCommand(e.project!!, context).execute() break } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt index dc6663c..77738ee 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt @@ -8,20 +8,182 @@ import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper import com.intellij.psi.* import com.intellij.psi.search.GlobalSearchScope +import com.intellij.ui.CheckboxTree +import com.intellij.ui.CheckboxTreeListener +import com.intellij.ui.CheckedTreeNode +import com.intellij.ui.components.JBScrollPane +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.FlowLayout +import javax.swing.* class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { private val psiElementFactory: PsiElementFactory = JavaPsiFacade.getInstance(project).elementFactory private val configService = service() override fun execute() { - val annotation = findTargetAnnotation(context.psiMethod, context.schema) - if (annotation != null) { - showAnnotationAlreadyExistMessage(project, context.schema.annotationClassName) + if (context.psiClass.methods.isEmpty()) { + showNoMethodMessage(project) return } - generateAnnotation(context.psiMethod, context.schema) + generateAnnotationForSelectedMethod() + } + + private fun generateAnnotationForSelectedMethod() { + val psiClass = context.psiClass + val testMethods = psiClass.methods.filter { canAddAnnotation(it) } + + if (testMethods.isEmpty()) { + showNoTestMethodCanAddMessage(project) + return + } + + val methodNames = testMethods.map { it.name }.toTypedArray() + val selected = BooleanArray(methodNames.size) { true } + + val dialog = object : DialogWrapper(project) { + private val tree: CheckboxTree = createMethodSelectionTree(methodNames, selected) + private val buttonPanel = JPanel(FlowLayout(FlowLayout.LEFT)) + + init { + init() + title = "Select Test Methods" + createButtons() + } + + private fun createButtons() { + val checkAllButton = JButton("Check All").apply { + addActionListener { + setAllNodesChecked(true) + } + } + + val uncheckAllButton = JButton("Uncheck All").apply { + addActionListener { + setAllNodesChecked(false) + } + } + + buttonPanel.add(checkAllButton) + buttonPanel.add(uncheckAllButton) + } + + private fun setAllNodesChecked(checked: Boolean) { + val root = tree.model.root as CheckedTreeNode + setNodeAndChildrenChecked(root, checked) + tree.repaint() + } + + private fun setNodeAndChildrenChecked(node: CheckedTreeNode, checked: Boolean) { + node.isChecked = checked + for (i in 0 until node.childCount) { + val child = node.getChildAt(i) as? CheckedTreeNode ?: continue + setNodeAndChildrenChecked(child, checked) + + // Update the selected array if this is a leaf node (method) + if (child.userObject is String) { + val index = methodNames.indexOf(child.userObject as String) + if (index >= 0) { + selected[index] = checked + } + } + } + } + + override fun createCenterPanel(): JComponent { + val panel = JPanel(BorderLayout()) + panel.preferredSize = Dimension(400, 400) + + // Add the button panel at the top + panel.add(buttonPanel, BorderLayout.NORTH) + + // Add the tree with scroll pane in the center + val treePanel = JPanel(BorderLayout()) + treePanel.border = BorderFactory.createEmptyBorder(5, 5, 5, 5) + treePanel.add(JBScrollPane(tree), BorderLayout.CENTER) + panel.add(treePanel, BorderLayout.CENTER) + + return panel + } + } + + if (dialog.showAndGet()) { + // Process selected methods after OK is clicked + testMethods.filterIndexed { index, _ -> selected[index] } + .forEach { method -> + generateAnnotationForSingleMethod(method) + } + } + } + + private fun createMethodSelectionTree(methodNames: Array, selected: BooleanArray): CheckboxTree { + val root = CheckedTreeNode("Test Methods") + + methodNames.forEachIndexed { index, name -> + val node = CheckedTreeNode(name) + node.isChecked = selected[index] + root.add(node) + } + + return CheckboxTree( + object : CheckboxTree.CheckboxTreeCellRenderer() { + override fun customizeRenderer( + tree: JTree, + value: Any, + selected: Boolean, + expanded: Boolean, + leaf: Boolean, + row: Int, + hasFocus: Boolean + ) { + if (value is CheckedTreeNode) { + when (value.userObject) { + is String -> textRenderer.append(value.userObject as String) + else -> textRenderer.append(value.userObject.toString()) + } + } + } + }, + root + ).apply { + addCheckboxTreeListener(object : CheckboxTreeListener { + override fun nodeStateChanged(node: CheckedTreeNode) { + if (node.userObject is String) { + val index = methodNames.indexOf(node.userObject as String) + if (index >= 0) { + selected[index] = node.isChecked + } + } + } + }) + isRootVisible = false // Hide the root node + showsRootHandles = true // Show handles for the root's children + } + } + + + + private fun isMethodJunitTestMethod(psiMethod: PsiMethod): Boolean { + val annotations = psiMethod.annotations + return annotations.any { it.qualifiedName == "org.junit.Test" || it.qualifiedName == "org.junit.jupiter.api.Test" || it.qualifiedName == "Test"} + } + + private fun canAddAnnotation(psiMethod: PsiMethod): Boolean { + if (!isMethodJunitTestMethod(psiMethod)) { + return false + } + val annotation = findTargetAnnotation(psiMethod, context.schema) + return annotation == null + } + + private fun generateAnnotationForSingleMethod(psiMethod: PsiMethod) { + if (!canAddAnnotation(psiMethod)) { + return + } + generateAnnotation(psiMethod, context.schema) } protected fun generateAnnotation(psiMethod: PsiMethod, schema: AnnotationSchema) { diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestCaseCheckCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestCaseCheckCommand.kt index 4aa9714..47127bf 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestCaseCheckCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/UnittestCaseCheckCommand.kt @@ -39,6 +39,33 @@ abstract class UnittestCaseCheckCommand(protected val project: Project, protecte ) } + fun showNotJunitTestMethodMessage(project: Project) { + Messages.showMessageDialog( + project, + "This method is not a JUnit test method", + "Annotation Generation Action", + Messages.getWarningIcon() + ) + } + + fun showNoMethodMessage(project: Project) { + Messages.showMessageDialog( + project, + "No methods found in this class", + "Annotation Generation Action", + Messages.getWarningIcon() + ) + } + + fun showNoTestMethodCanAddMessage(project: Project) { + Messages.showMessageDialog( + project, + "No test methods found in the class that can add annotation.", + "No Test Methods Can Add Annotation", + Messages.getInformationIcon() + ) + } + fun showAnnotationAlreadyExistMessage(project: Project, annotationName: String) { Messages.showMessageDialog( project, diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt index d657ed5..bdba324 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/context/CaseCheckContext.kt @@ -8,18 +8,21 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import com.intellij.psi.PsiClass import com.intellij.psi.PsiMethod data class CaseCheckContext( + val psiClass: PsiClass, val psiMethod: PsiMethod, val schema: AnnotationSchema, val parser: AnnotationParser ) { companion object { - fun create(psiMethod: PsiMethod): CaseCheckContext { + fun create(psiMethod: PsiMethod, psiClass: PsiClass): CaseCheckContext { val configService = service() val schema = configService.getSchema() return CaseCheckContext( + psiClass = psiClass, psiMethod = psiMethod, schema = schema, parser = AnnotationParser(schema) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt index 6fd36c7..3bc5375 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/inspectors/UnittestInspector.kt @@ -5,6 +5,7 @@ import com.github.jaksonlin.pitestintellij.commands.unittestannotations.Unittest import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.intellij.codeInspection.* import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil import java.util.concurrent.ConcurrentHashMap class UnittestInspector : AbstractBaseJavaLocalInspectionTool() { @@ -39,8 +40,9 @@ class UnittestInspector : AbstractBaseJavaLocalInspectionTool() { // Check containing class using cache val containingClass = psiMethod.containingClass ?: return val qualifiedName = containingClass.qualifiedName ?: return - - val context = CaseCheckContext.create(psiMethod) + // get the psiclass for the containing class + val psiClass = PsiTreeUtil.getParentOfType(psiMethod, PsiClass::class.java) ?: return + val context = CaseCheckContext.create(psiMethod, psiClass) UnittestFileInspectorCommand(holder, project, context).execute() } diff --git a/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt index b6fa67c..75869d0 100644 --- a/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt +++ b/src/test/kotlin/com/github/jaksonlin/pitestintellij/completion/AnnotationCompletionTest.kt @@ -98,8 +98,8 @@ class AnnotationCompletionTest : LightJavaCodeInsightFixtureTestCase(), TestBase val completions = myFixture.completeBasic() assertNotNull(completions) - assertTrue(completions.any { it.lookupString == "Boundary Value" }) - assertTrue(completions.any { it.lookupString == "Error Handling" }) + assertTrue(completions.any { it.lookupString == "BoundaryValue" }) + assertTrue(completions.any { it.lookupString == "ErrorHandling" }) assertTrue(completions.any { it.lookupString == "Performance" }) assertTrue(completions.any { it.lookupString == "Security" }) } From e0447ad768ffee79bf4457e11d0d783a69a793ad Mon Sep 17 00:00:00 2001 From: zhislin Date: Sun, 24 Nov 2024 22:20:59 +0800 Subject: [PATCH 18/24] add git4idea --- build.gradle.kts | 5 +- gradle.properties | 2 +- .../annotations/AnnotationFieldConfig.kt | 5 +- .../annotations/AnnotationSchema.kt | 40 ++- .../annotations/ValueProvider.kt | 24 ++ .../GenerateAnnotationCommand.kt | 232 ++++++++++-------- .../services/ValueProviderService.kt | 87 +++++++ .../jaksonlin/pitestintellij/util/GitUtil.kt | 188 ++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 9 files changed, 480 insertions(+), 104 deletions(-) create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt create mode 100644 src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt diff --git a/build.gradle.kts b/build.gradle.kts index b57b759..b2400e3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,7 +30,10 @@ intellij { type.set(providers.gradleProperty("platformType")) // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. - plugins.set(providers.gradleProperty("platformBundledPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) }) + plugins.set(providers.gradleProperty("platformBundledPlugins") + .map { it.split(',').map(String::trim).filter(String::isNotEmpty) } + .map { it + listOf("git4idea") } + ) } // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog diff --git a/gradle.properties b/gradle.properties index ea0a1a5..058419d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ platformVersion = 2022.2 # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP platformPlugins = # Example: platformBundledPlugins = com.intellij.java -platformBundledPlugins = com.intellij.java, com.intellij.gradle +platformBundledPlugins = com.intellij.java, com.intellij.gradle,com.intellij.java,git4idea # Gradle Releases -> https://github.com/gradle/gradle/releases gradleVersion = 8.9 diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt index 23d2cca..acc839c 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationFieldConfig.kt @@ -18,7 +18,7 @@ sealed class DefaultValue { @Serializable @SerialName("NullValue") - data object NullValue : DefaultValue() + object NullValue : DefaultValue() } @@ -28,5 +28,6 @@ data class AnnotationFieldConfig( val type: AnnotationFieldType, val required: Boolean = false, val defaultValue: DefaultValue = DefaultValue.NullValue, - val validation: FieldValidation? = null + val validation: FieldValidation? = null, + val valueProvider: ValueProvider? = null ) \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 66068be..620faad 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -1,6 +1,7 @@ package com.github.jaksonlin.pitestintellij.annotations import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json @Serializable data class AnnotationSchema( @@ -8,7 +9,11 @@ data class AnnotationSchema( val fields: List ) { companion object { - // Default schema matching UnittestCaseInfoContextG + private val json = Json { + ignoreUnknownKeys = true + isLenient = true + } + val DEFAULT_SCHEMA = """ { "annotationClassName": "UnittestCaseInfo", @@ -17,6 +22,9 @@ data class AnnotationSchema( "name": "author", "type": "STRING", "required": true, + "valueProvider": { + "type": "GIT_AUTHOR" + }, "validation": { "allowEmpty": false }, @@ -29,6 +37,9 @@ data class AnnotationSchema( "name": "title", "type": "STRING", "required": true, + "valueProvider": { + "type": "METHOD_NAME_BASED" + }, "validation": { "allowEmpty": false }, @@ -41,6 +52,9 @@ data class AnnotationSchema( "name": "targetClass", "type": "STRING", "required": true, + "valueProvider": { + "type": "CLASS_NAME" + }, "validation": { "allowEmpty": false }, @@ -53,6 +67,9 @@ data class AnnotationSchema( "name": "targetMethod", "type": "STRING", "required": true, + "valueProvider": { + "type": "METHOD_NAME" + }, "validation": { "allowEmpty": false }, @@ -65,6 +82,10 @@ data class AnnotationSchema( "name": "lastUpdateTime", "type": "STRING", "required": true, + "valueProvider": { + "type": "LAST_MODIFIER_TIME", + "format": "yyyy-MM-dd HH:mm:ss" + }, "validation": { "allowEmpty": false }, @@ -77,6 +98,9 @@ data class AnnotationSchema( "name": "lastUpdateAuthor", "type": "STRING", "required": true, + "valueProvider": { + "type": "LAST_MODIFIER_AUTHOR" + }, "validation": { "allowEmpty": false }, @@ -89,6 +113,9 @@ data class AnnotationSchema( "name": "methodSignature", "type": "STRING", "required": true, + "valueProvider": { + "type": "METHOD_SIGNATURE" + }, "validation": { "allowEmpty": false }, @@ -101,6 +128,10 @@ data class AnnotationSchema( "name": "testPoints", "type": "STRING_LIST", "required": true, + "valueProvider": { + "type": "FIXED_VALUE", + "value": ["Functionality"] + }, "defaultValue": { "type": "StringListValue", "value": [] @@ -133,6 +164,10 @@ data class AnnotationSchema( "name": "status", "type": "STRING", "required": false, + "valueProvider": { + "type": "FIXED_VALUE", + "value": "TODO" + }, "defaultValue": { "type": "StringValue", "value": "TODO" @@ -154,6 +189,9 @@ data class AnnotationSchema( "name": "description", "type": "STRING", "required": true, + "valueProvider": { + "type": "METHOD_NAME_BASED" + }, "validation": { "allowEmpty": false }, diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt new file mode 100644 index 0000000..6ce3f25 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt @@ -0,0 +1,24 @@ +package com.github.jaksonlin.pitestintellij.annotations + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class ValueProvider( + val type: ValueProviderType, + val format: String? = null, + val value: JsonElement? = null +) + +@Serializable +enum class ValueProviderType { + GIT_AUTHOR, + LAST_MODIFIER_AUTHOR, + LAST_MODIFIER_TIME, + CURRENT_DATE, + METHOD_NAME_BASED, + FIXED_VALUE, + CLASS_NAME, + METHOD_NAME, + METHOD_SIGNATURE +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt index 77738ee..d0cc21b 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt @@ -1,12 +1,19 @@ package com.github.jaksonlin.pitestintellij.commands.unittestannotations +import com.github.jaksonlin.pitestintellij.annotations.AnnotationFieldType import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.github.jaksonlin.pitestintellij.annotations.DefaultValue import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.github.jaksonlin.pitestintellij.services.AnnotationConfigService +import com.github.jaksonlin.pitestintellij.services.ValueProviderService +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.psi.* @@ -23,13 +30,21 @@ import javax.swing.* class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):UnittestCaseCheckCommand(project, context) { private val psiElementFactory: PsiElementFactory = JavaPsiFacade.getInstance(project).elementFactory private val configService = service() - + private val valueProviderService = project.service() override fun execute() { - if (context.psiClass.methods.isEmpty()) { - showNoMethodMessage(project) - return - } - generateAnnotationForSelectedMethod() + ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Generating Annotations") { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = true + + if (context.psiClass.methods.isEmpty()) { + ApplicationManager.getApplication().invokeLater { + showNoMethodMessage(project) + } + return + } + generateAnnotationForSelectedMethod() + } + }) } private fun generateAnnotationForSelectedMethod() { @@ -37,85 +52,95 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni val testMethods = psiClass.methods.filter { canAddAnnotation(it) } if (testMethods.isEmpty()) { - showNoTestMethodCanAddMessage(project) + ApplicationManager.getApplication().invokeLater { + showNoTestMethodCanAddMessage(project) + } return } - val methodNames = testMethods.map { it.name }.toTypedArray() - val selected = BooleanArray(methodNames.size) { true } - - val dialog = object : DialogWrapper(project) { - private val tree: CheckboxTree = createMethodSelectionTree(methodNames, selected) - private val buttonPanel = JPanel(FlowLayout(FlowLayout.LEFT)) + // UI interaction needs to happen on EDT + ApplicationManager.getApplication().invokeLater { + val methodNames = testMethods.map { it.name }.toTypedArray() + val selected = BooleanArray(methodNames.size) { true } + + val dialog = object : DialogWrapper(project) { + private val tree: CheckboxTree = createMethodSelectionTree(methodNames, selected) + private val buttonPanel = JPanel(FlowLayout(FlowLayout.LEFT)) - init { - init() - title = "Select Test Methods" - createButtons() - } + init { + init() + title = "Select Test Methods" + createButtons() + } - private fun createButtons() { - val checkAllButton = JButton("Check All").apply { - addActionListener { - setAllNodesChecked(true) + private fun createButtons() { + val checkAllButton = JButton("Check All").apply { + addActionListener { + setAllNodesChecked(true) + } } - } - - val uncheckAllButton = JButton("Uncheck All").apply { - addActionListener { - setAllNodesChecked(false) + + val uncheckAllButton = JButton("Uncheck All").apply { + addActionListener { + setAllNodesChecked(false) + } } - } - buttonPanel.add(checkAllButton) - buttonPanel.add(uncheckAllButton) - } + buttonPanel.add(checkAllButton) + buttonPanel.add(uncheckAllButton) + } - private fun setAllNodesChecked(checked: Boolean) { - val root = tree.model.root as CheckedTreeNode - setNodeAndChildrenChecked(root, checked) - tree.repaint() - } + private fun setAllNodesChecked(checked: Boolean) { + val root = tree.model.root as CheckedTreeNode + setNodeAndChildrenChecked(root, checked) + tree.repaint() + } - private fun setNodeAndChildrenChecked(node: CheckedTreeNode, checked: Boolean) { - node.isChecked = checked - for (i in 0 until node.childCount) { - val child = node.getChildAt(i) as? CheckedTreeNode ?: continue - setNodeAndChildrenChecked(child, checked) - - // Update the selected array if this is a leaf node (method) - if (child.userObject is String) { - val index = methodNames.indexOf(child.userObject as String) - if (index >= 0) { - selected[index] = checked + private fun setNodeAndChildrenChecked(node: CheckedTreeNode, checked: Boolean) { + node.isChecked = checked + for (i in 0 until node.childCount) { + val child = node.getChildAt(i) as? CheckedTreeNode ?: continue + setNodeAndChildrenChecked(child, checked) + + // Update the selected array if this is a leaf node (method) + if (child.userObject is String) { + val index = methodNames.indexOf(child.userObject as String) + if (index >= 0) { + selected[index] = checked + } } } } - } - - override fun createCenterPanel(): JComponent { - val panel = JPanel(BorderLayout()) - panel.preferredSize = Dimension(400, 400) - - // Add the button panel at the top - panel.add(buttonPanel, BorderLayout.NORTH) - - // Add the tree with scroll pane in the center - val treePanel = JPanel(BorderLayout()) - treePanel.border = BorderFactory.createEmptyBorder(5, 5, 5, 5) - treePanel.add(JBScrollPane(tree), BorderLayout.CENTER) - panel.add(treePanel, BorderLayout.CENTER) - - return panel - } - } - if (dialog.showAndGet()) { - // Process selected methods after OK is clicked - testMethods.filterIndexed { index, _ -> selected[index] } - .forEach { method -> - generateAnnotationForSingleMethod(method) + override fun createCenterPanel(): JComponent { + val panel = JPanel(BorderLayout()) + panel.preferredSize = Dimension(400, 400) + + // Add the button panel at the top + panel.add(buttonPanel, BorderLayout.NORTH) + + // Add the tree with scroll pane in the center + val treePanel = JPanel(BorderLayout()) + treePanel.border = BorderFactory.createEmptyBorder(5, 5, 5, 5) + treePanel.add(JBScrollPane(tree), BorderLayout.CENTER) + panel.add(treePanel, BorderLayout.CENTER) + + return panel } + } + + if (dialog.showAndGet()) { + // Back to background thread for processing + ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Applying Annotations") { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = true + testMethods.filterIndexed { index, _ -> selected[index] } + .forEach { method -> + generateAnnotationForSingleMethod(method) + } + } + }) + } } } @@ -172,26 +197,29 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni } private fun canAddAnnotation(psiMethod: PsiMethod): Boolean { - if (!isMethodJunitTestMethod(psiMethod)) { - return false + return ApplicationManager.getApplication().runReadAction { + isMethodJunitTestMethod(psiMethod) && findTargetAnnotation(psiMethod, context.schema) == null } - val annotation = findTargetAnnotation(psiMethod, context.schema) - return annotation == null } private fun generateAnnotationForSingleMethod(psiMethod: PsiMethod) { - if (!canAddAnnotation(psiMethod)) { - return + ApplicationManager.getApplication().executeOnPooledThread { + generateAnnotation(psiMethod, context.schema) } - generateAnnotation(psiMethod, context.schema) } protected fun generateAnnotation(psiMethod: PsiMethod, schema: AnnotationSchema) { + // First compute the annotation text in a read action + val annotationText = ReadAction.compute { + buildAnnotationStr(schema) + } + + // Then use the computed text in the write action WriteCommandAction.runWriteCommandAction(project) { if (configService.isAutoImport()) { addImportIfNeeded(psiMethod, schema.annotationClassName) } - val annotation = buildAnnotation(schema) + val annotation = buildAnnotation(annotationText) psiMethod.modifierList.addAfter(annotation, null) } } @@ -236,31 +264,37 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni } } - private fun buildAnnotation(schema: AnnotationSchema): PsiAnnotation { - // Build the annotation text with default values - val requiredFields = schema.fields.filter { it.required } - - val fieldValues = requiredFields.joinToString(",\n ") { field -> - val defaultValueStr = when (val default = field.defaultValue) { - is DefaultValue.StringValue -> "\"${default.value}\"" - is DefaultValue.StringListValue -> { - val values = default.value.joinToString("\", \"") { "\"$it\"" } - if (values.isEmpty()) "{}" else "{$values}" + // we should do this in a read action + private fun buildAnnotationStr(schema: AnnotationSchema) : String { + val annotationText = buildString { + append("@${schema.annotationClassName}(\n") + schema.fields.filter{ it.required }.forEachIndexed { index, field -> + if (index > 0) append(",\n") + append(" ${field.name} = ") + + // Use value provider if available + val value = field.valueProvider?.let { provider -> + valueProviderService.provideValue(provider, context) + } ?: field.defaultValue + + when (field.type) { + AnnotationFieldType.STRING -> append("\"$value\"") + AnnotationFieldType.STRING_LIST -> { + val list = (value as? List<*>)?.joinToString(", ") { + val str = it.toString() + if (str.startsWith("\"") && str.endsWith("\"")) str else "\"$str\"" + } + append("{$list}") + } } - is DefaultValue.NullValue -> "null" - null -> "\"\"" // Empty string for required fields without default } - "${field.name} = $defaultValueStr" - } - - val annotationText = if (fieldValues.isEmpty()) { - "@${schema.annotationClassName}" - } else { - """@${schema.annotationClassName}( - $fieldValues -)""" + append("\n)") } + return annotationText + } + private fun buildAnnotation(annotationText:String): PsiAnnotation { + // Build the annotation text with default values return psiElementFactory.createAnnotationFromText(annotationText, null) } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt new file mode 100644 index 0000000..71abce2 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt @@ -0,0 +1,87 @@ +package com.github.jaksonlin.pitestintellij.services + +import com.github.jaksonlin.pitestintellij.annotations.AnnotationFieldType +import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema +import com.github.jaksonlin.pitestintellij.annotations.ValueProvider +import com.github.jaksonlin.pitestintellij.annotations.ValueProviderType +import com.github.jaksonlin.pitestintellij.context.CaseCheckContext +import com.github.jaksonlin.pitestintellij.utils.GitUtil +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import java.text.SimpleDateFormat +import java.util.* + +@Service(Service.Level.PROJECT) +class ValueProviderService(private val project: Project) { + fun provideValue(provider: ValueProvider, context: CaseCheckContext): Any? { + return when (provider.type) { + ValueProviderType.GIT_AUTHOR -> getGitAuthor() + ValueProviderType.LAST_MODIFIER_AUTHOR -> getLastModifierAuthor(context.psiMethod) + ValueProviderType.LAST_MODIFIER_TIME -> getLastModifierTime(context.psiMethod) + ValueProviderType.CURRENT_DATE -> getCurrentDate(provider.format) + ValueProviderType.METHOD_NAME_BASED -> generateDescription(context.psiMethod) + ValueProviderType.FIXED_VALUE -> provider.value + ValueProviderType.CLASS_NAME -> getClassName(context.psiClass) + ValueProviderType.METHOD_NAME -> getMethodName(context.psiMethod) + ValueProviderType.METHOD_SIGNATURE -> getMethodSignature(context.psiMethod) + } + } + + private fun getGitAuthor(): String { + return GitUtil.getGitUserInfo(project).toString() + } + + private fun getLastModifierAuthor(psiMethod: PsiMethod): String { + return GitUtil.getLastModifyInfo(project, psiMethod)?.toString() + ?: getGitAuthor() + } + + private fun getLastModifierTime(psiMethod: PsiMethod): String { + var timestamp = GitUtil.getLastModifyInfo(project, psiMethod)?.timestamp + timestamp = timestamp?.times(1000) + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + return dateFormat.format(Date((timestamp ?: System.currentTimeMillis()))) + } + + private fun getCurrentDate(format: String?): String { + val dateFormat = SimpleDateFormat(format ?: "yyyy-MM-dd HH:mm:ss") + return dateFormat.format(Date()) + } + + private fun generateDescription(psiMethod: PsiMethod): String { + val methodName = psiMethod.name + // Convert camelCase or snake_case to space-separated words + return methodName.replace(Regex("([a-z])([A-Z])"), "$1 $2") + .replace("_", " ") + .lowercase() + .capitalizeFirst() + } + + private fun getClassName(psiClass: PsiClass): String { + return psiClass.qualifiedName ?: psiClass.name ?: "" + } + + private fun getMethodName(psiMethod: PsiMethod): String { + return psiMethod.name + } + + private fun getMethodSignature(psiMethod: PsiMethod): String { + return buildString { + append(psiMethod.name) + append("(") + append(psiMethod.parameterList.parameters.joinToString(", ") { param -> + "${param.name}: ${param.type.presentableText}" + }) + append(")") + psiMethod.returnType?.let { append(": ${it.presentableText}") } + } + } +} + +private fun String.capitalizeFirst(): String { + return if (isNotEmpty()) { + this[0].uppercase() + substring(1) + } else this +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt new file mode 100644 index 0000000..27f7412 --- /dev/null +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt @@ -0,0 +1,188 @@ +package com.github.jaksonlin.pitestintellij.utils + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import git4idea.GitUtil +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import git4idea.commands.Git +import git4idea.commands.GitCommand +import git4idea.commands.GitLineHandler +import git4idea.config.GitConfigUtil +import git4idea.repo.GitConfig +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.psi.PsiMethod + +object GitUtil { + fun getRepositoryManager(project: Project): GitRepositoryManager { + return GitRepositoryManager.getInstance(project) + } + + fun getCurrentRepository(project: Project, element: PsiElement): GitRepository? { + val file = element.containingFile?.virtualFile + return file?.let { getRepositoryForFile(project, it) } + } + + fun getRepositoryForFile(project: Project, file: VirtualFile): GitRepository? { + return GitRepositoryManager.getInstance(project).getRepositoryForFile(file) + } + + fun getGitUserInfo(project: Project): GitUserInfo { + val repository = getRepositoryManager(project).repositories.firstOrNull() + return repository?.let { + val name = ApplicationManager.getApplication().executeOnPooledThread { + GitConfigUtil.getValue(project, repository.root, GitConfigUtil.USER_NAME) + }.get() + val email = ApplicationManager.getApplication().executeOnPooledThread { + GitConfigUtil.getValue(project, repository.root, GitConfigUtil.USER_EMAIL) + }.get() + GitUserInfo( + name = name ?: "Unknown", + email = email ?: "unknown@email.com" + ) + } ?: GitUserInfo("Unknown", "unknown@email.com") + } + + + + fun getLastCommitInfo(project: Project, file: PsiFile): GitCommitInfo? { + val repository = file.virtualFile?.let { getRepositoryForFile(project, it) } ?: return null + val git = Git.getInstance() + + val handler = GitLineHandler( + project, + repository.root, + GitCommand.LOG + ).apply { + addParameters( + "--max-count=1", + "--pretty=format:%an|%ae|%ad|%s", + "--date=format:%Y-%m-%d %H:%M:%S", + "--", + file.virtualFile.path + ) + } + val output = ApplicationManager.getApplication().executeOnPooledThread> { + git.runCommand(handler).output + }.get() + return output.firstOrNull()?.let { line -> + val parts = line.split("|") + if (parts.size >= 4) { + GitCommitInfo( + author = parts[0], + email = parts[1], + date = parts[2], + message = parts[3] + ) + } else null + } + } + + fun getLastModifyInfo(project: Project, psiMethod: PsiMethod): GitUserInfo? { + val file = psiMethod.containingFile?.virtualFile ?: return null + val repository = getRepositoryForFile(project, file) ?: return null + val git = Git.getInstance() + + // Get method's line range + val document = FileDocumentManager.getInstance().getDocument(file) ?: return null + val startOffset = psiMethod.textRange.startOffset + val endOffset = psiMethod.textRange.endOffset + val startLine = document.getLineNumber(startOffset) + 1 // Git blame uses 1-based line numbers + val endLine = document.getLineNumber(endOffset) + 1 + + val handler = GitLineHandler( + project, + repository.root, + GitCommand.BLAME + ).apply { + addParameters( + "-L", "$startLine,$endLine", // Limit blame to method's lines + "--porcelain", // Get detailed output + file.path + ) + } + + val output = ApplicationManager.getApplication().executeOnPooledThread> { + git.runCommand(handler).output + }.get() + + // Parse blame output to get the most recent commit + var latestCommit: BlameInfo? = null + var currentCommit: BlameInfo? = null + + output.forEach { line -> + when { + line.startsWith("author ") -> { + val authorName = line.substringAfter("author ").trim() + if (authorName == "Not Committed Yet") { + val gitUserInfo = getGitUserInfo(project) + currentCommit = currentCommit?.copy( + author = gitUserInfo.name, + email = gitUserInfo.email + ) ?: BlameInfo(author = gitUserInfo.name, email = gitUserInfo.email) + } else { + currentCommit = currentCommit?.copy( + author = authorName + ) ?: BlameInfo(author = authorName) + } + } + line.startsWith("author-mail ") -> { + val email = line.substringAfter("author-mail ").trim().removeSurrounding("<", ">") + if (email == "not.committed.yet") { + val gitUserInfo = getGitUserInfo(project) + currentCommit = currentCommit?.copy( + author = gitUserInfo.name, + email = gitUserInfo.email + ) ?: BlameInfo(author = gitUserInfo.name, email = gitUserInfo.email) + } else { + currentCommit = currentCommit?.copy( + email = email + ) ?: BlameInfo(author = currentCommit?.author ?: "", email = email) + } + } + line.startsWith("author-time ") -> { + val timestamp = line.substringAfter("author-time ").trim().toLongOrNull() + if (timestamp != null) { + currentCommit = currentCommit?.copy(timestamp = timestamp) + if (latestCommit == null || (currentCommit?.timestamp ?: 0) > (latestCommit?.timestamp ?: 0)) { + currentCommit?.let { latestCommit = it } + } + } + currentCommit = null // Reset for next commit + } + } + } + + return latestCommit?.let { + GitUserInfo( + name = it.author, + email = it.email ?: "unknown@email.com", + timestamp = it.timestamp ?: System.currentTimeMillis() // If timestamp is not available, use current time + ) + } + } + + private data class BlameInfo( + val author: String, + val email: String? = null, + val timestamp: Long = 0 + ) +} + +data class GitUserInfo( + val name: String, + val email: String, + val timestamp: Long = 0 +) { + override fun toString(): String = "$name <$email>" +} + +data class GitCommitInfo( + val author: String, + val email: String, + val date: String, + val message: String +) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 155dbd5..4295574 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -31,6 +31,7 @@ com.intellij.modules.platform com.intellij.modules.java + Git4Idea messages.MyBundle From d1923cf9f1293817820752d1eb7d4fca6b1b2fab Mon Sep 17 00:00:00 2001 From: zhislin Date: Sun, 24 Nov 2024 23:10:38 +0800 Subject: [PATCH 19/24] fix creator --- .../annotations/AnnotationSchema.kt | 2 +- .../annotations/ValueProvider.kt | 2 + .../GenerateAnnotationCommand.kt | 6 ++ .../services/ValueProviderService.kt | 18 ++++- .../jaksonlin/pitestintellij/util/GitUtil.kt | 70 ++++++++++++++++++- 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt index 620faad..d41e519 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/AnnotationSchema.kt @@ -23,7 +23,7 @@ data class AnnotationSchema( "type": "STRING", "required": true, "valueProvider": { - "type": "GIT_AUTHOR" + "type": "FIRST_CREATOR_AUTHOR" }, "validation": { "allowEmpty": false diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt index 6ce3f25..e9dbc35 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/annotations/ValueProvider.kt @@ -13,6 +13,8 @@ data class ValueProvider( @Serializable enum class ValueProviderType { GIT_AUTHOR, + FIRST_CREATOR_AUTHOR, + FIRST_CREATOR_TIME, LAST_MODIFIER_AUTHOR, LAST_MODIFIER_TIME, CURRENT_DATE, diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt index d0cc21b..ac0e5d4 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt @@ -216,6 +216,12 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni // Then use the computed text in the write action WriteCommandAction.runWriteCommandAction(project) { + // Ensure document is committed before PSI modifications + val document = PsiDocumentManager.getInstance(project).getDocument(psiMethod.containingFile) + if (document != null) { + PsiDocumentManager.getInstance(project).commitDocument(document) + } + if (configService.isAutoImport()) { addImportIfNeeded(psiMethod, schema.annotationClassName) } diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt index 71abce2..740e45f 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt @@ -1,11 +1,9 @@ package com.github.jaksonlin.pitestintellij.services -import com.github.jaksonlin.pitestintellij.annotations.AnnotationFieldType -import com.github.jaksonlin.pitestintellij.annotations.AnnotationSchema import com.github.jaksonlin.pitestintellij.annotations.ValueProvider import com.github.jaksonlin.pitestintellij.annotations.ValueProviderType import com.github.jaksonlin.pitestintellij.context.CaseCheckContext -import com.github.jaksonlin.pitestintellij.utils.GitUtil +import com.github.jaksonlin.pitestintellij.util.GitUtil import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project import com.intellij.psi.PsiClass @@ -26,6 +24,8 @@ class ValueProviderService(private val project: Project) { ValueProviderType.CLASS_NAME -> getClassName(context.psiClass) ValueProviderType.METHOD_NAME -> getMethodName(context.psiMethod) ValueProviderType.METHOD_SIGNATURE -> getMethodSignature(context.psiMethod) + ValueProviderType.FIRST_CREATOR_AUTHOR -> getFirstCreatorAuthor(context.psiMethod) + ValueProviderType.FIRST_CREATOR_TIME -> getFirstCreatorTime(context.psiMethod) } } @@ -78,6 +78,18 @@ class ValueProviderService(private val project: Project) { psiMethod.returnType?.let { append(": ${it.presentableText}") } } } + + private fun getFirstCreatorAuthor(psiMethod: PsiMethod): String { + return GitUtil.getFirstCreatorInfo(project, psiMethod)?.toString() + ?: getGitAuthor() + } + + private fun getFirstCreatorTime(psiMethod: PsiMethod): String { + var timestamp = GitUtil.getFirstCreatorInfo(project, psiMethod)?.timestamp + timestamp = timestamp?.times(1000) + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + return dateFormat.format(Date((timestamp ?: System.currentTimeMillis()))) + } } private fun String.capitalizeFirst(): String { diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt index 27f7412..df64ce5 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GitUtil.kt @@ -1,9 +1,8 @@ -package com.github.jaksonlin.pitestintellij.utils +package com.github.jaksonlin.pitestintellij.util import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import git4idea.GitUtil import git4idea.repo.GitRepository import git4idea.repo.GitRepositoryManager import com.intellij.psi.PsiElement @@ -12,7 +11,6 @@ import git4idea.commands.Git import git4idea.commands.GitCommand import git4idea.commands.GitLineHandler import git4idea.config.GitConfigUtil -import git4idea.repo.GitConfig import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.psi.PsiMethod @@ -81,6 +79,72 @@ object GitUtil { } } + fun getFirstCreatorInfo(project: Project, psiMethod: PsiMethod): GitUserInfo? { + val file = psiMethod.containingFile?.virtualFile ?: return null + val repository = getRepositoryForFile(project, file) ?: return null + val git = Git.getInstance() + + // Get method's line range + val document = FileDocumentManager.getInstance().getDocument(file) ?: return null + val startOffset = psiMethod.textRange.startOffset + val endOffset = psiMethod.textRange.endOffset + val startLine = document.getLineNumber(startOffset) + 1 + val endLine = document.getLineNumber(endOffset) + 1 + + val handler = GitLineHandler( + project, + repository.root, + GitCommand.BLAME + ).apply { + addParameters( + "-L", "$startLine,$endLine", + "--porcelain", + file.path + ) + } + + val output = ApplicationManager.getApplication().executeOnPooledThread> { + git.runCommand(handler).output + }.get() + + // Get only the first commit's information + var authorName: String? = null + var email: String? = null + var timestamp: Long? = null + + // Only process lines until we find the first complete record + for (line in output) { + when { + line.startsWith("author ") -> authorName = line.substringAfter("author ").trim() + line.startsWith("author-mail ") -> email = line.substringAfter("author-mail ").trim().removeSurrounding("<", ">") + line.startsWith("author-time ") -> { + timestamp = line.substringAfter("author-time ").trim().toLongOrNull() + if (timestamp != null && authorName != null && email != null) { + break // Exit as soon as we have all needed information + } + } + } + } + + // Handle uncommitted changes + if (authorName == "Not Committed Yet" || email == "not.committed.yet") { + val gitUserInfo = getGitUserInfo(project) + return GitUserInfo( + name = gitUserInfo.name, + email = gitUserInfo.email, + timestamp = timestamp ?: System.currentTimeMillis() + ) + } + + return if (authorName != null && email != null) { + GitUserInfo( + name = authorName, + email = email, + timestamp = timestamp ?: System.currentTimeMillis() + ) + } else null + } + fun getLastModifyInfo(project: Project, psiMethod: PsiMethod): GitUserInfo? { val file = psiMethod.containingFile?.virtualFile ?: return null val repository = getRepositoryForFile(project, file) ?: return null From 09828f3af8b2e8e6eb5d679a8cf684d696f11bf8 Mon Sep 17 00:00:00 2001 From: zhislin Date: Tue, 26 Nov 2024 12:03:37 +0800 Subject: [PATCH 20/24] update find logic --- .../services/ValueProviderService.kt | 80 +++++++++++++++++-- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt index 740e45f..488a161 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt @@ -6,8 +6,11 @@ import com.github.jaksonlin.pitestintellij.context.CaseCheckContext import com.github.jaksonlin.pitestintellij.util.GitUtil import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass +import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod +import com.intellij.psi.search.GlobalSearchScope import java.text.SimpleDateFormat import java.util.* @@ -21,9 +24,10 @@ class ValueProviderService(private val project: Project) { ValueProviderType.CURRENT_DATE -> getCurrentDate(provider.format) ValueProviderType.METHOD_NAME_BASED -> generateDescription(context.psiMethod) ValueProviderType.FIXED_VALUE -> provider.value - ValueProviderType.CLASS_NAME -> getClassName(context.psiClass) - ValueProviderType.METHOD_NAME -> getMethodName(context.psiMethod) - ValueProviderType.METHOD_SIGNATURE -> getMethodSignature(context.psiMethod) + ValueProviderType.CLASS_NAME -> guessClassUnderTestClassName(context.psiClass) // return the qualified name for the class under test + ValueProviderType.METHOD_NAME -> guessMethodUnderTestMethodName(context.psiMethod) // return the method name for the method under test + ValueProviderType.METHOD_SIGNATURE -> tryGetMethodUnderTestSignature(context.psiClass, context.psiMethod) + ?: "" ValueProviderType.FIRST_CREATOR_AUTHOR -> getFirstCreatorAuthor(context.psiMethod) ValueProviderType.FIRST_CREATOR_TIME -> getFirstCreatorTime(context.psiMethod) } @@ -59,23 +63,83 @@ class ValueProviderService(private val project: Project) { .capitalizeFirst() } - private fun getClassName(psiClass: PsiClass): String { - return psiClass.qualifiedName ?: psiClass.name ?: "" + private fun guessClassUnderTestClassName(psiClass: PsiClass): String { + // Get base class name without Test prefix/suffix + val baseClassName = psiClass.name?.removePrefix("Test")?.removeSuffix("Test") ?: return "" + + // Get test class package and normalize it + val testPackage = psiClass.qualifiedName + ?.removeSuffix(psiClass.name!!) + ?.removeSuffix(".") + ?.replace(".test.", ".") + ?.replace(".tests.", ".") + ?.removeSuffix(".test") + ?.removeSuffix(".tests") + ?: return "" + + return "$testPackage.$baseClassName" } - private fun getMethodName(psiMethod: PsiMethod): String { + private fun guessMethodUnderTestMethodName(psiMethod: PsiMethod): String { return psiMethod.name + .removePrefix("test") + .removePrefix("should") + .removePrefix("testShould") + // Take only the part before first underscore if it exists + .split("_") + .first() + .lowercase() // just lowercase for case-insensitive matching + } + + private fun tryGetMethodUnderTestSignature(psiClass: PsiClass, psiMethod: PsiMethod): String? { + val guessedClassName = guessClassUnderTestClassName(psiClass) + val guessedMethodName = guessMethodUnderTestMethodName(psiMethod) + .lowercase() // normalize to lowercase for comparison + + // Find the class under test using PSI API + val project = psiClass.project + val psiManager = PsiManager.getInstance(project) + val psiFacade = JavaPsiFacade.getInstance(project) + + // Try to find the class under test + val classUnderTest = psiFacade.findClass(guessedClassName, GlobalSearchScope.projectScope(project)) + ?: return null + + // Find matching method in the class under test + val methodUnderTest = classUnderTest.methods.firstOrNull { method -> + method.name.lowercase().contains(guessedMethodName.lowercase()) + } ?: return null + + // Return the method signature + return getMethodSignature(methodUnderTest) } private fun getMethodSignature(psiMethod: PsiMethod): String { return buildString { + // Add visibility modifier + append(psiMethod.modifierList.text.trim()) + append(" ") + + // Add return type if not constructor + if (!psiMethod.isConstructor) { + append(psiMethod.returnType?.presentableText ?: "void") + append(" ") + } + + // Add method name and parameters append(psiMethod.name) append("(") append(psiMethod.parameterList.parameters.joinToString(", ") { param -> - "${param.name}: ${param.type.presentableText}" + "${param.type.presentableText} ${param.name}" }) append(")") - psiMethod.returnType?.let { append(": ${it.presentableText}") } + + // Add throws clause if present + val throwsList = psiMethod.throwsList.referencedTypes + if (throwsList.isNotEmpty()) { + append(" throws ") + append(throwsList.joinToString(", ") { it.presentableText }) + } } } From acfa8369f7a2addf3671debc540f740e06d60e78 Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 27 Nov 2024 15:43:59 +0800 Subject: [PATCH 21/24] align with the same setting --- .../pitest/PrepareEnvironmentCommand.kt | 21 ++++++++------- .../pitestintellij/util/GradleUtils.kt | 26 ++++++++++++++++--- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PrepareEnvironmentCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PrepareEnvironmentCommand.kt index 1c8eb22..60f4575 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PrepareEnvironmentCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/PrepareEnvironmentCommand.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.ReadAction import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project +import com.intellij.openapi.project.rootManager import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.ui.Messages @@ -112,25 +113,27 @@ class PrepareEnvironmentCommand(project: Project, context: PitestContext) : Pite context.targetClassFilePath = targetClassInfo.file.normalize().toString().replace("\\", "/") } - private fun prepareReportDirectory(testVirtualFile: VirtualFile, className: String){ + private fun prepareReportDirectory(testVirtualFile: VirtualFile, className: String) { // prepare the report directory val parentModulePath = ReadAction.compute { - val projectModule = ProjectRootManager.getInstance(project).fileIndex.getModuleForFile(testVirtualFile) - if (projectModule == null) { - showError("Cannot find module for test file") - throw IllegalStateException("Cannot find module for test file") + ?: throw IllegalStateException("Cannot find module for test file") + + val modulePath = GradleUtils.getUpperModulePath(project, projectModule) + if (modulePath.isEmpty()) { + // If no parent module found, use the current module's path + projectModule.rootManager.contentRoots.firstOrNull()?.path + ?: throw IllegalStateException("Cannot find module root path") + } else { + modulePath } - - GradleUtils.getUpperModulePath(project, projectModule) } + context.reportDirectory = Paths.get(parentModulePath, "build", "reports", "pitest", className).toString() File(context.reportDirectory!!).mkdirs() - } - private fun collectClassPathFileForPitest(reportDirectory:String, targetPackageName:String, resourceDirectories: List?){ val classPathFileContent = ReadAction.compute { val classpath = GradleUtils.getCompilationOutputPaths(project) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GradleUtils.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GradleUtils.kt index 6ca62f3..01d272e 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GradleUtils.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/util/GradleUtils.kt @@ -1,6 +1,7 @@ package com.github.jaksonlin.pitestintellij.util import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project import com.intellij.openapi.project.modules import com.intellij.openapi.project.rootManager @@ -34,11 +35,30 @@ object GradleUtils { return outputPaths } - // find the direct parent module's base directory of the child module in the project +// // find the direct parent module's base directory of the child module in the project +// fun getUpperModulePath(project: Project, childModule: Module): String { +// var candidateModuleName = "" +// var candidateModule: Module? = null +// for (module in project.modules) { +// val moduleName = module.name +// if (childModule.name.contains(moduleName) && childModule.name != moduleName) { +// if (moduleName.length > candidateModuleName.length) { +// candidateModuleName = moduleName +// candidateModule = module +// } +// } +// } +// val contentRoots = candidateModule?.rootManager?.contentRoots +// return if (contentRoots != null && contentRoots.isNotEmpty()) { +// contentRoots[0].path +// } else { +// "" +// } +// } fun getUpperModulePath(project:Project, childModule:Module):String{ var candidateModuleName = "" var candidateModule: Module? = null - for (module in project.modules) { + for (module in ModuleManager.getInstance(project).modules) { val moduleName = module.name if (childModule.name.contains(moduleName) && childModule.name != moduleName) { if (moduleName.length > candidateModuleName.length){ @@ -47,7 +67,7 @@ object GradleUtils { } } } - return candidateModule?.rootManager?.contentRoots?.get(0)?.path ?: "" + return candidateModule?.rootManager?.contentRoots?.firstOrNull()?.path ?: "" } fun getTestRunDependencies(project: Project): List { From 7ac9ba0032f62af4b29bfc973771ff915240e6f5 Mon Sep 17 00:00:00 2001 From: zhislin Date: Wed, 27 Nov 2024 21:34:40 +0800 Subject: [PATCH 22/24] =?UTF-8?q?[bugfix]bug1:=20=E6=9C=89=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=E7=9A=84=E5=87=BD=E6=95=B0=E6=8F=90=E5=8F=96=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E4=BC=9A=E4=B8=AD=E6=96=AD;[bugfix]bug2:=20=E5=9C=A8?= =?UTF-8?q?=E7=94=9F=E6=88=90=E6=B3=A8=E8=A7=A3=E6=97=B6=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=BA=94=E7=94=A8TestXXX=E6=88=96=E8=80=85XXXTest=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E6=98=AF=E5=90=A6=E6=98=AF=E6=B5=8B=E8=AF=95=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/ValueProviderService.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt index 488a161..fbf665c 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/services/ValueProviderService.kt @@ -105,6 +105,20 @@ class ValueProviderService(private val project: Project) { val classUnderTest = psiFacade.findClass(guessedClassName, GlobalSearchScope.projectScope(project)) ?: return null + // Skip if the target class is a test class + val hasTestMethods = classUnderTest.methods.any { method -> + method.annotations.any { annotation -> + val annotationName = annotation.qualifiedName + annotationName == "org.junit.jupiter.api.Test" || // JUnit 5 + annotationName == "org.junit.Test" || // JUnit 4 + annotationName?.contains("Test") ?: false // Custom test annotation + } + } + + if (hasTestMethods) { + return null + } + // Find matching method in the class under test val methodUnderTest = classUnderTest.methods.firstOrNull { method -> method.name.lowercase().contains(guessedMethodName.lowercase()) @@ -116,8 +130,11 @@ class ValueProviderService(private val project: Project) { private fun getMethodSignature(psiMethod: PsiMethod): String { return buildString { - // Add visibility modifier - append(psiMethod.modifierList.text.trim()) + // Add visibility modifier (excluding annotations) + psiMethod.modifierList.text.trim() + .split(" ") + .filter { it.isNotEmpty() && !it.startsWith("@") } + .joinTo(this, " ") append(" ") // Add return type if not constructor @@ -140,7 +157,7 @@ class ValueProviderService(private val project: Project) { append(" throws ") append(throwsList.joinToString(", ") { it.presentableText }) } - } + }.replace("\n", " ").replace(Regex("\\s+"), " ").trim() } private fun getFirstCreatorAuthor(psiMethod: PsiMethod): String { From d43744cf93768d3c8fa2323855e0b7cbf58e5a38 Mon Sep 17 00:00:00 2001 From: zhislin Date: Fri, 29 Nov 2024 14:43:22 +0800 Subject: [PATCH 23/24] [bugfix]generate multiple annotations forgot to use new contxt --- CHANGELOG.md | 2 +- gradle.properties | 2 +- .../unittestannotations/GenerateAnnotationCommand.kt | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a51646e..2afc1e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,6 @@ ## 1.0.2 - 2024-10-12 - Bugfix: change plugin name introduce a bug in the code. -## 1.0.3-preview - 2024-11-14 +## 1.0.3 - 2024-11-14 - Add unit test annotation generation action. - Add unit test annotation inspection. diff --git a/gradle.properties b/gradle.properties index 058419d..d3235ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.jaksonlin.pitestintellij pluginName = pitest-gradle pluginRepositoryUrl = https://github.com/jaksonlin/pitest-gradle # SemVer format -> https://semver.org -pluginVersion = 1.0.3-preview +pluginVersion = 1.0.3 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 222 diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt index ac0e5d4..2bb2644 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/unittestannotations/GenerateAnnotationCommand.kt @@ -211,7 +211,8 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni protected fun generateAnnotation(psiMethod: PsiMethod, schema: AnnotationSchema) { // First compute the annotation text in a read action val annotationText = ReadAction.compute { - buildAnnotationStr(schema) + val newContext = context.copy(psiMethod = psiMethod) + buildAnnotationStr(schema, newContext) } // Then use the computed text in the write action @@ -271,7 +272,7 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni } // we should do this in a read action - private fun buildAnnotationStr(schema: AnnotationSchema) : String { + private fun buildAnnotationStr(schema: AnnotationSchema, buildAnnotationContext: CaseCheckContext) : String { val annotationText = buildString { append("@${schema.annotationClassName}(\n") schema.fields.filter{ it.required }.forEachIndexed { index, field -> @@ -280,7 +281,7 @@ class GenerateAnnotationCommand(project: Project, context: CaseCheckContext):Uni // Use value provider if available val value = field.valueProvider?.let { provider -> - valueProviderService.provideValue(provider, context) + valueProviderService.provideValue(provider, buildAnnotationContext) } ?: field.defaultValue when (field.type) { From 569f86a99b7269bf14c5a1f3a398be5a61ac3bd1 Mon Sep 17 00:00:00 2001 From: zhislin Date: Sat, 28 Dec 2024 09:29:02 +0800 Subject: [PATCH 24/24] add skip fail test --- .../pitestintellij/commands/pitest/BuildPitestCommandCommand.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/BuildPitestCommandCommand.kt b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/BuildPitestCommandCommand.kt index 969a7a3..7cf2e9c 100644 --- a/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/BuildPitestCommandCommand.kt +++ b/src/main/kotlin/com/github/jaksonlin/pitestintellij/commands/pitest/BuildPitestCommandCommand.kt @@ -40,6 +40,7 @@ class BuildPitestCommandCommand (project: Project, context: PitestContext) : Pit "2.0", "--mutators", "STRONGER", + "--skipFailingTests", ) } } \ No newline at end of file