From fb0bfeda5a4c0d57793f8b5c10a1879c6a7545f0 Mon Sep 17 00:00:00 2001 From: "aleksandr.izmaylov" Date: Wed, 17 Jul 2019 12:35:05 +0300 Subject: [PATCH] [uast] small part of implementation of code generation/modification on UAST. reviewed in IDEA-CR-50669 GitOrigin-RevId: 89328f198c55576137f831c1963bf359cdae9fa9 --- .../src/META-INF/JavaPsiPlugin.xml | 8 +- .../uast/generate/UastCodeGenerationPlugin.kt | 63 ++++++ .../generate/JavaUastCodeGenerationPlugin.kt | 200 ++++++++++++++++++ .../test/java/AbstractJavaUastLightTest.kt | 11 + .../java/generate/JavaUastGenerationTest.kt | 180 ++++++++++++++++ 5 files changed, 458 insertions(+), 4 deletions(-) create mode 100644 uast/uast-common/src/org/jetbrains/uast/generate/UastCodeGenerationPlugin.kt create mode 100644 uast/uast-java/src/org/jetbrains/uast/java/generate/JavaUastCodeGenerationPlugin.kt create mode 100644 uast/uast-tests/test/org/jetbrains/uast/test/java/AbstractJavaUastLightTest.kt create mode 100644 uast/uast-tests/test/org/jetbrains/uast/test/java/generate/JavaUastGenerationTest.kt diff --git a/java/java-psi-impl/src/META-INF/JavaPsiPlugin.xml b/java/java-psi-impl/src/META-INF/JavaPsiPlugin.xml index 3deaeff27640..4617dcbf891d 100644 --- a/java/java-psi-impl/src/META-INF/JavaPsiPlugin.xml +++ b/java/java-psi-impl/src/META-INF/JavaPsiPlugin.xml @@ -51,6 +51,8 @@ + @@ -126,10 +128,8 @@ - - - - + + diff --git a/uast/uast-common/src/org/jetbrains/uast/generate/UastCodeGenerationPlugin.kt b/uast/uast-common/src/org/jetbrains/uast/generate/UastCodeGenerationPlugin.kt new file mode 100644 index 000000000000..15432ea3cd91 --- /dev/null +++ b/uast/uast-common/src/org/jetbrains/uast/generate/UastCodeGenerationPlugin.kt @@ -0,0 +1,63 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.uast.generate + +import com.intellij.lang.Language +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiType +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.uast.* + +@ApiStatus.Experimental +interface UastCodeGenerationPlugin { + companion object { + val extensionPointName = ExtensionPointName("org.jetbrains.uast.generate.uastCodeGenerationPlugin") + private val extensions by lazy { extensionPointName.extensionList } + + @JvmStatic + fun byLanguage(language: Language) = extensions.firstOrNull { it.language == language } + } + + val language: Language + + fun createBinaryExpression(leftOperand: UExpression, rightOperand: UExpression, operator: UastBinaryOperator): UBinaryExpression? + + /** + * Create binary expression, and possibly remove unnecessary parenthesis, so it could become [UPolyadicExpression], e.g + * [createFlatBinaryExpression](1 + 2, 2, +) could produce 1 + 2 + 2, which is polyadic expression + */ + @JvmDefault + fun createFlatBinaryExpression(leftOperand: UExpression, rightOperand: UExpression, operator: UastBinaryOperator): UPolyadicExpression? = + createBinaryExpression(leftOperand, rightOperand, operator) + + fun createSimpleReference(name: String, project: Project): USimpleNameReferenceExpression? + + fun createSimpleReference(variable: UVariable): USimpleNameReferenceExpression? + + fun createParenthesizedExpression(expression: UExpression): UParenthesizedExpression? + + fun createReturnExpresion(expression: UExpression, inLambda: Boolean = false): UReturnExpression? + + fun createLocalVariable(name: String?, type: PsiType?, initializer: UExpression, immutable: Boolean = false): ULocalVariable? + + fun createBlockExpression(expressions: List, project: Project): UBlockExpression? + + fun createLambdaExpression(parameters: List, body: UExpression): ULambdaExpression? + + fun createDeclarationExpression(declarations: List, project: Project): UDeclarationsExpression? + + fun replace(oldElement: UElement, newElement: T, elementType: Class): T? +} + +@ApiStatus.Experimental +inline fun UElement.replace(newElement: T): T? = + this.getLanguagePlugin() + .let { UastCodeGenerationPlugin.byLanguage(it.language) } + ?.replace(this, newElement, T::class.java) + +@ApiStatus.Experimental +inline fun T.refreshed() = sourcePsi?.toUElementOfType() + +val UElement.generationPlugin: UastCodeGenerationPlugin? + @ApiStatus.Experimental + get() = getLanguagePlugin().let { UastCodeGenerationPlugin.byLanguage(it.language) } \ No newline at end of file diff --git a/uast/uast-java/src/org/jetbrains/uast/java/generate/JavaUastCodeGenerationPlugin.kt b/uast/uast-java/src/org/jetbrains/uast/java/generate/JavaUastCodeGenerationPlugin.kt new file mode 100644 index 000000000000..f3c7b3e6d0ee --- /dev/null +++ b/uast/uast-java/src/org/jetbrains/uast/java/generate/JavaUastCodeGenerationPlugin.kt @@ -0,0 +1,200 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.uast.java.generate + +import com.intellij.codeInsight.BlockUtils +import com.intellij.lang.Language +import com.intellij.lang.java.JavaLanguage +import com.intellij.lang.jvm.JvmModifier +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.codeStyle.VariableKind +import com.intellij.psi.impl.source.tree.CompositeElement +import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.psiutils.ParenthesesUtils +import org.jetbrains.uast.* +import org.jetbrains.uast.generate.UastCodeGenerationPlugin +import org.jetbrains.uast.java.* +import java.lang.AssertionError + +class JavaUastCodeGenerationPlugin : UastCodeGenerationPlugin { + override fun replace(oldElement: UElement, newElement: T, elementType: Class): T? = + newElement.sourcePsi?.let { oldElement.sourcePsi?.replace(it) }?.toUElementOfExpectedTypes(elementType) + + override fun createDeclarationExpression(declarations: List, project: Project): UDeclarationsExpression? { + return JavaUDeclarationsExpression(null, declarations) + } + + private fun PsiElementFactory.createExpresionStatement(expression: PsiExpression): PsiStatement? { + val statement = createStatementFromText("x;", null) as? PsiExpressionStatement ?: return null + statement.expression.replace(expression) + return statement + } + + override fun createReturnExpresion(expression: UExpression, inLambda: Boolean): UReturnExpression? { + val factory = JavaPsiFacade.getElementFactory(expression.sourcePsi?.project ?: return null) + val returnStatement = factory.createStatementFromText("return ;", null) as? PsiReturnStatement ?: return null + + (returnStatement as CompositeElement).addChild(expression.sourcePsi!!.node, returnStatement.lastChild.node) + + return JavaUReturnExpression(returnStatement, null) + } + + override fun createLocalVariable(name: String?, type: PsiType?, initializer: UExpression, immutable: Boolean): ULocalVariable? { + val initializerPsi = initializer.sourcePsi as? PsiExpression ?: return null + + if (name == null) { + return createLocalVariableWithSuggestedName(type, initializer, immutable) + } + + val factory = JavaPsiFacade.getElementFactory(initializerPsi.project) + val variable = (type ?: initializer.getExpressionType())?.let { variableType -> + factory.createVariableDeclarationStatement(name, variableType, initializerPsi, + initializerPsi.context).declaredElements.firstOrNull() as? PsiLocalVariable + } ?: return null + + if (immutable && !variable.hasModifier(JvmModifier.FINAL)) { + PsiUtil.setModifierProperty(variable, PsiModifier.FINAL, true) + } + + if (!immutable && variable.hasModifier(JvmModifier.FINAL)) { + PsiUtil.setModifierProperty(variable, PsiModifier.FINAL, false) + } + + return JavaULocalVariable(variable, null) + } + + private fun createLocalVariableWithSuggestedName(type: PsiType?, initializer: UExpression, immutable: Boolean): ULocalVariable? { + val initializerPsi = initializer.sourcePsi as? PsiExpression ?: return null + val codeStyleManager = JavaCodeStyleManager.getInstance(initializerPsi.project) + val resultType = type ?: initializerPsi.type ?: return null + var suggestedNameInfo = codeStyleManager.suggestVariableName(VariableKind.LOCAL_VARIABLE, null, initializerPsi, resultType) + suggestedNameInfo = codeStyleManager.suggestUniqueVariableName(suggestedNameInfo, initializerPsi, true) + val name = suggestedNameInfo.names.firstOrNull() ?: return null + return createLocalVariable(name, type, initializer, immutable) + } + + override fun createBlockExpression(expressions: List, project: Project): UBlockExpression? { + val factory = JavaPsiFacade.getElementFactory(project) + val blockStatement = BlockUtils.createBlockStatement(project) + + for (expression in expressions) { + if (expression is JavaUDeclarationsExpression) { + for (declaration in expression.declarations) { + if (declaration.sourcePsi is PsiLocalVariable) { + blockStatement.codeBlock.add(declaration.sourcePsi?.parent as? PsiDeclarationStatement ?: return null) + } + } + continue + } + + expression.sourcePsi?.let { psi -> + psi as? PsiStatement ?: (psi as? PsiExpression)?.let { factory.createExpresionStatement(it) } + }?.let { blockStatement.codeBlock.add(it) } ?: return null + } + + return JavaUBlockExpression(blockStatement, null) + } + + override fun createLambdaExpression(parameters: List, body: UExpression): ULambdaExpression? { + val factory = JavaPsiFacade.getElementFactory(body.sourcePsi?.project ?: return null) + val lambda = factory.createExpressionFromText( + buildString { + parameters.joinTo(this, separator = ",", prefix = "(", postfix = ")") { "x" } + + append("->") + append("{}") + }, + null + ) as? PsiLambdaExpression ?: return null + + lambda.parameterList.parameters.forEachIndexed { i, p -> + p.replace(parameters[i].sourcePsi ?: return null) + } + + if (lambda.parameterList.parameters.any { it.typeElement == null }) + lambda.parameterList.parameters.forEach { + it.typeElement?.delete() + if (it.children.getOrNull(1) !is PsiIdentifier) + it.children.getOrNull(1)?.delete() + } + + if (lambda.parameterList.parametersCount == 1 && lambda.parameterList.parameters[0].typeElement == null) { + lambda.parameterList.firstChild.delete() + lambda.parameterList.lastChild.delete() + } + + val normalizedBody = when (val bodyPsi = body.sourcePsi) { + is PsiExpression -> bodyPsi + is PsiCodeBlock -> normalizeBlockForLambda(bodyPsi) + is PsiBlockStatement -> normalizeBlockForLambda(bodyPsi.codeBlock) + else -> return null + } + + lambda.body?.replace(normalizedBody) ?: return null + + return JavaULambdaExpression(lambda, null) + } + + private fun normalizeBlockForLambda(block: PsiCodeBlock): PsiElement = block + .takeIf { block.statementCount == 1 } + ?.let { block.statements[0] as? PsiReturnStatement } + ?.let { it.returnValue } ?: block + + override fun createParenthesizedExpression(expression: UExpression): UParenthesizedExpression? { + val factory = JavaPsiFacade.getElementFactory(expression.sourcePsi?.project ?: return null) + val parenthesizedExpression = factory.createExpressionFromText("()", null) as? PsiParenthesizedExpression ?: return null + parenthesizedExpression.children.getOrNull(1)?.replace(expression.sourcePsi ?: return null) ?: return null + return JavaUParenthesizedExpression(parenthesizedExpression, null) + } + + override fun createSimpleReference(name: String, project: Project): USimpleNameReferenceExpression? { + val factory = JavaPsiFacade.getElementFactory(project) + val reference = factory.createExpressionFromText(name, null) + return JavaUSimpleNameReferenceExpression(reference, name, null) + } + + override fun createSimpleReference(variable: UVariable): USimpleNameReferenceExpression? { + return createSimpleReference(variable.name ?: return null, variable.sourcePsi?.project ?: return null) + } + + override val language: Language + get() = JavaLanguage.INSTANCE + + + override fun createBinaryExpression(leftOperand: UExpression, + rightOperand: UExpression, + operator: UastBinaryOperator): UBinaryExpression? { + val project = leftOperand.sourcePsi?.project ?: return null + val leftPsi = leftOperand.sourcePsi ?: return null + val rightPsi = rightOperand.sourcePsi ?: return null + val factory = JavaPsiFacade.getElementFactory(project) + + val operatorSymbol = when (operator) { + UastBinaryOperator.LOGICAL_AND -> "&&" + else -> return null + } + val psiBinaryExpression = factory.createExpressionFromText("a $operatorSymbol b", null) as? PsiBinaryExpression + ?: return null + psiBinaryExpression.lOperand.replace(leftPsi) + psiBinaryExpression.rOperand?.replace(rightPsi) + + return JavaUBinaryExpression(psiBinaryExpression, null) + } + + override fun createFlatBinaryExpression(leftOperand: UExpression, + rightOperand: UExpression, + operator: UastBinaryOperator): UPolyadicExpression? { + val binaryExpression = createBinaryExpression(leftOperand, rightOperand, operator) ?: return null + val binarySourcePsi = binaryExpression.sourcePsi as? PsiBinaryExpression ?: return null + val factory = JavaPsiFacade.getElementFactory(binarySourcePsi.project) + val dummyParent = factory.createStatementFromText("a;", null) + dummyParent.firstChild.replace(binarySourcePsi) + ParenthesesUtils.removeParentheses(dummyParent.firstChild as PsiExpression, false) + return when(val result = dummyParent.firstChild) { + is PsiBinaryExpression -> JavaUBinaryExpression(result, null) + is PsiPolyadicExpression -> JavaUPolyadicExpression(result, null) + else -> throw AssertionError("Unexpected type " + result.javaClass) + } + } +} \ No newline at end of file diff --git a/uast/uast-tests/test/org/jetbrains/uast/test/java/AbstractJavaUastLightTest.kt b/uast/uast-tests/test/org/jetbrains/uast/test/java/AbstractJavaUastLightTest.kt new file mode 100644 index 000000000000..20630214e32f --- /dev/null +++ b/uast/uast-tests/test/org/jetbrains/uast/test/java/AbstractJavaUastLightTest.kt @@ -0,0 +1,11 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.uast.test.java + +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase +import junit.framework.TestCase + +abstract class AbstractJavaUastLightTest : LightJavaCodeInsightFixtureTestCase() { + protected fun fail(message: String): Nothing { + throw AssertionError(message) + } +} \ No newline at end of file diff --git a/uast/uast-tests/test/org/jetbrains/uast/test/java/generate/JavaUastGenerationTest.kt b/uast/uast-tests/test/org/jetbrains/uast/test/java/generate/JavaUastGenerationTest.kt new file mode 100644 index 000000000000..d5ef8a79ae8f --- /dev/null +++ b/uast/uast-tests/test/org/jetbrains/uast/test/java/generate/JavaUastGenerationTest.kt @@ -0,0 +1,180 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.uast.test.java.generate + +import com.intellij.lang.java.JavaLanguage +import com.intellij.psi.* +import junit.framework.TestCase +import org.jetbrains.uast.* +import org.jetbrains.uast.generate.UastCodeGenerationPlugin +import org.jetbrains.uast.generate.refreshed +import org.jetbrains.uast.generate.replace +import org.jetbrains.uast.test.java.AbstractJavaUastLightTest + +class JavaUastGenerationTest : AbstractJavaUastLightTest() { + + private val psiFactory: PsiElementFactory + get() = JavaPsiFacade.getElementFactory(myFixture.project) + private val generatePlugin: UastCodeGenerationPlugin + get() = UastCodeGenerationPlugin.byLanguage(JavaLanguage.INSTANCE)!! + + fun `test logical and operation with simple operands`() { + val left = psiFactory.createExpressionFromText("true", null).toUElementOfType() + ?: fail("Cannot create left UExpression") + val right = psiFactory.createExpressionFromText("false", null).toUElementOfType() + ?: fail("Cannot create right UExpression") + + val expression = generatePlugin.createBinaryExpression(left, right, UastBinaryOperator.LOGICAL_AND) + ?: fail("Cannot create expression") + + TestCase.assertEquals("true && false", expression.sourcePsi?.text) + } + + fun `test logical and operation with simple operands with parenthesis`() { + val left = psiFactory.createExpressionFromText("(true)", null).toUElementOfType() + ?: fail("Cannot create left UExpression") + val right = psiFactory.createExpressionFromText("(false)", null).toUElementOfType() + ?: fail("Cannot create right UExpression") + + val expression = generatePlugin.createFlatBinaryExpression(left, right, UastBinaryOperator.LOGICAL_AND) + ?: fail("Cannot create expression") + + TestCase.assertTrue(expression.sourcePsi is PsiBinaryExpression) + TestCase.assertEquals("true && false", expression.sourcePsi?.text) + } + + fun `test logical and operation with simple operands with parenthesis polyadic`() { + val left = psiFactory.createExpressionFromText("(true && false)", null).toUElementOfType() + ?: fail("Cannot create left UExpression") + val right = psiFactory.createExpressionFromText("(false)", null).toUElementOfType() + ?: fail("Cannot create right UExpression") + + val expression = generatePlugin.createFlatBinaryExpression(left, right, UastBinaryOperator.LOGICAL_AND) + ?: fail("Cannot create expression") + + TestCase.assertTrue(expression.sourcePsi is PsiPolyadicExpression) + TestCase.assertEquals("true && false && false", expression.sourcePsi?.text) + } + + fun `test simple reference creating from variable`() { + val variable = psiFactory.createVariableDeclarationStatement( + "a", + PsiType.INT + , null + ).declaredElements.getOrNull(0)?.toUElementOfType() ?: fail("cannot create variable") + + val reference = generatePlugin.createSimpleReference(variable) ?: fail("cannot create reference") + TestCase.assertEquals("a", reference.identifier) + } + + fun `test simple reference by name`() { + val reference = generatePlugin.createSimpleReference("a", myFixture.project) ?: fail("cannot create reference") + TestCase.assertEquals("a", reference.identifier) + } + + fun `test parenthesised expression`() { + val expression = psiFactory.createExpressionFromText("a + b", null).toUElementOfType() + ?: fail("cannot create expression") + val parenthesizedExpression = generatePlugin.createParenthesizedExpression(expression) + ?: fail("cannot create parenthesized expression") + + TestCase.assertEquals("(a + b)", parenthesizedExpression.sourcePsi?.text) + } + + fun `test return expression`() { + val expression = psiFactory.createExpressionFromText("a + b", null).toUElementOfType() + ?: fail("Cannot find plugin") + + val returnExpression = generatePlugin.createReturnExpresion(expression) ?: fail("cannot create return expression") + + TestCase.assertEquals("return a + b;", returnExpression.sourcePsi?.text) + } + + fun `test variable declaration without type`() { + val expression = psiFactory.createExpressionFromText("1 + 2", null).toUElementOfType() + ?: fail("cannot create variable declaration") + + val declaration = generatePlugin.createLocalVariable("a", null, expression) ?: fail("cannot create variable") + + TestCase.assertEquals("int a = 1 + 2;", declaration.sourcePsi?.text) + } + + fun `test variable declaration with type`() { + val expression = psiFactory.createExpressionFromText("b", null).toUElementOfType() + ?: fail("cannot create variable declaration") + + val declaration = generatePlugin.createLocalVariable("a", PsiType.DOUBLE, expression) ?: fail("cannot create variable") + + TestCase.assertEquals("double a = b;", declaration.sourcePsi?.text) + } + + fun `test final variable declaration`() { + val expression = psiFactory.createExpressionFromText("b", null).toUElementOfType() + ?: fail("cannot create variable declaration") + + val declaration = generatePlugin.createLocalVariable("a", PsiType.DOUBLE, expression, true) ?: fail("cannot create variable") + + TestCase.assertEquals("final double a = b;", declaration.sourcePsi?.text) + } + + fun `test block expression`() { + val statement1 = psiFactory.createStatementFromText("System.out.println();", null).toUElementOfType() + ?: fail("cannot create statement") + val statement2 = psiFactory.createStatementFromText("System.out.println(2);", null).toUElementOfType() + ?: fail("cannot create statement") + + val block = generatePlugin.createBlockExpression(listOf(statement1, statement2), myFixture.project) ?: fail("cannot create block") + + TestCase.assertEquals("{" + + "System.out.println();" + + "System.out.println(2);" + + "}", block.sourcePsi?.text) + } + + fun `test lambda expression`() { + val statement = psiFactory.createStatementFromText("System.out.println();", null).toUElementOfType() + ?: fail("cannot create statement") + + val parameter1 = psiFactory.createParameter("a", PsiType.INT).toUElementOfType() ?: fail("cannot create parameter") + + val parameter2 = psiFactory.createParameter("b", PsiType.INT).toUElementOfType() ?: fail("cannot create parameter") + (parameter2.sourcePsi as PsiParameter).typeElement?.delete() + (parameter2.sourcePsi as PsiParameter).children[1].delete() + + val lambda = generatePlugin.createLambdaExpression(listOf(parameter1, parameter2), statement) ?: fail("cannot create lambda") + + TestCase.assertEquals("(a,b)->System.out.println()", lambda.sourcePsi?.text) + } + + fun `test lambda expression with simplified block body`() { + val block = psiFactory.createStatementFromText("{ return \"10\"; }", null).toUElementOfType() + ?: fail("cannot create block") + + val parameter1 = psiFactory.createParameter("a", PsiType.INT).toUElementOfType() ?: fail("cannot create parameter") + (parameter1.sourcePsi as PsiParameter).typeElement?.delete() + (parameter1.sourcePsi as PsiParameter).children[1].delete() + val lambda = generatePlugin.createLambdaExpression(listOf(parameter1), block) ?: fail("cannot create lambda") + TestCase.assertEquals("""a->"10"""", lambda.sourcePsi?.text) + } + + fun `test function argument replacement`() { + val expression = psiFactory.createExpressionFromText("f(a)", null).toUElementOfType() + ?: fail("cannot create expression") + val newArgument = psiFactory.createExpressionFromText("b", null).toUElementOfType() + ?: fail("cannot create reference") + + TestCase.assertNotNull(expression.valueArguments[0].replace(newArgument)) + val updated = expression.refreshed() ?: fail("cannot update expression") + TestCase.assertEquals("f", updated.methodName) + TestCase.assertTrue(updated.valueArguments[0] is USimpleNameReferenceExpression) + TestCase.assertEquals("b", (updated.valueArguments[0] as USimpleNameReferenceExpression).identifier) + } + + fun `test suggested name`() { + val expression = psiFactory.createExpressionFromText("f(a) + 1", null).toUElementOfType() + ?: fail("cannot create expression") + val variable = generatePlugin.createLocalVariable(null, PsiType.INT, expression, true) + ?: fail("cannot create variable") + + TestCase.assertEquals("final int i = f(a) + 1;", variable.sourcePsi?.text) + } +} \ No newline at end of file