diff --git a/build.gradle.kts b/build.gradle.kts index 3ce3e34..d4b9930 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ import java.util.Properties plugins { kotlin("jvm") version "1.8.20" + java id("io.gitlab.arturbosch.detekt") version "1.21.0" `maven-publish` } @@ -12,6 +13,7 @@ version = "2.1.0" allprojects { apply { plugin("kotlin") + plugin("java") plugin("io.gitlab.arturbosch.detekt") } @@ -23,6 +25,11 @@ allprojects { testRuntimeOnly("org.junit.platform:junit-platform-console:1.9.2") } + java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + repositories { mavenCentral() } @@ -65,10 +72,14 @@ val spaceUsername = getLocalProperty("spaceUsername") val spacePassword = getLocalProperty("spacePassword") configure(subprojects) { - apply(plugin = "maven-publish") - val subprojectName = this.name + // We don't need to publish the root ij project + if (subprojectName == "ij") { + return@configure + } + + apply(plugin = "maven-publish") publishing { publications { register("maven") { diff --git a/gradle.properties b/gradle.properties index 4a78f86..5224af1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,6 @@ kotlin.code.style=official pluginName = Kotlin Test Framework With IJ platformType = IC platformVersion = 2022.2.5 -platformPlugins = Kotlin +platformPlugins = Kotlin, java org.gradle.jvmargs = -XX:MaxMetaspaceSize=1g diff --git a/ij/build.gradle.kts b/ij/build.gradle.kts index bcbeb3b..fb46937 100644 --- a/ij/build.gradle.kts +++ b/ij/build.gradle.kts @@ -2,16 +2,20 @@ group = rootProject.group version = rootProject.version plugins { - id("org.jetbrains.intellij") version "1.14.1" + id("org.jetbrains.intellij") version "1.15.0" } fun properties(key: String) = providers.gradleProperty(key) -intellij { - pluginName = properties("pluginName") - version = properties("platformVersion") - type = properties("platformType") +allprojects { + apply(plugin = "org.jetbrains.intellij") - // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. - plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) } + intellij { + pluginName = properties("pluginName") + version = properties("platformVersion") + type = properties("platformType") + + // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. + plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) } + } } diff --git a/ij/common/build.gradle.kts b/ij/common/build.gradle.kts new file mode 100644 index 0000000..27c6f70 --- /dev/null +++ b/ij/common/build.gradle.kts @@ -0,0 +1,2 @@ +group = rootProject.group +version = rootProject.version diff --git a/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiElementAnalyzerUtil.kt b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiElementAnalyzerUtil.kt new file mode 100644 index 0000000..7b45052 --- /dev/null +++ b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiElementAnalyzerUtil.kt @@ -0,0 +1,39 @@ +package org.jetbrains.academy.test.system.ij.analyzer + +import com.intellij.psi.PsiElement + +/** + * Retrieves the constant value of the PsiElement. + * + * @param constantExpressionClass The classes of the constant expressions to search for. + * @return The constant value as a string, or null if no constant value is found. + * @throws IllegalArgumentException If the PsiElement contains multiple constant values. + */ +fun PsiElement.getConstValue(vararg constantExpressionClass: Class): String? { + val possibleValue = extractElementsOfTypes(*constantExpressionClass) + if (possibleValue.isEmpty()) { + return null + } + require(possibleValue.size == 1) { "Parser error! A const variable must have only one value" } + return possibleValue.first().text.trimIndent() +} + +/** + * Retrieves the body of a block represented by the given code block class. + * + * @param codeBlockClass the class representing the code block + * @return the body of the block as a string, or null if nobody is found + */ +fun PsiElement.getBlockBody(codeBlockClass: Class): String? { + val possibleBody = extractElementsOfTypes(codeBlockClass) + if (possibleBody.isEmpty()) { + return null + } + return possibleBody.first().text.trimBraces().trimIndent() +} + +/** + * Trims leading and trailing braces from a string. + */ +private fun String.trimBraces() = dropWhile { it.isWhitespace() }.removePrefix("{") + .dropLastWhile { it.isWhitespace() }.removeSuffix("}") diff --git a/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiFileAnalyzerUtil.kt b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiFileAnalyzerUtil.kt new file mode 100644 index 0000000..756d3e9 --- /dev/null +++ b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiFileAnalyzerUtil.kt @@ -0,0 +1,87 @@ +package org.jetbrains.academy.test.system.ij.analyzer + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.parentsOfType + +/** Extracts elements of given type from related files in project. */ +fun PsiElement.extractElementsOfTypes(vararg psiElementClass: Class): MutableCollection = + psiElementClass.flatMap { PsiTreeUtil.collectElementsOfType(this, it) }.toMutableList() + +/** + * Checks if the PsiFile contains an element of the specified type and with the given name. + * + * @param T the type of the PsiNamedElement subclass to check for + * @param psiElementClass the class object representing the type of the element to check for + * @param name the name of the element to search for + * @return true if an element of the specified type and name exists in the PsiFile, false otherwise + */ +fun PsiFile.hasElementOfTypeWithName(psiElementClass: Class, name: String): Boolean = + ApplicationManager.getApplication().runReadAction { + extractElementsOfTypes(psiElementClass).any { it.name == name } + } + +/** + * Retrieves the text of the parent element of the given element. + * + * @param element The element for which to retrieve the parent text. + * @param isParentTypeFunction True if the parent element's type is a function; false otherwise. + * @param parentClass The class of the parent element. + * @return The text of the parent element, or null if the parent does not exist or is not of the specified type. + */ +private fun getParentText( + element: PsiElement, + isParentTypeFunction: Boolean, + parentClass: Class +): String? { + return if (isParentTypeFunction) { + element.parentsOfType(parentClass).firstOrNull()?.name + } else { + element.parent?.text + } +} + +/** + * Checks if the given PsiFile has an expression with the specified parent. + * + * @param expression The text of the expression to search for. + * @param parent The text of the parent to compare with. + * @param isParentTypeFunction Specifies if the parent should be treated as a function or not. + * @param parentClass The class of the parent element. + * @param expressionClass The classes of the expression elements to search for. + * @return true if the PsiFile has an expression with the specified parent, false otherwise. + */ +fun PsiFile.hasExpressionWithParent( + expression: String, + parent: String?, + isParentTypeFunction: Boolean, + parentClass: Class, + vararg expressionClass: Class +): Boolean = + ApplicationManager.getApplication().runReadAction { + val expressions: MutableCollection = extractElementsOfTypes(*expressionClass) + expressions.any { it.text == expression && getParentText(it, isParentTypeFunction, parentClass) == parent } + } + +/** + * Finds usages of a given method in the PSI file. + * + * @param methodName The name of the method to find usages of. + * @param callExpressionClass The class representing the call expression to search for. + * @param methodClass The class representing the method to search for usages of. + * @return A list of strings representing the names of the methods in which the given method is used. + */ +fun PsiFile.findMethodUsages( + methodName: String, + callExpressionClass: Class, + methodClass: Class +): List = + ApplicationManager.getApplication().runReadAction> { + val callExpression = extractElementsOfTypes(callExpressionClass) + callExpression.filter { it.text == methodName }.mapNotNull { + it.parentsOfType(methodClass).firstOrNull()?.name + }.toList() + } diff --git a/ij/src/main/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingUtil.kt b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingUtil.kt similarity index 78% rename from ij/src/main/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingUtil.kt rename to ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingUtil.kt index c5027b7..9b77dd1 100644 --- a/ij/src/main/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingUtil.kt +++ b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingUtil.kt @@ -6,8 +6,6 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.psi.PsiFile import com.intellij.psi.codeStyle.CodeStyleManager -import org.jetbrains.academy.test.system.inspections.applyInspections -import org.jetbrains.kotlin.idea.inspections.KotlinUnusedImportInspection import org.junit.jupiter.api.Assertions // TODO: make it possible to check different aspects of formatting @@ -32,10 +30,3 @@ fun PsiFile.formatting(): String? { } return ApplicationManager.getApplication().runReadAction { text } } - -fun PsiFile.checkIfOptimizeImportsWereApplied() { - Assertions.assertTrue( - applyInspections(listOf(KotlinUnusedImportInspection())).isEmpty(), - "Please, apply \"Optimize import\" option when formatting code." - ) -} diff --git a/ij/src/main/kotlin/org/jetbrains/academy/test/system/inspections/InspectionsUtil.kt b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/inspections/InspectionsUtil.kt similarity index 90% rename from ij/src/main/kotlin/org/jetbrains/academy/test/system/inspections/InspectionsUtil.kt rename to ij/common/src/main/kotlin/org/jetbrains/academy/test/system/inspections/InspectionsUtil.kt index 5359cd6..fa57675 100644 --- a/ij/src/main/kotlin/org/jetbrains/academy/test/system/inspections/InspectionsUtil.kt +++ b/ij/common/src/main/kotlin/org/jetbrains/academy/test/system/inspections/InspectionsUtil.kt @@ -8,7 +8,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.progress.ProgressManager import com.intellij.psi.PsiFile -fun PsiFile.applyInspections(inspections: List): List { +fun PsiFile.applyLocalInspections(inspections: List): List { val problems: MutableList = mutableListOf() val inspectionManager = InspectionManager.getInstance(project) ProgressManager.getInstance().executeProcessUnderProgress( diff --git a/ij/java-psi/build.gradle.kts b/ij/java-psi/build.gradle.kts new file mode 100644 index 0000000..b92d5ae --- /dev/null +++ b/ij/java-psi/build.gradle.kts @@ -0,0 +1,6 @@ +group = rootProject.group +version = rootProject.version + +dependencies { + implementation(project(":ij:common")) +} diff --git a/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/PsiFileAnalyzerUtil.kt b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/PsiFileAnalyzerUtil.kt new file mode 100644 index 0000000..426bfb4 --- /dev/null +++ b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/PsiFileAnalyzerUtil.kt @@ -0,0 +1,57 @@ +package org.jetbrains.academy.test.system.java.ij.analyzer + +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiCodeBlock +import org.jetbrains.academy.test.system.ij.analyzer.extractElementsOfTypes +import org.jetbrains.academy.test.system.ij.analyzer.getBlockBody +import org.jetbrains.academy.test.system.ij.analyzer.getConstValue +import org.jetbrains.academy.test.system.ij.formatting.formatting + +/** + * Checks if PsiFile contains a constant property with the given element value. + * + * @param elementValue The value to search for in the constant properties. + * @return true if a constant property with the specified value is found, false otherwise. + */ +fun PsiFile.hasConstantWithGivenValue(elementValue: String): Boolean = + ApplicationManager.getApplication().runReadAction { + val elements = extractElementsOfTypes(PsiField::class.java) + elements.any { it.modifierList?.text?.contains("final") ?: false && it.getConstValue(PsiLiteralExpression::class.java) == elementValue } + } + +/** + * Formatting code content. + * + * @param content The source code to wrap and format. + * @param project An instance of the project, used for creating PsiFile. + * @return The formatted code content. + */ +private fun formattingContent(content: String, project: Project): String { + val wrappedCode = "class WrappedClass { void wrappedMethod() { $content } }" + val factory = PsiFileFactory.getInstance(project) + val contentPsiFile = factory.createFileFromText("Content.java", JavaFileType.INSTANCE, wrappedCode) + val formattingContent = contentPsiFile.formatting() ?: "" + return formattingContent.lines().drop(2).dropLast(2).joinToString(System.lineSeparator()).trimIndent() +} + +/** + * Finds methods within the given PsiFile that have the specified body content. + * + * @param content The body content to search for in the methods. + * @return A list of method names whose bodies match the provided content. + */ +fun PsiFile.findMethodsWithContent(content: String): List = + ApplicationManager.getApplication().runReadAction> { + formatting() + val formattingContent = formattingContent(content, project) + + val methods = extractElementsOfTypes(PsiMethod::class.java) + methods.filter { it.getBlockBody(PsiCodeBlock::class.java) == formattingContent }.mapNotNull { it.name }.toList() + } diff --git a/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/formatting/UnusedImportsUtil.kt b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/formatting/UnusedImportsUtil.kt new file mode 100644 index 0000000..61ed179 --- /dev/null +++ b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/formatting/UnusedImportsUtil.kt @@ -0,0 +1,17 @@ +package org.jetbrains.academy.test.system.java.ij.formatting + +import com.intellij.psi.PsiFile +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.psi.codeStyle.JavaCodeStyleManager + +fun PsiFile.checkIfOptimizeImportsWereApplied() { + val originalCode = ApplicationManager.getApplication().runReadAction { text } + WriteCommandAction.runWriteCommandAction(project) { + JavaCodeStyleManager.getInstance(project).optimizeImports(this) + } + val optimizedCode = ApplicationManager.getApplication().runReadAction { text } + assert(originalCode.trimIndent() == optimizedCode.trimIndent()) { + "Please, apply \"Optimize import\" option when formatting code." + } +} diff --git a/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/test/BaseIjTestClass.kt b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/test/BaseIjTestClass.kt new file mode 100644 index 0000000..2823548 --- /dev/null +++ b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/test/BaseIjTestClass.kt @@ -0,0 +1,48 @@ +package org.jetbrains.academy.test.system.java.test + +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.PsiNewExpression +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiCallExpression +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.jetbrains.academy.test.system.ij.analyzer.findMethodUsages +import org.jetbrains.academy.test.system.ij.analyzer.hasElementOfTypeWithName +import org.jetbrains.academy.test.system.ij.analyzer.hasExpressionWithParent +import org.jetbrains.academy.test.system.java.ij.analyzer.findMethodsWithContent +import org.jetbrains.academy.test.system.java.ij.analyzer.hasConstantWithGivenValue + +/** + * A Base test class to create tests with PSI without adding intellij dependency into courses directly. + */ +open class BaseIjTestClass : BasePlatformTestCase() { + + fun hasConstantWithGivenValue(elementValue: String): Boolean = + myFixture.file.hasConstantWithGivenValue(elementValue) + + fun findMethodsWithContent(content: String): List = + myFixture.file.findMethodsWithContent(content) + + fun findMethodUsages(content: String): List = + myFixture.file.findMethodUsages(content, PsiCallExpression::class.java, PsiMethod::class.java) + + fun hasProperty(propertyName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(PsiField::class.java, propertyName) + + fun hasMethod(methodName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(PsiMethod::class.java, methodName) + + fun hasClass(className: String): Boolean = myFixture.file.hasElementOfTypeWithName(PsiClass::class.java, className) + + fun hasParameter(parameterName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(PsiParameter::class.java, parameterName) + + fun hasExpressionWithParent(expression: String, parent: String?, isParentTypeFunction: Boolean = false): Boolean = + myFixture.file.hasExpressionWithParent( + expression, parent, isParentTypeFunction, PsiMethod::class.java, + PsiNewExpression::class.java, PsiReferenceExpression::class.java, PsiMethodCallExpression::class.java + ) +} diff --git a/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/BaseFormattingUtilTests.kt b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/BaseFormattingUtilTests.kt new file mode 100644 index 0000000..c542e19 --- /dev/null +++ b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/BaseFormattingUtilTests.kt @@ -0,0 +1,17 @@ +package org.jetbrains.academy.test.system.java.ij + +import com.intellij.psi.PsiFile +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.junit.Ignore + +@Ignore +open class BaseFormattingUtilTests : BasePlatformTestCase() { + + protected fun testWithError(code: String, action: (PsiFile) -> Unit) { + assertThrows(AssertionError::class.java) { action(myFixture.configureByText("dummy.java", code)) } + } + + protected fun testWithoutError(code: String, action: (PsiFile) -> Unit) { + action(myFixture.configureByText("dummy.java", code)) + } +} diff --git a/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/BaseIjTestClassTests.kt b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/BaseIjTestClassTests.kt new file mode 100644 index 0000000..71fb4ca --- /dev/null +++ b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/BaseIjTestClassTests.kt @@ -0,0 +1,329 @@ +package org.jetbrains.academy.test.system.java.ij + +import org.jetbrains.academy.test.system.java.test.BaseIjTestClass +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.Arguments + +class BaseIjTestClassTests : BaseIjTestClass() { + + companion object { + @JvmStatic + fun findMethodUsagesTestProvider() = listOf( + Arguments.of("method(\"Content\")", listOf("outerFunction", "method2")), + Arguments.of("method(\"y is greater than 5\")", listOf("method1")), + Arguments.of("method(\"y is less than 5\")", listOf("method1")), + Arguments.of("method(content)", emptyList()) + ) + + @JvmStatic + fun hasExpressionWithParentTestProvider() = listOf( + Arguments.of("productPrice.size()", "sum / productPrice.size()"), + Arguments.of("new File(\"Exception.txt\")", "(new File(\"Exception.txt\"), \"UTF-8\")"), + Arguments.of("new PrintWriter(new File(\"Exception.txt\"), \"UTF-8\")", "calculateAveragePrice"), + Arguments.of("Integer.MAX_VALUE", "private static final int CONSTANT = Integer.MAX_VALUE;") + ) + + @JvmStatic + fun findMethodsWithContentTestProvider() = listOf( + Arguments.of(""" + String actions = "Some actions"; + System.out.println("Content"); + System.out.println(actions); + """, "method1") + ) + } + + @ParameterizedTest + @MethodSource("findMethodsWithContentTestProvider") + fun testFindMethodsWithContent(content: String, methodName: String) { + val example = """ + public class ExampleClass { + + public void method1() { + String actions = "Some actions"; + System.out.println("Content"); + System.out.println(actions); + } + + public void method2() { + String content = "Content"; + String actions = "Some actions"; + System.out.println(actions + content); + } + + public void method3() { + System.out.println("Content"); + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + assert(listOf(methodName).equals(findMethodsWithContent(content))) { + "The name of a method with this content \n $content \n must be $methodName" + } + } + + @ParameterizedTest + @MethodSource("findMethodsWithContentTestProvider") + fun testFindMethodsWithContentWithBrokenFormatting(content: String, methodName: String) { + val example = """ + public class ExampleClass { + + public void method1() { + String actions = "Some actions"; + System.out.println("Content"); + System.out.println(actions); + } + + + public void method2() { + System.out.println("Content"); + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + assert(listOf(methodName).equals(findMethodsWithContent(content))) { + "The name of a method with this content \n $content \n must be $methodName" + } + } + + fun testFindMethodsWithContentWithNestedBodies() { + val example = """ + public class ExampleClass { + + public static int outerFunction(int x) { + System.out.println("Outer function started."); + + Operation operation = new Operation() { + @Override + public int innerFunction(int y) { + return y * y; + } + }; + + int squaredX = operation.calculate(x); + int modifiedResult = squaredX + 10; + return modifiedResult; + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + val content = "return y * y;" + val methodName = "innerFunction" + assert(listOf(methodName).equals(findMethodsWithContent(content))) { + "The name of a method with this content \n $content \n must be $methodName" + } + } + + fun testHasConstantWithGivenValue() { + val example = """ + public class ExampleClass { + + private static final String CONSTANT1 = "some text"; + private double notConstant = 0.5; + public static final int CONSTANT2 = 2; + public final int CONSTANT50 = 50; + public int consts = 500; + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + var value = "\"some text\"" + assert(hasConstantWithGivenValue(value)) { "There must exist a constant with value $value" } + value = "2" + assert(hasConstantWithGivenValue(value)) { "There must exist a constant with value $value" } + value = "50" + assert(hasConstantWithGivenValue(value)) { "There must exist a constant with value $value" } + assertFalse(hasConstantWithGivenValue("0.5")) + assertFalse(hasConstantWithGivenValue("500")) + } + + fun testHasConstantWithGivenValueWithBrokenFormatting() { + val example = """ + public class ExampleClass { + + private static final String CONSTANT1 = "some text"; + private double notConstant = 0.5; + public static final int CONSTANT2 = 2; + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + var value = "\"some text\"" + assert(hasConstantWithGivenValue(value)) { "There must exist a constant with value $value" } + value = "2" + assert(hasConstantWithGivenValue(value)) { "There must exist a constant with value $value" } + assertFalse(hasConstantWithGivenValue("0.5")) + } + + @ParameterizedTest + @MethodSource("findMethodUsagesTestProvider") + fun testFindMethodUsages(methodName: String, methodsList: List) { + val example = """ + public class ExampleClass { + + public static int outerFunction(int x) { + Function innerFunction = y -> y * y; + method("Content"); + int squaredX = innerFunction.apply(x); + return squaredX + 10; + } + + public void method(String message) { + System.out.println(message); + } + + public void method1(int y) { + String actions = "Some actions"; + if (y > 5) { + method("y is greater than 5"); + } else { + method("y is less than 5"); + } + } + + public void method2() { + method("Content"); + System.out.println("Content"); + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + assert(methodsList.equals(findMethodUsages(methodName))) { + "Method $methodName should be called in methods: $methodsList" + } + } + + fun testHasProperty() { + val example = """ + public class ExampleClass { + private static final String CONSTANT = "some text"; + private final double value = 0.5; + private int number = 2; + + public void method() { + System.out.println("Content"); + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + var value = "CONSTANT" + assert(hasProperty(value)) { "There must exist a property with name $value" } + value = "value" + assert(hasProperty(value)) { "There must exist a property with name $value" } + value = "number" + assert(hasProperty(value)) { "There must exist a property with name $value" } + assertFalse(hasProperty("method")) + assertFalse(hasProperty("Content")) + } + + fun testHasMethod() { + val example = """ + public class ExampleClass { + private static final String CONSTANT = "some text"; + private static final double value = 0.5; + + public void method() { + String actions = "Some actions"; + System.out.println("Content"); + System.out.println(actions); + } + + public void notMethod() { + String content = "Content"; + String actions = "Some actions"; + System.out.println(actions + content); + } + + public void value() { + System.out.println("Content"); + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + var value = "method" + assert(hasMethod(value)) { "There must exist a method with name $value" } + value = "notMethod" + assert(hasMethod(value)) { "There must exist a method with name $value" } + value = "value" + assert(hasMethod(value)) { "There must exist a method with name $value" } + assertFalse(hasMethod("CONSTANT")) + assertFalse(hasMethod("Content")) + } + + fun testHasClass() { + val example = """ + public class ExampleClass { + public void method() { + String actions = "Some actions"; + System.out.println(actions); + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + val name = "ExampleClass" + Assertions.assertTrue(hasClass(name), "There must exist a class with name $name") + Assertions.assertFalse(hasClass("method")) + Assertions.assertFalse(hasClass("Class")) + } + + fun testHasParameter() { + val example = """ + public class ExampleClass { + private static final String PARAMETER = "some text"; + + public void method1(Int parameter1, String parameter2) { + System.out.println(parameter2); + } + + public void method2(Double y) { + System.out.println("Content"); + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + var value = "parameter1" + Assertions.assertTrue(hasParameter(value), "There must exist a parameter with name $value") + value = "parameter2" + Assertions.assertTrue(hasParameter(value), "There must exist a parameter with name $value") + value = "y" + Assertions.assertTrue(hasParameter(value), "There must exist a parameter with name $value") + Assertions.assertFalse(hasParameter("PARAMETER")) + Assertions.assertFalse(hasParameter("Content")) + } + + @ParameterizedTest + @MethodSource("hasExpressionWithParentTestProvider") + fun testHasExpressionWithParent(expression: String, parent: String?) { + val example = """ + import java.io.File; + import java.io.PrintWriter; + import java.util.List; + + public class ExampleClass { + + private static final int CONSTANT = Integer.MAX_VALUE; + + public static Integer calculateAveragePrice(List productPrice) { + try { + int sum = 0; + for (Integer price : productPrice) { + sum += price; + } + return sum / productPrice.size(); + } catch (Exception error) { + try (PrintWriter writer = new PrintWriter(new File("Exception.txt"), "UTF-8")) { + writer.print(error.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + } + } + """.trimIndent() + myFixture.configureByText("Task.java", example) + assert(hasExpressionWithParent(expression, parent)) { + "There must exist an expression $expression with parent $parent" + } + } +} diff --git a/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/FormattingTests.kt b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/FormattingTests.kt new file mode 100644 index 0000000..3587351 --- /dev/null +++ b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/FormattingTests.kt @@ -0,0 +1,39 @@ +package org.jetbrains.academy.test.system.java.ij + +import org.jetbrains.academy.test.system.ij.formatting.checkIfFormattingRulesWereApplied + +class FormattingTests : BaseFormattingUtilTests() { + fun testWrongFormatting() { + testWithError( + """ + public class ExampleClass { + + public static void funWithFormattingIssues() { + System.out.println ("This function definitely has formatting issues" ); + System.out.println( "... that could be easily fixed using one shortcut"); + for ( int i = 1; i <= 10; i++) { + System.out.println("Please, format me!"); + } + } + } + """.trimIndent() + ) { it.checkIfFormattingRulesWereApplied() } + } + + fun testRightFormatting() { + testWithoutError( + """ + public class ExampleClass { + + public static void funWithFormattingIssues() { + System.out.println("This function definitely has formatting issues"); + System.out.println("... that could be easily fixed using one shortcut"); + for (int i = 1; i <= 10; i++) { + System.out.println("Please, format me!"); + } + } + } + """.trimIndent() + ) { it.checkIfFormattingRulesWereApplied() } + } +} diff --git a/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/UnusedImportsTests.kt b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/UnusedImportsTests.kt new file mode 100644 index 0000000..52b16e7 --- /dev/null +++ b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/UnusedImportsTests.kt @@ -0,0 +1,48 @@ +package org.jetbrains.academy.test.system.java.ij + +import org.jetbrains.academy.test.system.java.ij.formatting.checkIfOptimizeImportsWereApplied + +class UnusedImportsTests : BaseFormattingUtilTests() { + + fun testUnusedImports() { + testWithError( + """ + import java.io.File; + import com.intellij.psi.PsiElement; + + public class ExampleClass { + + public static void funWithFormattingIssues() { + System.out.println("This function definitely has formatting issues"); + System.out.println("... that could be easily fixed using one shortcut"); + for (int i = 1; i <= 10; i++) { + System.out.println("Please, format me!"); + } + } + } + """.trimIndent() + ) + { it.checkIfOptimizeImportsWereApplied() } + } + + fun testUsedImports() { + testWithoutError( + """ + import java.io.File; + + public class ExampleClass { + + public static void funWithFormattingIssues() { + System.out.println("This function definitely has formatting issues"); + System.out.println("... that could be easily fixed using one shortcut"); + for (int i = 1; i <= 10; i++) { + System.out.println("Please, format me!"); + } + Class file = File.class; + System.out.println(file); + } + } + """.trimIndent() + ) { it.checkIfOptimizeImportsWereApplied() } + } +} diff --git a/ij/kotlin-psi/build.gradle.kts b/ij/kotlin-psi/build.gradle.kts new file mode 100644 index 0000000..b92d5ae --- /dev/null +++ b/ij/kotlin-psi/build.gradle.kts @@ -0,0 +1,6 @@ +group = rootProject.group +version = rootProject.version + +dependencies { + implementation(project(":ij:common")) +} diff --git a/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/PsiFileAnalyzerUtil.kt b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/PsiFileAnalyzerUtil.kt new file mode 100644 index 0000000..f8af3dc --- /dev/null +++ b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/PsiFileAnalyzerUtil.kt @@ -0,0 +1,45 @@ +package org.jetbrains.academy.test.system.kotlin.ij.analyzer + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import org.jetbrains.academy.test.system.ij.analyzer.* +import org.jetbrains.academy.test.system.ij.formatting.formatting +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtConstantExpression +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtBlockExpression + +/** + * Checks if PsiFile contains a constant property with the given element value. + * + * @param elementValue The value to search for in the constant properties. + * @return true if a constant property with the specified value is found, false otherwise. + */ +fun PsiFile.hasConstantWithGivenValue(elementValue: String): Boolean = + ApplicationManager.getApplication().runReadAction { + val elements = extractElementsOfTypes(KtProperty::class.java) + elements.any { it.modifierList?.text?.contains("const") ?: false && it.getConstValue(KtConstantExpression::class.java, KtStringTemplateExpression::class.java) == elementValue } + } + +/** + * Finds methods within the given PsiFile that have the specified body content. + * + * @param content The body content to search for in the methods. + * @return A list of method names whose bodies match the provided content. + */ +fun PsiFile.findMethodsWithContent(content: String): List = + WriteCommandAction.runWriteCommandAction>(project) { + formatting() + val factory = PsiFileFactory.getInstance(project) + val contentPsiFile = factory.createFileFromText("Content.kt", KotlinFileType.INSTANCE, content) + val formattingContent = contentPsiFile.formatting() ?: "" + + val methods = ApplicationManager.getApplication().runReadAction> { + extractElementsOfTypes(KtNamedFunction::class.java) + } + methods.filter { it.getBlockBody(KtBlockExpression::class.java) == formattingContent }.mapNotNull { it.name }.toList() + } diff --git a/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/formatting/UnusedImportsUtil.kt b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/formatting/UnusedImportsUtil.kt new file mode 100644 index 0000000..1f76a67 --- /dev/null +++ b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/formatting/UnusedImportsUtil.kt @@ -0,0 +1,11 @@ +package org.jetbrains.academy.test.system.kotlin.ij.formatting + +import com.intellij.psi.PsiFile +import org.jetbrains.academy.test.system.inspections.applyLocalInspections +import org.jetbrains.kotlin.idea.inspections.KotlinUnusedImportInspection + +fun PsiFile.checkIfOptimizeImportsWereApplied() { + assert(applyLocalInspections(listOf(KotlinUnusedImportInspection())).isEmpty()) { + "Please, apply \"Optimize import\" option when formatting code." + } +} diff --git a/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/test/BaseIjTestClass.kt b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/test/BaseIjTestClass.kt new file mode 100644 index 0000000..da2cf00 --- /dev/null +++ b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/test/BaseIjTestClass.kt @@ -0,0 +1,46 @@ +package org.jetbrains.academy.test.system.kotlin.test + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.jetbrains.academy.test.system.ij.analyzer.findMethodUsages +import org.jetbrains.academy.test.system.ij.analyzer.hasElementOfTypeWithName +import org.jetbrains.academy.test.system.ij.analyzer.hasExpressionWithParent +import org.jetbrains.academy.test.system.kotlin.ij.analyzer.findMethodsWithContent +import org.jetbrains.academy.test.system.kotlin.ij.analyzer.hasConstantWithGivenValue +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtCallExpression + +/** + * A Base test class to create tests with PSI without adding intellij dependency into courses directly. + */ +open class BaseIjTestClass : BasePlatformTestCase() { + + fun hasConstantWithGivenValue(elementValue: String): Boolean = + myFixture.file.hasConstantWithGivenValue(elementValue) + + fun findMethodsWithContent(content: String): List = + myFixture.file.findMethodsWithContent(content) + + fun findMethodUsages(content: String): List = + myFixture.file.findMethodUsages(content, KtCallExpression::class.java, KtNamedFunction::class.java) + + fun hasProperty(propertyName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(KtProperty::class.java, propertyName) + + fun hasMethod(methodName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(KtNamedFunction::class.java, methodName) + + fun hasClass(className: String): Boolean = myFixture.file.hasElementOfTypeWithName(KtClass::class.java, className) + + fun hasParameter(parameterName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(KtParameter::class.java, parameterName) + + fun hasExpressionWithParent(expression: String, parent: String?, isParentTypeFunction: Boolean = false): Boolean = + myFixture.file.hasExpressionWithParent( + expression, parent, isParentTypeFunction, KtNamedFunction::class.java, + KtDotQualifiedExpression::class.java, KtCallExpression::class.java + ) +} diff --git a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/BaseFormattingUtilTests.kt b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseFormattingUtilTests.kt similarity index 90% rename from ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/BaseFormattingUtilTests.kt rename to ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseFormattingUtilTests.kt index f829488..c327b3c 100644 --- a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/BaseFormattingUtilTests.kt +++ b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseFormattingUtilTests.kt @@ -1,4 +1,4 @@ -package org.jetbrains.academy.test.system.ij.formatting +package org.jetbrains.academy.test.system.kotlin.ij import com.intellij.psi.PsiFile import com.intellij.testFramework.fixtures.BasePlatformTestCase diff --git a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/BaseIjTestClassTests.kt b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseIjTestClassTests.kt similarity index 72% rename from ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/BaseIjTestClassTests.kt rename to ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseIjTestClassTests.kt index 28471f3..999d4b6 100644 --- a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/BaseIjTestClassTests.kt +++ b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseIjTestClassTests.kt @@ -1,11 +1,44 @@ -package org.jetbrains.academy.test.system.ij.formatting +package org.jetbrains.academy.test.system.kotlin.ij -import org.jetbrains.academy.test.system.test.BaseIjTestClass import org.junit.jupiter.api.Assertions +import org.jetbrains.academy.test.system.kotlin.test.BaseIjTestClass +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource class BaseIjTestClassTests : BaseIjTestClass() { - fun testFindMethodsWithContent() { + companion object { + @JvmStatic + fun findMethodUsagesTestProvider() = listOf( + Arguments.of("method(\"Content\")", listOf("innerFunction", "method2")), + Arguments.of("method(\"y is greater than 5\")", listOf("method1")), + Arguments.of("method(\"y is less than 5\")", listOf("method1")), + Arguments.of("method(content)", emptyList()) + ) + + @JvmStatic + fun hasExpressionWithParentTestProvider() = listOf( + Arguments.of("productPrice.sum()", "productPrice.sum() / productPrice.count()"), + Arguments.of("File(\\\"Exception.txt\\\")", "File(\\\"Exception.txt\\\")"), + Arguments.of("PrintWriter(File(\\\"Exception.txt\\\"), Charsets.UTF_8).use { it.print(error.toString()) }", "calculateAveragePrice"), + Arguments.of("Int.MAX_VALUE", "val CONSTANT = Int.MAX_VALUE"), + Arguments.of("Int.MAX_VALUE", null) + ) + + @JvmStatic + fun findMethodsWithContentTestProvider() = listOf( + Arguments.of(""" + val actions = "Some actions" + println("Content") + println(actions) + """, "method1") + ) + } + + @ParameterizedTest + @MethodSource("findMethodsWithContentTestProvider") + fun testFindMethodsWithContent(content: String, methodName: String) { val example = """ class ExampleClass { @@ -27,12 +60,6 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.kt", example) - val content = """ - val actions = "Some actions" - println("Content") - println(actions) - """.trimIndent() - val methodName = "method1" Assertions.assertEquals( listOf(methodName), findMethodsWithContent(content), @@ -56,7 +83,9 @@ class BaseIjTestClassTests : BaseIjTestClass() { ) } - fun testFindMethodsWithContentWithBrokenFormatting() { + @ParameterizedTest + @MethodSource("findMethodsWithContentTestProvider") + fun testFindMethodsWithContentWithBrokenFormatting(content: String, methodName: String) { val example = """ class ExampleClass { @@ -72,12 +101,6 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.kt", example) - val content = """ - val actions = "Some actions" - println("Content") - println(actions) - """.trimIndent() - val methodName = "method1" Assertions.assertEquals( listOf(methodName), findMethodsWithContent(content), @@ -139,7 +162,9 @@ class BaseIjTestClassTests : BaseIjTestClass() { Assertions.assertFalse(hasConstantWithGivenValue("0.5")) } - fun testFindMethodUsages() { + @ParameterizedTest + @MethodSource("findMethodUsagesTestProvider") + fun testFindMethodUsages(methodName: String, methodsList: List) { val example = """ class ExampleClass { fun outerFunction(x: Int): Int { @@ -172,30 +197,11 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.kt", example) - var methodName = "method(\"Content\")" - var methodsList = listOf("innerFunction", "method2") - Assertions.assertEquals( - methodsList, - findMethodUsages(methodName), - "Method $methodName should be called in methods: $methodsList" - ) - methodName = "method(\"y is greater than 5\")" - methodsList = listOf("method1") - Assertions.assertEquals( - methodsList, - findMethodUsages(methodName), - "Method $methodName should be called in methods: $methodsList" - ) - methodName = "method(\"y is less than 5\")" - methodsList = listOf("method1") Assertions.assertEquals( methodsList, findMethodUsages(methodName), "Method $methodName should be called in methods: $methodsList" ) - methodName = "method(content)" - methodsList = listOf() - Assertions.assertEquals(methodsList, findMethodUsages(methodName), "Method $methodName should not be called") } fun testHasProperty() { @@ -251,7 +257,45 @@ class BaseIjTestClassTests : BaseIjTestClass() { Assertions.assertFalse(hasMethod("Content")) } - fun testHasExpressionWithParent() { + fun testHasClass() { + val example = """ + class ExampleClass { + fun method() = { "Some actions" } + } + """.trimIndent() + myFixture.configureByText("Task.kt", example) + val name = "ExampleClass" + Assertions.assertTrue(hasClass(name), "There must exist a class with name $name") + Assertions.assertFalse(hasClass("method")) + Assertions.assertFalse(hasClass("Class")) + } + + fun testHasParameter() { + val example = """ + private const val PARAMETER = "some text" + + fun method1(parameter1: Int, parameter2: String) { + println(parameter2) + } + + fun method2(y: Double) { + println("Content") + } + """.trimIndent() + myFixture.configureByText("Task.kt", example) + var value = "parameter1" + Assertions.assertTrue(hasParameter(value), "There must exist a parameter with name $value") + value = "parameter2" + Assertions.assertTrue(hasParameter(value), "There must exist a parameter with name $value") + value = "y" + Assertions.assertTrue(hasParameter(value), "There must exist a parameter with name $value") + Assertions.assertFalse(hasParameter("PARAMETER")) + Assertions.assertFalse(hasParameter("Content")) + } + + @ParameterizedTest + @MethodSource("hasExpressionWithParentTestProvider") + fun testHasExpressionWithParent(expression: String, parent: String?) { val example = """ private const val CONSTANT = Int.MAX_VALUE @@ -265,32 +309,9 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.kt", example) - var expression: String = "productPrice.sum()" - var parent: String? = "productPrice.sum() / productPrice.count()" - Assertions.assertTrue( - hasExpressionWithParent(expression, parent), - "There must exist an expression $expression with parent $parent" - ) - expression = "File(\"Exception.txt\")" - parent = "File(\"Exception.txt\")" Assertions.assertTrue( hasExpressionWithParent(expression, parent), "There must exist an expression $expression with parent $parent" ) - expression = "PrintWriter(File(\"Exception.txt\"), Charsets.UTF_8).use { it.print(error.toString()) }" - parent = "calculateAveragePrice" - Assertions.assertTrue( - hasExpressionWithParent(expression, parent, true), - "There must exist an expression $expression with parent $parent" - ) - expression = "Int.MAX_VALUE" - parent = "val CONSTANT = Int.MAX_VALUE" - Assertions.assertFalse(hasExpressionWithParent(expression, parent)) - expression = "Int.MAX_VALUE" - parent = null - Assertions.assertTrue( - hasExpressionWithParent(expression, parent, true), - "There must exist an expression $expression with parent $parent" - ) } } diff --git a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingTests.kt b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/FormattingTests.kt similarity index 88% rename from ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingTests.kt rename to ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/FormattingTests.kt index 28b4331..aed2086 100644 --- a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/FormattingTests.kt +++ b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/FormattingTests.kt @@ -1,4 +1,6 @@ -package org.jetbrains.academy.test.system.ij.formatting +package org.jetbrains.academy.test.system.kotlin.ij + +import org.jetbrains.academy.test.system.ij.formatting.checkIfFormattingRulesWereApplied class FormattingTests : BaseFormattingUtilTests() { fun testWrongFormatting() { diff --git a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/UnusedImportsTests.kt b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/UnusedImportsTests.kt similarity index 88% rename from ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/UnusedImportsTests.kt rename to ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/UnusedImportsTests.kt index 8f255eb..34263e8 100644 --- a/ij/src/test/kotlin/org/jetbrains/academy/test/system/ij/formatting/UnusedImportsTests.kt +++ b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/UnusedImportsTests.kt @@ -1,4 +1,6 @@ -package org.jetbrains.academy.test.system.ij.formatting +package org.jetbrains.academy.test.system.kotlin.ij + +import org.jetbrains.academy.test.system.kotlin.ij.formatting.checkIfOptimizeImportsWereApplied class UnusedImportsTests : BaseFormattingUtilTests() { fun testUnusedImports() { diff --git a/ij/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/IjCodeAnalyzerUtil.kt b/ij/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/IjCodeAnalyzerUtil.kt deleted file mode 100644 index ee10f5e..0000000 --- a/ij/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/IjCodeAnalyzerUtil.kt +++ /dev/null @@ -1,148 +0,0 @@ -package org.jetbrains.academy.test.system.ij.analyzer - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiFileFactory -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.psi.util.parentsOfType -import org.jetbrains.academy.test.system.ij.formatting.formatting -import org.jetbrains.kotlin.idea.KotlinFileType -import org.jetbrains.kotlin.psi.* -import org.junit.jupiter.api.Assertions - -/** Extracts [kotlin elements][KtElement] of given type from kotlin related files in project. */ -/** Extracts elements of given type from [PsiElement] subtree. */ -fun PsiElement.extractElementsOfTypes(vararg psiElementClass: Class): MutableCollection = - psiElementClass.flatMap { PsiTreeUtil.collectElementsOfType(this, it) }.toMutableList() - -/** - * Retrieves the constant value of a property, if it is declared as a constant. - * - * @return The constant value of the property as a string, or null if the property is not constant or has no value. - * @throws IllegalStateException if the constant property contains more than one value. - */ -private fun KtProperty.getConstValue(): String? { - val possibleValue = - extractElementsOfTypes(KtConstantExpression::class.java, KtStringTemplateExpression::class.java) - if (possibleValue.isEmpty()) { - return null - } - Assertions.assertEquals(possibleValue.size, 1, "Parser error! A const variable must have only one value") - return possibleValue.first().text.trimIndent() -} - -/** - * Checks if PsiFile contains a constant property with the given element value. - * - * @param elementValue The value to search for in the constant properties. - * @return true if a constant property with the specified value is found, false otherwise. - */ -fun PsiFile.hasConstantWithGivenValue(elementValue: String): Boolean = - ApplicationManager.getApplication().runReadAction { - val elements = extractElementsOfTypes(KtProperty::class.java) - elements.any { it.modifierList?.text?.contains("const") ?: false && it.getConstValue() == elementValue } - } - -/** - * Retrieves the body with braces of a named function as a string. - * - * @return The body of the function as a string, or null if the function has no body. - * @throws IllegalStateException if the function contains more than one body. - */ -private fun KtNamedFunction.getBlockBody(): String? { - val possibleBody = extractElementsOfTypes(KtBlockExpression::class.java) - if (possibleBody.isEmpty()) { - return null - } - return possibleBody.first().text.trimBraces().trimIndent() -} - -/** - * Trims leading and trailing braces from a string. - */ -private fun String.trimBraces() = dropWhile { it.isWhitespace() }.removePrefix("{") - .dropLastWhile { it.isWhitespace() }.removeSuffix("}") - -/** - * Finds methods within the given PsiFile that have the specified body content. - * - * @param content The body content to search for in the methods. - * @return A list of method names whose bodies match the provided content. - */ -fun PsiFile.findMethodsWithContent(content: String): List = - ApplicationManager.getApplication().runReadAction> { - formatting() - val factory = PsiFileFactory.getInstance(project) - val contentPsiFile = factory.createFileFromText("Content.kt", KotlinFileType.INSTANCE, content) - val formattingContent = contentPsiFile.formatting() ?: "" - - val methods = extractElementsOfTypes(KtNamedFunction::class.java) - methods.filter { it.getBlockBody() == formattingContent }.mapNotNull { it.name }.toList() - } - -/** - * Finds all the methods in the file where a specific method is called. - * - * @param methodName The name of the method to search for its usages. - * @return A list of strings containing the names of methods where the specified method is called. - */ -fun PsiFile.findMethodUsages(methodName: String): List = - ApplicationManager.getApplication().runReadAction> { - val callExpression = extractElementsOfTypes(KtCallExpression::class.java) - callExpression.filter { it.text == methodName }.mapNotNull { - it.parentsOfType(KtNamedFunction::class.java).firstOrNull()?.name - }.toList() - } - -/** - * Checks if the PsiFile contains a property with the specified property name. - * - * @param propertyName The name of the property to search for. - * @return True if the PsiFile contains a property with the given name, false otherwise. - */ -fun PsiFile.hasProperty(propertyName: String): Boolean = ApplicationManager.getApplication().runReadAction { - extractElementsOfTypes(KtProperty::class.java).any { it.name == propertyName } -} - -/** - * Checks if the PsiFile contains a method with the specified method name. - * - * @param methodName The name of the method to search for. - * @return True if the PsiFile contains a method with the given name, false otherwise. - */ -fun PsiFile.hasMethod(methodName: String): Boolean = ApplicationManager.getApplication().runReadAction { - extractElementsOfTypes(KtNamedFunction::class.java).any { it.name == methodName } -} - -/** - * Retrieves the text of the parent element of the given PsiElement. - * - * @param element The PsiElement for which to find the parent. - * @param isParentTypeFunction If true, the parent element is expected to be a function (KtNamedFunction). - * If false, the parent element is expected to be a class or any other non-function type. - * @return The text of the parent element if found, or "no name" if the parent is not found or has no name (for functions). - */ -private fun getParentText(element: PsiElement, isParentTypeFunction: Boolean): String? { - return if (isParentTypeFunction) { - element.parentsOfType(KtNamedFunction::class.java).firstOrNull()?.name - } else { - element.parent?.text - } -} - -/** - * Checks if the PsiFile contains an expression with the specified text that has a specific parent element. - * - * @param expression The text of the expression to search for. - * @param parent The text of the parent element to check against. - * @param isParentTypeFunction If true, the parent element is expected to be a function (KtNamedFunction). - * If false, the parent element is expected to be a class or any other non-function type. - * @return True if the PsiFile contains an expression with the given text and the specified parent, false otherwise. - */ -fun PsiFile.hasExpressionWithParent(expression: String, parent: String?, isParentTypeFunction: Boolean): Boolean = - ApplicationManager.getApplication().runReadAction { - val expressions: MutableCollection = - extractElementsOfTypes(KtDotQualifiedExpression::class.java, KtCallExpression::class.java) - expressions.any { it.text == expression && getParentText(it, isParentTypeFunction) == parent } - } diff --git a/ij/src/main/kotlin/org/jetbrains/academy/test/system/test/BaseIjTestClass.kt b/ij/src/main/kotlin/org/jetbrains/academy/test/system/test/BaseIjTestClass.kt deleted file mode 100644 index 1252588..0000000 --- a/ij/src/main/kotlin/org/jetbrains/academy/test/system/test/BaseIjTestClass.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.jetbrains.academy.test.system.test - -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import org.jetbrains.academy.test.system.ij.analyzer.findMethodUsages -import org.jetbrains.academy.test.system.ij.analyzer.findMethodsWithContent -import org.jetbrains.academy.test.system.ij.analyzer.hasConstantWithGivenValue -import org.jetbrains.academy.test.system.ij.analyzer.hasMethod -import org.jetbrains.academy.test.system.ij.analyzer.hasProperty -import org.jetbrains.academy.test.system.ij.analyzer.hasExpressionWithParent - - -/** - * A Base test class to create tests with PSI without adding intellij dependency into courses directly. - */ -open class BaseIjTestClass : BasePlatformTestCase() { - - fun hasConstantWithGivenValue(elementValue: String): Boolean = - myFixture.file.hasConstantWithGivenValue(elementValue) - - fun findMethodsWithContent(content: String): List = - myFixture.file.findMethodsWithContent(content) - - fun findMethodUsages(content: String): List = myFixture.file.findMethodUsages(content) - - fun hasProperty(propertyName: String): Boolean = myFixture.file.hasProperty(propertyName) - - fun hasMethod(methodName: String): Boolean = myFixture.file.hasMethod(methodName) - - fun hasExpressionWithParent(expression: String, parent: String?, isParentTypeFunction: Boolean = false): Boolean = - myFixture.file.hasExpressionWithParent(expression, parent, isParentTypeFunction) -} diff --git a/settings.gradle.kts b/settings.gradle.kts index fa99df3..c06c676 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,3 +12,9 @@ include( ) rootProject.name = "kotlin-test-system" +include("ij:kotlin-psi") +findProject(":ij:kotlin-psi")?.name = "kotlin-psi" +include("ij:java-psi") +findProject(":ij:java-psi")?.name = "java-psi" +include("ij:common") +findProject(":ij:common")?.name = "common"