From 94dce6dadd9b84f5f007ae298bba3f0638cfac05 Mon Sep 17 00:00:00 2001 From: anchouls Date: Wed, 13 Dec 2023 15:23:08 +0100 Subject: [PATCH] Moved some functions from PsiAnalyzerUtil to the common module and parameterized tests --- .../ij/analyzer/PsiElementAnalyzerUtil.kt | 39 ++++ .../system/ij/analyzer/PsiFileAnalyzerUtil.kt | 87 ++++++++ .../java/ij/analyzer/IjCodeAnalyzerUtil.kt | 196 ------------------ .../java/ij/analyzer/PsiFileAnalyzerUtil.kt | 57 +++++ .../test/system/java/test/BaseIjTestClass.kt | 36 ++-- .../system/java/ij/BaseIjTestClassTests.kt | 92 ++++---- .../kotlin/ij/analyzer/IjCodeAnalyzerUtil.kt | 176 ---------------- .../kotlin/ij/analyzer/PsiFileAnalyzerUtil.kt | 45 ++++ .../system/kotlin/test/BaseIjTestClass.kt | 35 ++-- .../system/kotlin/ij/BaseIjTestClassTests.kt | 101 ++++----- 10 files changed, 359 insertions(+), 505 deletions(-) create mode 100644 ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiElementAnalyzerUtil.kt create mode 100644 ij/common/src/main/kotlin/org/jetbrains/academy/test/system/ij/analyzer/PsiFileAnalyzerUtil.kt delete mode 100644 ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/IjCodeAnalyzerUtil.kt create mode 100644 ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/PsiFileAnalyzerUtil.kt delete mode 100644 ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/IjCodeAnalyzerUtil.kt create mode 100644 ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/PsiFileAnalyzerUtil.kt 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/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/IjCodeAnalyzerUtil.kt b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/IjCodeAnalyzerUtil.kt deleted file mode 100644 index da0a41b..0000000 --- a/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/ij/analyzer/IjCodeAnalyzerUtil.kt +++ /dev/null @@ -1,196 +0,0 @@ -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.PsiElement -import com.intellij.psi.PsiField -import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiNamedElement -import com.intellij.psi.PsiCallExpression -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.PsiFileFactory -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiCodeBlock -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.psi.util.parentsOfType -import org.jetbrains.academy.test.system.ij.formatting.formatting - -/** Extracts elements of given type from related files in project. */ -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 PsiField.getConstValue(): String? { - val possibleValue = extractElementsOfTypes(PsiLiteralExpression::class.java) - if (possibleValue.isEmpty()) { - return null - } - require(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(PsiField::class.java) - elements.any { it.modifierList?.text?.contains("final") ?: 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 PsiMethod.getBlockBody(): String? { - val possibleBody = extractElementsOfTypes(PsiCodeBlock::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("}") - -/** - * 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() == 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(PsiCallExpression::class.java) - callExpression.filter { it.text == methodName }.mapNotNull { - it.parentsOfType(PsiMethod::class.java).firstOrNull()?.name - }.toList() - } - -/** - * Checks if the given PsiFile contains an element of the specified type and with the provided name. - * - * @param psiElementClass The class object representing the type of the element to search for. - * @param name The name of the element to search for. - * @return `true` if an element with the specified type and name is found, `false` otherwise. - */ -fun PsiFile.hasElementOfTypeWithName(psiElementClass: Class, name: String): Boolean = - ApplicationManager.getApplication().runReadAction { - extractElementsOfTypes(psiElementClass).any { it.name == name } - } - -/** - * Checks if the PsiFile contains a filed with the specified name. - * - * @param filedName The name of the filed to search for. - * @return True if the PsiFile contains a property with the given name, false otherwise. - */ -fun PsiFile.hasProperty(filedName: String): Boolean = hasElementOfTypeWithName(PsiField::class.java, filedName) - -/** - * 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 = hasElementOfTypeWithName(PsiMethod::class.java, methodName) - -/** - * Checks whether the [PsiFile] contains a class with the specified [className]. - * - * @param className The name of the class to check for. - * @return `true` if the [PsiFile] contains a class with the specified [className], `false` otherwise. - */ -fun PsiFile.hasClass(className: String): Boolean = hasElementOfTypeWithName(PsiClass::class.java, className) - -/** - * Checks if the [PsiFile] has a parameter with the specified name. - * - * @param parameterName the name of the parameter to check - * @return true if the [PsiFile] contains a parameter with the specified name, false otherwise - */ -fun PsiFile.hasParameter(parameterName: String): Boolean = hasElementOfTypeWithName(PsiParameter::class.java, parameterName) - -/** - * 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. - * 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(PsiMethod::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. - * 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( - PsiNewExpression::class.java, - PsiReferenceExpression::class.java, - PsiMethodCallExpression::class.java - ) - expressions.any { it.text == expression && getParentText(it, isParentTypeFunction) == parent } - } 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/test/BaseIjTestClass.kt b/ij/java-psi/src/main/kotlin/org/jetbrains/academy/test/system/java/test/BaseIjTestClass.kt index 58e833b..2823548 100644 --- 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 @@ -1,14 +1,19 @@ 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.java.ij.analyzer.findMethodUsages +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 -import org.jetbrains.academy.test.system.java.ij.analyzer.hasMethod -import org.jetbrains.academy.test.system.java.ij.analyzer.hasProperty -import org.jetbrains.academy.test.system.java.ij.analyzer.hasExpressionWithParent -import org.jetbrains.academy.test.system.java.ij.analyzer.hasClass -import org.jetbrains.academy.test.system.java.ij.analyzer.hasParameter /** * A Base test class to create tests with PSI without adding intellij dependency into courses directly. @@ -21,16 +26,23 @@ open class BaseIjTestClass : BasePlatformTestCase() { fun findMethodsWithContent(content: String): List = myFixture.file.findMethodsWithContent(content) - fun findMethodUsages(content: String): List = myFixture.file.findMethodUsages(content) + fun findMethodUsages(content: String): List = + myFixture.file.findMethodUsages(content, PsiCallExpression::class.java, PsiMethod::class.java) - fun hasProperty(propertyName: String): Boolean = myFixture.file.hasProperty(propertyName) + fun hasProperty(propertyName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(PsiField::class.java, propertyName) - fun hasMethod(methodName: String): Boolean = myFixture.file.hasMethod(methodName) + fun hasMethod(methodName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(PsiMethod::class.java, methodName) - fun hasClass(className: String): Boolean = myFixture.file.hasClass(className) + fun hasClass(className: String): Boolean = myFixture.file.hasElementOfTypeWithName(PsiClass::class.java, className) - fun hasParameter(parameterName: String): Boolean = myFixture.file.hasParameter(parameterName) + 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) + 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/BaseIjTestClassTests.kt b/ij/java-psi/src/test/kotlin/org/jetbrains/academy/test/system/java/ij/BaseIjTestClassTests.kt index 134ce62..71fb4ca 100644 --- 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 @@ -2,10 +2,42 @@ 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() { - fun testFindMethodsWithContent() { + 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 { @@ -27,18 +59,14 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.java", example) - val content = """ - String actions = "Some actions"; - System.out.println("Content"); - System.out.println(actions); - """.trimIndent() - val methodName = "method1" assert(listOf(methodName).equals(findMethodsWithContent(content))) { "The name of a method with this content \n $content \n must be $methodName" } } - fun testFindMethodsWithContentWithBrokenFormatting() { + @ParameterizedTest + @MethodSource("findMethodsWithContentTestProvider") + fun testFindMethodsWithContentWithBrokenFormatting(content: String, methodName: String) { val example = """ public class ExampleClass { @@ -55,12 +83,6 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.java", example) - val content = """ - String actions = "Some actions"; - System.out.println("Content"); - System.out.println(actions); - """.trimIndent() - val methodName = "method1" assert(listOf(methodName).equals(findMethodsWithContent(content))) { "The name of a method with this content \n $content \n must be $methodName" } @@ -133,7 +155,9 @@ class BaseIjTestClassTests : BaseIjTestClass() { assertFalse(hasConstantWithGivenValue("0.5")) } - fun testFindMethodUsages() { + @ParameterizedTest + @MethodSource("findMethodUsagesTestProvider") + fun testFindMethodUsages(methodName: String, methodsList: List) { val example = """ public class ExampleClass { @@ -164,26 +188,9 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.java", example) - var methodName = "method(\"Content\")" - var methodsList = listOf("outerFunction", "method2") assert(methodsList.equals(findMethodUsages(methodName))) { "Method $methodName should be called in methods: $methodsList" } - methodName = "method(\"y is greater than 5\")" - methodsList = listOf("method1") - assert(methodsList.equals(findMethodUsages(methodName))) { - "Method $methodName should be called in methods: $methodsList" - } - methodName = "method(\"y is less than 5\")" - methodsList = listOf("method1") - assert(methodsList.equals(findMethodUsages(methodName))) { - "Method $methodName should be called in methods: $methodsList" - } - methodName = "method(content)" - methodsList = listOf() - assert(methodsList.equals(findMethodUsages(methodName))) { - "Method $methodName should not be called" - } } fun testHasProperty() { @@ -284,7 +291,9 @@ class BaseIjTestClassTests : BaseIjTestClass() { Assertions.assertFalse(hasParameter("Content")) } - fun testHasExpressionWithParent() { + @ParameterizedTest + @MethodSource("hasExpressionWithParentTestProvider") + fun testHasExpressionWithParent(expression: String, parent: String?) { val example = """ import java.io.File; import java.io.PrintWriter; @@ -313,23 +322,6 @@ class BaseIjTestClassTests : BaseIjTestClass() { } """.trimIndent() myFixture.configureByText("Task.java", example) - var expression = "productPrice.size()" - var parent: String? = "sum / productPrice.size()" - assert(hasExpressionWithParent(expression, parent)) { - "There must exist an expression $expression with parent $parent" - } - expression = "new File(\"Exception.txt\")" - parent = "(new File(\"Exception.txt\"), \"UTF-8\")" - assert(hasExpressionWithParent(expression, parent)) { - "There must exist an expression $expression with parent $parent" - } - expression = "new PrintWriter(new File(\"Exception.txt\"), \"UTF-8\")" - parent = "calculateAveragePrice" - assert(hasExpressionWithParent(expression, parent, true)) { - "There must exist an expression $expression with parent $parent" - } - expression = "Integer.MAX_VALUE" - parent = "private static final int CONSTANT = Integer.MAX_VALUE;" assert(hasExpressionWithParent(expression, parent)) { "There must exist an expression $expression with parent $parent" } diff --git a/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/IjCodeAnalyzerUtil.kt b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/IjCodeAnalyzerUtil.kt deleted file mode 100644 index 86646ec..0000000 --- a/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/ij/analyzer/IjCodeAnalyzerUtil.kt +++ /dev/null @@ -1,176 +0,0 @@ -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.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiFileFactory -import com.intellij.psi.PsiNamedElement -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. */ -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 = - 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() == 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 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 } - } - -/** - * 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 = hasElementOfTypeWithName(KtProperty::class.java, 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 = hasElementOfTypeWithName(KtNamedFunction::class.java, methodName) - -/** - * Checks whether the [PsiFile] contains a class with the specified [className]. - * - * @param className The name of the class to check for. - * @return `true` if the [PsiFile] contains a class with the specified [className], `false` otherwise. - */ -fun PsiFile.hasClass(className: String): Boolean = hasElementOfTypeWithName(KtClass::class.java, className) - -/** - * Checks if the [PsiFile] has a parameter with the specified name. - * - * @param parameterName the name of the parameter to check - * @return true if the [PsiFile] contains a parameter with the specified name, false otherwise - */ -fun PsiFile.hasParameter(parameterName: String): Boolean = hasElementOfTypeWithName(KtParameter::class.java, parameterName) - -/** - * 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/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/test/BaseIjTestClass.kt b/ij/kotlin-psi/src/main/kotlin/org/jetbrains/academy/test/system/kotlin/test/BaseIjTestClass.kt index fb9cee2..da2cf00 100644 --- 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 @@ -1,15 +1,17 @@ package org.jetbrains.academy.test.system.kotlin.test import com.intellij.testFramework.fixtures.BasePlatformTestCase -import org.jetbrains.academy.test.system.kotlin.ij.analyzer.findMethodUsages +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.academy.test.system.kotlin.ij.analyzer.hasMethod -import org.jetbrains.academy.test.system.kotlin.ij.analyzer.hasProperty -import org.jetbrains.academy.test.system.kotlin.ij.analyzer.hasExpressionWithParent -import org.jetbrains.academy.test.system.kotlin.ij.analyzer.hasClass -import org.jetbrains.academy.test.system.kotlin.ij.analyzer.hasParameter - +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. @@ -22,16 +24,23 @@ open class BaseIjTestClass : BasePlatformTestCase() { fun findMethodsWithContent(content: String): List = myFixture.file.findMethodsWithContent(content) - fun findMethodUsages(content: String): List = myFixture.file.findMethodUsages(content) + fun findMethodUsages(content: String): List = + myFixture.file.findMethodUsages(content, KtCallExpression::class.java, KtNamedFunction::class.java) - fun hasProperty(propertyName: String): Boolean = myFixture.file.hasProperty(propertyName) + fun hasProperty(propertyName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(KtProperty::class.java, propertyName) - fun hasMethod(methodName: String): Boolean = myFixture.file.hasMethod(methodName) + fun hasMethod(methodName: String): Boolean = + myFixture.file.hasElementOfTypeWithName(KtNamedFunction::class.java, methodName) - fun hasClass(className: String): Boolean = myFixture.file.hasClass(className) + fun hasClass(className: String): Boolean = myFixture.file.hasElementOfTypeWithName(KtClass::class.java, className) - fun hasParameter(parameterName: String): Boolean = myFixture.file.hasParameter(parameterName) + 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) + myFixture.file.hasExpressionWithParent( + expression, parent, isParentTypeFunction, KtNamedFunction::class.java, + KtDotQualifiedExpression::class.java, KtCallExpression::class.java + ) } diff --git a/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseIjTestClassTests.kt b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseIjTestClassTests.kt index 9fa9a1d..999d4b6 100644 --- a/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseIjTestClassTests.kt +++ b/ij/kotlin-psi/src/test/kotlin/org/jetbrains/academy/test/system/kotlin/ij/BaseIjTestClassTests.kt @@ -2,10 +2,43 @@ package org.jetbrains.academy.test.system.kotlin.ij 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() { @@ -287,7 +293,9 @@ class BaseIjTestClassTests : BaseIjTestClass() { Assertions.assertFalse(hasParameter("Content")) } - fun testHasExpressionWithParent() { + @ParameterizedTest + @MethodSource("hasExpressionWithParentTestProvider") + fun testHasExpressionWithParent(expression: String, parent: String?) { val example = """ private const val CONSTANT = Int.MAX_VALUE @@ -301,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" - ) } }