User: rednesto Date: 10 Jul 24 11:34 Revision: 3a2175f12ed5a6843df98363bce0c011e3e1b1c0 Summary: Translation: initial conversion to UAST TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=9393&personal=false Index: src/main/kotlin/translations/folding.kt =================================================================== --- src/main/kotlin/translations/folding.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/folding.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -34,8 +34,11 @@ import com.intellij.openapi.editor.FoldingGroup import com.intellij.openapi.options.BeanConfigurable import com.intellij.psi.PsiElement -import com.intellij.psi.PsiExpressionList -import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.textRange +import org.jetbrains.uast.toUElement +import org.jetbrains.uast.visitor.AbstractUastVisitor class TranslationCodeFoldingOptionsProvider : BeanConfigurable(TranslationFoldingSettings.instance), CodeFoldingOptionsProvider { @@ -88,23 +91,35 @@ val descriptors = mutableListOf() for (identifier in TranslationIdentifier.INSTANCES) { - val elements = PsiTreeUtil.findChildrenOfType(root, identifier.elementClass()) - for (element in elements) { + val uElement = root.toUElement() ?: continue + val children = mutableListOf() + uElement.accept(object : AbstractUastVisitor() { + override fun visitElement(node: UElement): Boolean { + if (identifier.elementClass().isAssignableFrom(node.javaClass)) { + children.add(node) + } + + return super.visitElement(node) + } + }) + for (element in children) { val translation = identifier.identifyUnsafe(element) val foldingElement = translation?.foldingElement ?: continue val range = - if (foldingElement is PsiExpressionList) { - val args = foldingElement.expressions.drop(translation.foldStart) - args.first().textRange.union(args.last().textRange) + if (foldingElement is UCallExpression && translation.foldStart != 0) { + val args = foldingElement.valueArguments.drop(translation.foldStart) + val startRange = args.first().textRange ?: continue + val endRange = args.last().textRange ?: continue + startRange.union(endRange) } else { - foldingElement.textRange + foldingElement.textRange ?: continue } if (!translation.required && translation.formattingError != null) { continue } descriptors.add( FoldingDescriptor( - translation.foldingElement.node, + translation.foldingElement.sourcePsi?.node!!, range, FoldingGroup.newGroup("mc.translation." + translation.key), if (translation.formattingError == TranslationInstance.Companion.FormattingError.MISSING) { Index: src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt =================================================================== --- src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -21,13 +21,15 @@ package com.demonwav.mcdev.translations.identification import com.intellij.codeInsight.completion.CompletionUtilCore -import com.intellij.psi.PsiLiteralExpression +import org.jetbrains.uast.ULiteralExpression -class LiteralTranslationIdentifier : TranslationIdentifier() { - override fun identify(element: PsiLiteralExpression): TranslationInstance? { - val statement = element.parent - if (element.value is String) { - val result = identify(element.project, element, statement, element) +class LiteralTranslationIdentifier : TranslationIdentifier() { + override fun identify(element: ULiteralExpression): TranslationInstance? { + val statement = element.uastParent + if (statement != null && element.value is String) { + val project = element.sourcePsi?.project + ?: return null + val result = identify(project, element, statement, element) return result?.copy( key = result.key.copy( infix = result.key.infix.replace( @@ -40,5 +42,5 @@ return null } - override fun elementClass(): Class = PsiLiteralExpression::class.java + override fun elementClass(): Class = ULiteralExpression::class.java } Index: src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt =================================================================== --- src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -22,29 +22,29 @@ import com.intellij.codeInsight.completion.CompletionUtilCore import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiField -import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiModifier -import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.impl.source.PsiClassReferenceType import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.uast.UField +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.resolveToUElement -class ReferenceTranslationIdentifier : TranslationIdentifier() { - override fun identify(element: PsiReferenceExpression): TranslationInstance? { - val reference = element.resolve() - val statement = element.parent +class ReferenceTranslationIdentifier : TranslationIdentifier() { + override fun identify(element: UReferenceExpression): TranslationInstance? { + val reference = element.resolveToUElement() ?: return null + val statement = element.uastParent ?: return null + val project = element.sourcePsi?.project ?: return null - if (reference is PsiField) { - val scope = GlobalSearchScope.allScope(element.project) + if (reference is UField) { + val scope = GlobalSearchScope.allScope(project) val stringClass = - JavaPsiFacade.getInstance(element.project).findClass("java.lang.String", scope) ?: return null - val isConstant = - reference.hasModifierProperty(PsiModifier.STATIC) && reference.hasModifierProperty(PsiModifier.FINAL) + JavaPsiFacade.getInstance(project).findClass("java.lang.String", scope) ?: return null + val isConstant = reference.isStatic && reference.isFinal val type = reference.type as? PsiClassReferenceType ?: return null val resolved = type.resolve() ?: return null if (isConstant && (resolved.isEquivalentTo(stringClass) || resolved.isInheritor(stringClass, true))) { - val referenceElement = reference.initializer as? PsiLiteral ?: return null - val result = identify(element.project, element, statement, referenceElement) + val referenceElement = reference.uastInitializer as? ULiteralExpression ?: return null + val result = identify(project, element, statement, referenceElement) return result?.copy( key = result.key.copy( @@ -60,7 +60,5 @@ return null } - override fun elementClass(): Class { - return PsiReferenceExpression::class.java + override fun elementClass(): Class = UReferenceExpression::class.java - } +} -} Index: src/main/kotlin/translations/identification/TranslationIdentifier.kt =================================================================== --- src/main/kotlin/translations/identification/TranslationIdentifier.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/identification/TranslationIdentifier.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -37,18 +37,22 @@ import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiCall import com.intellij.psi.PsiElement import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiExpressionList -import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter import java.util.IllegalFormatException import java.util.MissingFormatArgumentException +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.getContainingUClass -abstract class TranslationIdentifier { +abstract class TranslationIdentifier { @Suppress("UNCHECKED_CAST") - fun identifyUnsafe(element: PsiElement): TranslationInstance? { + fun identifyUnsafe(element: UElement): TranslationInstance? { return identify(element as T) } @@ -61,20 +65,19 @@ fun identify( project: Project, - element: PsiExpression, - container: PsiElement, - referenceElement: PsiElement, + element: UExpression, + container: UElement, + referenceElement: UElement, ): TranslationInstance? { - if (container !is PsiExpressionList) { - return null - } - val call = container.parent as? PsiCall ?: return null - val index = container.expressions.indexOf(element) + val call = container as? UCallExpression ?: return null + val index = container.valueArguments.indexOf(element) val method = call.referencedMethod ?: return null - val parameter = method.parameterList.getParameter(index) ?: return null - val translatableAnnotation = - AnnotationUtil.findAnnotation(parameter, TranslationConstants.TRANSLATABLE_ANNOTATION) ?: return null + val parameter = method.uastParameters.getOrNull(index) ?: return null + val translatableAnnotation = AnnotationUtil.findAnnotation( + parameter.javaPsi as PsiParameter, + TranslationConstants.TRANSLATABLE_ANNOTATION + ) ?: return null val prefix = translatableAnnotation.findAttributeValue(TranslationConstants.PREFIX)?.constantStringValue ?: "" @@ -84,13 +87,16 @@ translatableAnnotation.findAttributeValue(TranslationConstants.REQUIRED)?.constantValue as? Boolean ?: true val isPreEscapeException = - method.containingClass?.qualifiedName?.startsWith("net.minecraft.") == true && - isPreEscapeMcVersion(project, element) + method.getContainingUClass()?.qualifiedName?.startsWith("net.minecraft.") == true && + isPreEscapeMcVersion(project, element.sourcePsi!!) val allowArbitraryArgs = isPreEscapeException || translatableAnnotation.findAttributeValue( TranslationConstants.ALLOW_ARBITRARY_ARGS )?.constantValue as? Boolean ?: false - val translationKey = CommonDataflow.computeValue(element) as? String ?: return null + val translationKey = when (val javaPsi = element.javaPsi) { + is PsiExpression -> CommonDataflow.computeValue(javaPsi) as? String + else -> element.evaluateString() + } ?: return null val entries = TranslationIndex.getAllDefaultEntries(project).merge("") val translation = entries[prefix + translationKey + suffix]?.text @@ -109,15 +115,15 @@ ?: false val formatting = - (method.parameterList.parameters.last().type as? PsiEllipsisType) + (method.uastParameters.last().type as? PsiEllipsisType) ?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) == true val foldingElement = if (foldMethod) { call } else if ( index == 0 && - container.expressionCount > 1 && - method.parameterList.parametersCount == 2 && + container.valueArgumentCount > 1 && + method.uastParameters.size == 2 && formatting ) { container @@ -155,14 +161,15 @@ } } - private fun format(method: PsiMethod, translation: String, call: PsiCall): Pair? { + private fun format(method: UMethod, translation: String, call: UCallExpression): Pair? { val format = NUMBER_FORMATTING_PATTERN.replace(translation, "%$1s") val paramCount = STRING_FORMATTING_PATTERN.findAll(format).count() - val varargs = call.extractVarArgs(method.parameterList.parametersCount - 1, true, true) + val parametersCount = method.uastParameters.size + val varargs = call.extractVarArgs(parametersCount - 1, true, true) ?: return null val varargStart = if (varargs.size > paramCount) { - method.parameterList.parametersCount - 1 + paramCount + parametersCount - 1 + paramCount } else { -1 } Index: src/main/kotlin/translations/identification/TranslationInstance.kt =================================================================== --- src/main/kotlin/translations/identification/TranslationInstance.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/identification/TranslationInstance.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -20,12 +20,12 @@ package com.demonwav.mcdev.translations.identification -import com.intellij.psi.PsiElement +import org.jetbrains.uast.UElement data class TranslationInstance( - val foldingElement: PsiElement?, + val foldingElement: UElement?, val foldStart: Int, - val referenceElement: PsiElement?, + val referenceElement: UElement?, val key: Key, val text: String?, val required: Boolean, @@ -44,7 +44,7 @@ MISSING, SUPERFLUOUS } - fun find(element: PsiElement): TranslationInstance? = + fun find(element: UElement): TranslationInstance? = TranslationIdentifier.INSTANCES .firstOrNull { it.elementClass().isAssignableFrom(element.javaClass) } ?.identifyUnsafe(element) Index: src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt =================================================================== --- src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -29,17 +29,19 @@ import com.intellij.ide.util.gotoByName.ChooseByNamePopupComponent import com.intellij.openapi.application.ModalityState import com.intellij.openapi.project.Project -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiNamedElement import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.generate.getUastElementFactory +import org.jetbrains.uast.generate.replace +import org.jetbrains.uast.toUElementOfType class ChangeTranslationQuickFix(private val name: String) : LocalQuickFix { override fun getName() = name override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { - val literal = descriptor.psiElement as PsiLiteralExpression + val literal = descriptor.psiElement.toUElementOfType() ?: return val key = LiteralTranslationIdentifier().identify(literal)?.key ?: return val popup = ChooseByNamePopup.createPopup( project, @@ -50,17 +52,15 @@ object : ChooseByNamePopupComponent.Callback() { override fun elementChosen(element: Any) { val selectedKey = (element as PsiNamedElement).name ?: return - literal.containingFile.runWriteAction { - val insertion = selectedKey.substring( - key.prefix.length, - selectedKey.length - key.suffix.length, - ) + val insertion = selectedKey.substring( + key.prefix.length, + selectedKey.length - key.suffix.length, + ) - literal.replace( - JavaPsiFacade.getInstance(project).elementFactory.createExpressionFromText( - "\"$insertion\"", - literal.context, - ), - ) + val elementFactory = literal.getUastElementFactory(project) ?: return + val replacement = elementFactory.createStringLiteralExpression(insertion, element) + ?: return + descriptor.psiElement.containingFile.runWriteAction { + literal.replace(replacement) } } }, Index: src/main/kotlin/translations/inspections/MissingFormatInspection.kt =================================================================== --- src/main/kotlin/translations/inspections/MissingFormatInspection.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/inspections/MissingFormatInspection.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -24,31 +24,35 @@ import com.demonwav.mcdev.translations.identification.TranslationInstance.Companion.FormattingError import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemsHolder -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiReferenceExpression +import com.intellij.uast.UastHintedVisitorAdapter +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class MissingFormatInspection : TranslationInspection() { override fun getStaticDescription() = "Detects missing format arguments for translations" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), arrayOf(UExpression::class.java)) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - visit(expression) + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitExpression(node: UExpression): Boolean { + visit(node) + return super.visitElement(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - visit(expression, ChangeTranslationQuickFix("Use a different translation")) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + visit(node, ChangeTranslationQuickFix("Use a different translation")) + return true } - private fun visit(expression: PsiExpression, vararg quickFixes: LocalQuickFix) { + private fun visit(expression: UExpression, vararg quickFixes: LocalQuickFix) { val result = TranslationInstance.find(expression) if (result != null && result.required && result.formattingError == FormattingError.MISSING) { holder.registerProblem( - expression, + expression.sourcePsi!!, "There are missing formatting arguments to satisfy '${result.text}'", *quickFixes, ) Index: src/main/kotlin/translations/inspections/NoTranslationInspection.kt =================================================================== --- src/main/kotlin/translations/inspections/NoTranslationInspection.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/inspections/NoTranslationInspection.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -29,29 +29,35 @@ import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiLiteralExpression +import com.intellij.uast.UastHintedVisitorAdapter import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.toUElementOfType +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class NoTranslationInspection : TranslationInspection() { override fun getStaticDescription() = "Checks whether a translation key used in calls to StatCollector.translateToLocal(), " + "StatCollector.translateToLocalFormatted() or I18n.format() exists." - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), arrayOf(ULiteralExpression::class.java)) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - val result = LiteralTranslationIdentifier().identify(expression) + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + val result = LiteralTranslationIdentifier().identify(node) if (result != null && result.required && result.text == null) { holder.registerProblem( - expression, + node.sourcePsi!!, "The given translation key does not exist", CreateTranslationQuickFix, ChangeTranslationQuickFix("Use existing translation"), ) } + + return true } } @@ -60,7 +66,7 @@ override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { - val literal = descriptor.psiElement as PsiLiteralExpression + val literal = descriptor.psiElement.toUElementOfType() ?: return val translation = LiteralTranslationIdentifier().identify(literal) val literalValue = literal.value as String val key = translation?.key?.copy(infix = literalValue)?.full ?: literalValue @@ -71,7 +77,7 @@ Messages.getQuestionIcon(), ) if (result != null) { - TranslationFiles.add(literal, key, result) + TranslationFiles.add(literal.sourcePsi!!, key, result) } } catch (ignored: IncorrectOperationException) { } catch (e: Exception) { Index: src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt =================================================================== --- src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -27,58 +27,68 @@ import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project -import com.intellij.psi.JavaElementVisitor -import com.intellij.psi.PsiCall import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.SmartPointerManager -import com.intellij.psi.SmartPsiElementPointer +import com.intellij.uast.UastHintedVisitorAdapter +import com.intellij.uast.UastSmartPointer +import com.intellij.uast.createUastSmartPointer import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class SuperfluousFormatInspection : TranslationInspection() { override fun getStaticDescription() = "Detect superfluous format arguments for translations" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + private val typesHint: Array> = + arrayOf(UReferenceExpression::class.java, ULiteralExpression::class.java) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - val result = TranslationInstance.find(expression) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitExpression(node: UExpression): Boolean { + val result = TranslationInstance.find(node) if ( - result != null && result.foldingElement is PsiCall && + result != null && result.foldingElement is UCallExpression && result.formattingError == FormattingError.SUPERFLUOUS ) { - registerProblem(expression, result) + registerProblem(node, result) } + + return super.visitExpression(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - val result = TranslationInstance.find(expression) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + val result = TranslationInstance.find(node) if ( - result != null && result.required && result.foldingElement is PsiCall && + result != null && result.required && result.foldingElement is UCallExpression && result.formattingError == FormattingError.SUPERFLUOUS ) { registerProblem( - expression, + node, result, RemoveArgumentsQuickFix( - SmartPointerManager.getInstance(holder.project) - .createSmartPsiElementPointer(result.foldingElement), + result.foldingElement.createUastSmartPointer(), result.superfluousVarargStart, ), ChangeTranslationQuickFix("Use a different translation"), ) } + + return super.visitLiteralExpression(node) } private fun registerProblem( - expression: PsiExpression, + expression: UExpression, result: TranslationInstance, vararg quickFixes: LocalQuickFix, ) { holder.registerProblem( - expression, + expression.sourcePsi!!, "There are missing formatting arguments to satisfy '${result.text}'", *quickFixes, ) @@ -86,7 +96,7 @@ } private class RemoveArgumentsQuickFix( - private val call: SmartPsiElementPointer, + private val call: UastSmartPointer, private val position: Int, ) : LocalQuickFix { override fun getName() = "Remove superfluous arguments" @@ -94,7 +104,7 @@ override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { descriptor.psiElement.containingFile.runWriteAction { - call.element?.argumentList?.expressions?.drop(position)?.forEach { it.delete() } + call.element?.valueArguments?.drop(position)?.forEach { it.sourcePsi?.delete() } } } catch (ignored: IncorrectOperationException) { } Index: src/main/kotlin/translations/inspections/TranslationInspection.kt =================================================================== --- src/main/kotlin/translations/inspections/TranslationInspection.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/inspections/TranslationInspection.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -21,14 +21,14 @@ package com.demonwav.mcdev.translations.inspections import com.demonwav.mcdev.platform.mcp.McpModuleType -import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFile -abstract class TranslationInspection : AbstractBaseJavaLocalInspectionTool() { +abstract class TranslationInspection : LocalInspectionTool() { protected abstract fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor final override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { Index: src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt =================================================================== --- src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -24,59 +24,84 @@ import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethod import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.intention.FileModifier import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiCall import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiFile -import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiManager -import com.intellij.psi.PsiMethodCallExpression -import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiType +import com.intellij.uast.UastHintedVisitorAdapter +import com.intellij.uast.createUastSmartPointer import com.siyeh.ig.psiutils.CommentTracker import com.siyeh.ig.psiutils.MethodCallUtils +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UIdentifier +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.generate.replace +import org.jetbrains.uast.getContainingUClass +import org.jetbrains.uast.resolveToUElement +import org.jetbrains.uast.util.isMethodCall +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class WrongTypeInTranslationArgsInspection : TranslationInspection() { override fun getStaticDescription() = "Detect wrong argument types in translation arguments" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - doCheck(expression) + private val typesHint: Array> = + arrayOf(UReferenceExpression::class.java, ULiteralExpression::class.java) + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitElement(node: UElement): Boolean { + if (node is UReferenceExpression) { + doCheck(node) - } + } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - doCheck(expression) + return super.visitElement(node) } - private fun doCheck(element: PsiElement) { + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + doCheck(node) + return super.visitLiteralExpression(node) + } + + private fun doCheck(element: UElement) { val result = TranslationInstance.find(element) - if (result == null || result.foldingElement !is PsiCall || result.allowArbitraryArgs) { + if (result == null || result.foldingElement !is UCallExpression || result.allowArbitraryArgs) { return } - val args = result.foldingElement.argumentList ?: return + val args = result.foldingElement.valueArguments - if (!MethodCallUtils.isVarArgCall(result.foldingElement)) { + val javaCall = result.foldingElement.javaPsi as? PsiCall ?: return + if (!MethodCallUtils.isVarArgCall(javaCall)) { return } - val resolvedMethod = result.foldingElement.resolveMethod() ?: return - if ((resolvedMethod.parameterList.parameters.lastOrNull()?.type as? PsiEllipsisType) + val resolvedMethod = result.foldingElement.resolveToUElement() as? UMethod ?: return + val parameters = resolvedMethod.uastParameters + if ((parameters.lastOrNull()?.type as? PsiEllipsisType) ?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) != true ) { return } - val module = element.findModule() ?: return + val elementSourcePsi = element.sourcePsi ?: return + val module = elementSourcePsi.findModule() ?: return val componentName = module.getMappedClass("net.minecraft.network.chat.Component") val translatableName = module.getMappedMethod( "net.minecraft.network.chat.Component", @@ -84,30 +109,31 @@ "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" ) val isComponentTranslatable = resolvedMethod.name == translatableName && - resolvedMethod.containingClass?.qualifiedName == componentName + resolvedMethod.getContainingUClass()?.qualifiedName == componentName + val resolveScope = elementSourcePsi.resolveScope val booleanType = - PsiType.getTypeByName(CommonClassNames.JAVA_LANG_BOOLEAN, holder.project, element.resolveScope) + PsiType.getTypeByName(CommonClassNames.JAVA_LANG_BOOLEAN, holder.project, resolveScope) val numberType = - PsiType.getTypeByName(CommonClassNames.JAVA_LANG_NUMBER, holder.project, element.resolveScope) - val stringType = PsiType.getJavaLangString(PsiManager.getInstance(holder.project), element.resolveScope) - val componentType = PsiType.getTypeByName(componentName, holder.project, element.resolveScope) - for (arg in args.expressions.drop(resolvedMethod.parameterList.parametersCount - 1)) { - val type = arg.type ?: continue + PsiType.getTypeByName(CommonClassNames.JAVA_LANG_NUMBER, holder.project, resolveScope) + val stringType = PsiType.getJavaLangString(PsiManager.getInstance(holder.project), resolveScope) + val componentType = PsiType.getTypeByName(componentName, holder.project, resolveScope) + for (arg in args.drop(parameters.size - 1)) { + val type = arg.getExpressionType() ?: continue if (!booleanType.isAssignableFrom(type) && !numberType.isAssignableFrom(type) && !stringType.isAssignableFrom(type) && !componentType.isAssignableFrom(type) ) { - var fixes = arrayOf(WrapWithStringValueOfFix(arg)) - if (isComponentTranslatable && result.foldingElement is PsiMethodCallExpression) { - val referenceName = result.foldingElement.methodExpression.referenceNameElement + var fixes = arrayOf(WrapWithStringValueOfFix(arg.sourcePsi!!)) + if (isComponentTranslatable && result.foldingElement.isMethodCall()) { + val referenceName = result.foldingElement.methodIdentifier if (referenceName != null) { fixes = arrayOf(ReplaceWithTranslatableEscapedFix(referenceName)) + fixes } } holder.registerProblem( - arg, + arg.sourcePsi!!, "Translation argument is not a 'String', 'Number', 'Boolean' or 'Component'", *fixes ) @@ -117,19 +143,24 @@ } private class ReplaceWithTranslatableEscapedFix( - referenceName: PsiElement - ) : LocalQuickFixOnPsiElement(referenceName) { + identifier: UIdentifier + ) : LocalQuickFix { + + @FileModifier.SafeFieldForPreview + private val identifierPointer = identifier.createUastSmartPointer() + override fun getFamilyName() = "Replace with 'Component.translatableEscaped'" - override fun getText() = "Replace with 'Component.translatableEscaped'" - override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { - val module = startElement.findModule() ?: return + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val identifier = identifierPointer.element ?: return + val module = identifier.sourcePsi!!.findModule() ?: return val newMethodName = module.getMappedMethod( "net.minecraft.network.chat.Component", "translatableEscape", "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" ) - startElement.replace(JavaPsiFacade.getElementFactory(project).createIdentifier(newMethodName)) + val fakeSourcePsi = JavaPsiFacade.getElementFactory(project).createIdentifier(newMethodName) + identifier.replace(UIdentifier(fakeSourcePsi, identifier.uastParent)) } } Index: src/main/kotlin/translations/reference/contributors.kt =================================================================== --- src/main/kotlin/translations/reference/contributors.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/translations/reference/contributors.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -21,7 +21,6 @@ package com.demonwav.mcdev.translations.reference import com.demonwav.mcdev.translations.TranslationFiles -import com.demonwav.mcdev.translations.identification.TranslationIdentifier import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.lang.gen.psi.LangEntry import com.demonwav.mcdev.translations.lang.gen.psi.LangTypes @@ -34,36 +33,31 @@ import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.PsiReferenceRegistrar +import com.intellij.psi.registerUastReferenceProvider +import com.intellij.psi.uastReferenceProvider import com.intellij.util.ProcessingContext +import org.jetbrains.uast.UElement -class JavaReferenceContributor : PsiReferenceContributor() { +class UastReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { - for (identifier in TranslationIdentifier.INSTANCES) { - registrar.registerReferenceProvider( - PlatformPatterns.psiElement(identifier.elementClass()), - object : PsiReferenceProvider() { - override fun getReferencesByElement( - element: PsiElement, - context: ProcessingContext, - ): Array { - val result = identifier.identifyUnsafe(element) - if (result != null) { - val referenceElement = result.referenceElement ?: return emptyArray() - return arrayOf( + registrar.registerUastReferenceProvider( + { _, _ -> true }, + uastReferenceProvider { uExpr, psi -> + val translation = TranslationInstance.find(uExpr) + ?: return@uastReferenceProvider emptyArray() + val referenceElement = translation.referenceElement + ?: return@uastReferenceProvider emptyArray() + arrayOf( - TranslationReference( + TranslationReference( - referenceElement, - TextRange(1, referenceElement.textLength - 1), - result.key, + psi, + TextRange(1, referenceElement.asSourceString().length - 1), + translation.key, - ), - ) - } + ), + ) + } - return emptyArray() - } - }, - ) - } - } + ) + } +} -} class JsonReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { Index: src/main/kotlin/util/call-uast-utils.kt =================================================================== --- src/main/kotlin/util/call-uast-utils.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) +++ src/main/kotlin/util/call-uast-utils.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -0,0 +1,72 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.util + +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.toUElementOfType +import org.jetbrains.uast.util.isArrayInitializer + +val UCallExpression.referencedMethod: UMethod? + get() = this.resolve()?.toUElementOfType() + +fun UCallExpression.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array? { + val method = this.referencedMethod + val args = this.valueArguments + if (method == null || args.size < (index + 1)) { + return emptyArray() + } + + val psiParam = method.uastParameters[index].javaPsi as? PsiParameter + ?: return null + if (!psiParam.isVarArgs) { + return arrayOf(args[index].evaluate(allowTranslations, allowReferences)) + } + + val elements = args.drop(index) + return extractVarArgs(psiParam.type, elements, allowReferences, allowTranslations) +} + +private fun extractVarArgs( + type: PsiType, + elements: List, + allowReferences: Boolean, + allowTranslations: Boolean, +): Array? { + return if (elements[0].getExpressionType() == type) { + val initializer = elements[0] + if (initializer is UCallExpression && initializer.isArrayInitializer()) { + // We're dealing with an array initializer, let's analyse it! + initializer.valueArguments + .asSequence() + .map { it.evaluate(allowReferences, allowTranslations) } + .toTypedArray() + } else { + // We're dealing with a more complex expression that results in an array, give up + return null + } + } else { + elements.asSequence().map { it.evaluate(allowReferences, allowTranslations) }.toTypedArray() + } +} Index: src/main/kotlin/util/call-utils.kt =================================================================== --- src/main/kotlin/util/call-utils.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/util/call-utils.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -22,12 +22,9 @@ import com.intellij.psi.PsiCall import com.intellij.psi.PsiEnumConstant -import com.intellij.psi.PsiExpression import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiNewExpression -import com.intellij.psi.PsiSubstitutor -import com.intellij.psi.PsiType val PsiCall.referencedMethod: PsiMethod? get() = when (this) { @@ -36,41 +33,3 @@ is PsiEnumConstant -> this.resolveMethod() else -> null } - -fun PsiCall.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array? { - val method = this.referencedMethod - val args = this.argumentList?.expressions ?: return emptyArray() - if (method == null || args.size < (index + 1)) { - return emptyArray() - } - if (!method.parameterList.parameters[index].isVarArgs) { - return arrayOf(args[index].evaluate(allowTranslations, allowReferences)) - } - - val varargType = method.getSignature(PsiSubstitutor.EMPTY).parameterTypes[index] - val elements = args.drop(index) - return extractVarArgs(varargType, elements, allowReferences, allowTranslations) -} - -private fun extractVarArgs( - type: PsiType, - elements: List, - allowReferences: Boolean, - allowTranslations: Boolean, -): Array? { - return if (elements[0].type == type) { - val initializer = elements[0] - if (initializer is PsiNewExpression && initializer.arrayInitializer != null) { - // We're dealing with an array initializer, let's analyse it! - initializer.arrayInitializer!!.initializers - .asSequence() - .map { it.evaluate(allowReferences, allowTranslations) } - .toTypedArray() - } else { - // We're dealing with a more complex expression that results in an array, give up - return null - } - } else { - elements.asSequence().map { it.evaluate(allowReferences, allowTranslations) }.toTypedArray() - } -} Index: src/main/kotlin/util/expression-utils.kt =================================================================== --- src/main/kotlin/util/expression-utils.kt (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/kotlin/util/expression-utils.kt (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -22,34 +22,38 @@ import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.identification.TranslationInstance.Companion.FormattingError -import com.intellij.psi.PsiAnnotationMemberValue -import com.intellij.psi.PsiCall -import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.PsiTypeCastExpression -import com.intellij.psi.PsiVariable +import org.jetbrains.uast.UBinaryExpressionWithType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.UVariable +import org.jetbrains.uast.util.isTypeCast -fun PsiAnnotationMemberValue.evaluate(allowReferences: Boolean, allowTranslations: Boolean): String? { - val visited = mutableSetOf() +fun UExpression.evaluate(allowReferences: Boolean, allowTranslations: Boolean): String? { + val visited = mutableSetOf() - fun eval(expr: PsiAnnotationMemberValue?, defaultValue: String? = null): String? { + fun eval(expr: UExpression?, defaultValue: String? = null): String? { if (!visited.add(expr)) { return defaultValue } when { - expr is PsiTypeCastExpression && expr.operand != null -> + expr is UBinaryExpressionWithType && expr.isTypeCast() -> return eval(expr.operand, defaultValue) - expr is PsiReferenceExpression -> { - val reference = expr.advancedResolve(false).element - if (reference is PsiVariable && reference.initializer != null) { - return eval(reference.initializer, "\${${expr.text}}") + + expr is UReferenceExpression -> { + val reference = expr.resolve() + if (reference is UVariable && reference.uastInitializer != null) { + return eval(reference.uastInitializer, "\${${expr.asSourceString()}}") } } - expr is PsiLiteral -> + + expr is ULiteralExpression -> return expr.value.toString() - expr is PsiCall && allowTranslations -> - for (argument in expr.argumentList?.expressions ?: emptyArray()) { + + expr is UCallExpression && allowTranslations -> + for (argument in expr.valueArguments) { val translation = TranslationInstance.find(argument) ?: continue if (translation.formattingError == FormattingError.MISSING) { return "{ERROR: Missing formatting arguments for '${translation.text}'}" @@ -60,7 +64,7 @@ } return if (allowReferences && expr != null) { - "\${${expr.text}}" + "\${${expr.asSourceString()}}" } else { defaultValue } Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision fbbb2a2162bdf2a56241a81d8b2b317c11063232) +++ src/main/resources/META-INF/plugin.xml (revision 3a2175f12ed5a6843df98363bce0c011e3e1b1c0) @@ -249,11 +249,11 @@ - + - + @@ -517,28 +517,28 @@ implementationClass="com.demonwav.mcdev.inspection.IsCancelledInspection"/>