Skip to content

Commit

Permalink
Add mcdev support for Translatable.allowArbitraryArgs, for the 1.20.3…
Browse files Browse the repository at this point in the history
… update
  • Loading branch information
Earthcomputer committed Dec 17, 2023
1 parent 2f32b1f commit 7ac55fb
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
<val name="foldMethod" val="true"/>
</annotation>
</item>
<item name="net.minecraft.network.chat.Component net.minecraft.network.chat.MutableComponent translatableEscape(java.lang.String, java.lang.Object...) 0">
<annotation name="com.demonwav.mcdev.annotations.Translatable">
<val name="foldMethod" val="true"/>
<val name="allowArbitraryArgs" val="true"/>
</annotation>
</item>
<item name="net.minecraft.network.chat.Component net.minecraft.network.chat.MutableComponent translatableWithFallback(java.lang.String, java.lang.String) 0">
<annotation name="com.demonwav.mcdev.annotations.Translatable"/>
</item>
Expand Down
6 changes: 6 additions & 0 deletions externalAnnotations/net/minecraft/text/annotations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
<val name="foldMethod" val="true"/>
</annotation>
</item>
<item name="net.minecraft.text.Text net.minecraft.text.MutableText stringifiedTranslatable(java.lang.String, java.lang.Object...) 0">
<annotation name="com.demonwav.mcdev.annotations.Translatable">
<val name="foldMethod" val="true"/>
<val name="allowArbitraryArgs" val="true"/>
</annotation>
</item>
<item name="net.minecraft.text.Text net.minecraft.text.MutableText translatableWithFallback(java.lang.String, java.lang.String) 0">
<annotation name="com.demonwav.mcdev.annotations.Translatable"/>
</item>
Expand Down
14 changes: 13 additions & 1 deletion src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.demonwav.mcdev.platform.mcp.mappings

import com.demonwav.mcdev.util.MemberReference
import com.google.common.collect.ImmutableBiMap

/**
Expand All @@ -30,9 +31,20 @@ object HardcodedYarnToMojmap {
ImmutableBiMap.ofEntries(
"net.minecraft.item.ItemStack" mapTo "net.minecraft.world.item.ItemStack",
"net.minecraft.util.Formatting" mapTo "net.minecraft.ChatFormatting",
"net.minecraft.text.Text" mapTo "net.minecraft.network.chat.Component",
),
ImmutableBiMap.ofEntries(),
ImmutableBiMap.ofEntries(),
ImmutableBiMap.ofEntries(
MemberReference(
owner = "net.minecraft.util.Text",
name = "stringifiedTranslatable",
descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/text/MutableText;"
) mapTo MemberReference(
owner = "net.minecraft.network.chat.Component",
name = "translatableEscape",
descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;"
)
),
hashMapOf(),
false,
)
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/translations/TranslationConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object TranslationConstants {
const val TRANSLATABLE_ANNOTATION = "com.demonwav.mcdev.annotations.Translatable"
const val REQUIRED = "required"
const val FOLD_METHOD = "foldMethod"
const val ALLOW_ARBITRARY_ARGS = "allowArbitraryArgs"
const val PREFIX = "prefix"
const val SUFFIX = "suffix"
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@

package com.demonwav.mcdev.translations.identification

import com.demonwav.mcdev.platform.mcp.mappings.getMappedClass
import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethod
import com.demonwav.mcdev.translations.TranslationConstants
import com.demonwav.mcdev.translations.identification.TranslationInstance.Companion.FormattingError
import com.demonwav.mcdev.translations.index.TranslationIndex
import com.demonwav.mcdev.translations.index.merge
import com.demonwav.mcdev.util.constantStringValue
import com.demonwav.mcdev.util.constantValue
import com.demonwav.mcdev.util.descriptor
import com.demonwav.mcdev.util.extractVarArgs
import com.demonwav.mcdev.util.findModule
import com.demonwav.mcdev.util.referencedMethod
import com.intellij.codeInsight.AnnotationUtil
import com.intellij.codeInspection.dataFlow.CommonDataflow
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.PsiCallExpression
import com.intellij.psi.PsiElement
Expand Down Expand Up @@ -79,6 +84,12 @@ abstract class TranslationIdentifier<T : PsiElement> {
val required =
translatableAnnotation.findAttributeValue(TranslationConstants.REQUIRED)?.constantValue as? Boolean
?: true
val isPreEscapeException =
method.containingClass?.qualifiedName?.startsWith("net.minecraft.") == true &&
isPreEscapeMcVersion(project, element)
val allowArbitraryArgs = isPreEscapeException || translatableAnnotation.findAttributeValue(
TranslationConstants.ALLOW_ARBITRARY_ARGS
)?.constantValue as? Boolean ?: false

val translationKey = CommonDataflow.computeValue(element) as? String ?: return null

Expand All @@ -90,7 +101,8 @@ abstract class TranslationIdentifier<T : PsiElement> {
referenceElement,
TranslationInstance.Key(prefix, translationKey, suffix),
null,
required
required,
allowArbitraryArgs,
)

val foldMethod =
Expand Down Expand Up @@ -126,6 +138,7 @@ abstract class TranslationIdentifier<T : PsiElement> {
TranslationInstance.Key(prefix, translationKey, suffix),
formatted,
required,
allowArbitraryArgs,
if (superfluousParams >= 0) FormattingError.SUPERFLUOUS else null,
superfluousParams,
)
Expand All @@ -137,6 +150,7 @@ abstract class TranslationIdentifier<T : PsiElement> {
TranslationInstance.Key(prefix, translationKey, suffix),
translation,
required,
allowArbitraryArgs,
FormattingError.MISSING,
)
}
Expand All @@ -147,6 +161,7 @@ abstract class TranslationIdentifier<T : PsiElement> {
val paramCount = STRING_FORMATTING_PATTERN.findAll(format).count()

val varargs = call.extractVarArgs(method.parameterList.parametersCount - 1, true, true)
?: return null
val varargStart = if (varargs.size > paramCount) {
method.parameterList.parametersCount - 1 + paramCount
} else {
Expand All @@ -162,6 +177,21 @@ abstract class TranslationIdentifier<T : PsiElement> {
}
}

private fun isPreEscapeMcVersion(project: Project, contextElement: PsiElement): Boolean {
val module = contextElement.findModule() ?: return false
val componentClassName = module.getMappedClass("net.minecraft.network.chat.Component")
val componentClass = JavaPsiFacade.getInstance(project)
.findClass(componentClassName, contextElement.resolveScope) ?: return false
val translatableEscapeName = module.getMappedMethod(
"net.minecraft.network.chat.Component",
"translatableEscape",
"(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/Component;"
)
return componentClass.findMethodsByName(translatableEscapeName, false).any { method ->
method.descriptor?.startsWith("(Ljava/lang/String;[Ljava/lang/Object;)") == true
}
}

private val NUMBER_FORMATTING_PATTERN = Regex("%(\\d+\\$)?[\\d.]*[df]")
private val STRING_FORMATTING_PATTERN = Regex("[^%]?%(?:\\d+\\$)?s")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ data class TranslationInstance(
val key: Key,
val text: String?,
val required: Boolean,
val allowArbitraryArgs: Boolean,
val formattingError: FormattingError? = null,
val superfluousVarargStart: Int = -1,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.translations.inspections

import com.demonwav.mcdev.platform.mcp.mappings.getMappedClass
import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethod
import com.demonwav.mcdev.translations.identification.TranslationInstance
import com.demonwav.mcdev.util.findModule
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
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.siyeh.ig.psiutils.CommentTracker
import com.siyeh.ig.psiutils.MethodCallUtils

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)
}

override fun visitLiteralExpression(expression: PsiLiteralExpression) {
doCheck(expression)
}

private fun doCheck(element: PsiElement) {
val result = TranslationInstance.find(element)
if (result == null || result.foldingElement !is PsiCall || result.allowArbitraryArgs) {
return
}

val args = result.foldingElement.argumentList ?: return

if (!MethodCallUtils.isVarArgCall(result.foldingElement)) {
return
}

val resolvedMethod = result.foldingElement.resolveMethod() ?: return
if ((resolvedMethod.parameterList.parameters.lastOrNull()?.type as? PsiEllipsisType)
?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) != true
) {
return
}
val module = element.findModule() ?: return
val componentName = module.getMappedClass("net.minecraft.network.chat.Component")
val translatableName = module.getMappedMethod(
"net.minecraft.network.chat.Component",
"translatable",
"(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;"
)
val isComponentTranslatable = resolvedMethod.name == translatableName &&
resolvedMethod.containingClass?.qualifiedName == componentName

val booleanType =
PsiType.getTypeByName(CommonClassNames.JAVA_LANG_BOOLEAN, holder.project, element.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
if (!booleanType.isAssignableFrom(type) &&
!numberType.isAssignableFrom(type) &&
!stringType.isAssignableFrom(type) &&
!componentType.isAssignableFrom(type)
) {
var fixes = arrayOf<LocalQuickFix>(WrapWithStringValueOfFix(arg))
if (isComponentTranslatable && result.foldingElement is PsiMethodCallExpression) {
val referenceName = result.foldingElement.methodExpression.referenceNameElement
if (referenceName != null) {
fixes = arrayOf<LocalQuickFix>(ReplaceWithTranslatableEscapedFix(referenceName)) + fixes
}
}
holder.registerProblem(
arg,
"Translation argument is not a 'String', 'Number', 'Boolean' or 'Component'",
*fixes
)
}
}
}
}

private class ReplaceWithTranslatableEscapedFix(
referenceName: PsiElement
) : LocalQuickFixOnPsiElement(referenceName) {
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
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))
}
}

private class WrapWithStringValueOfFix(element: PsiElement) : LocalQuickFixOnPsiElement(element) {
override fun getFamilyName() = "Wrap with 'String.valueOf()'"
override fun getText() = "Wrap with 'String.valueOf()'"

override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
val ct = CommentTracker()
ct.replace(startElement, "String.valueOf(${ct.text(startElement)})")
}
}
}
17 changes: 5 additions & 12 deletions src/main/kotlin/util/call-utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiNewExpression
import com.intellij.psi.PsiSubstitutor
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypeCastExpression

val PsiCall.referencedMethod: PsiMethod?
get() = when (this) {
Expand All @@ -36,7 +35,7 @@ val PsiCall.referencedMethod: PsiMethod?
else -> null
}

fun PsiCall.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array<String?> {
fun PsiCall.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array<String?>? {
val method = this.referencedMethod
val args = this.argumentList?.expressions ?: return emptyArray()
if (method == null || args.size < (index + 1)) {
Expand All @@ -56,24 +55,18 @@ private fun extractVarArgs(
elements: List<PsiExpression>,
allowReferences: Boolean,
allowTranslations: Boolean,
): Array<String?> {
tailrec fun resolveReference(expression: PsiExpression): Array<String?> {
if (expression is PsiTypeCastExpression && expression.operand != null) {
return resolveReference(expression.operand!!)
}
return arrayOf(expression.evaluate(allowTranslations, allowReferences))
}

): Array<String?>? {
return if (elements[0].type == type) {
// We're dealing with an array initializer, let's analyse it!
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 {
resolveReference(initializer)
// 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()
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,13 @@
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.translations.inspections.SuperfluousFormatInspection"/>
<localInspection displayName="Wrong translation argument types"
groupName="Minecraft"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.translations.inspections.WrongTypeInTranslationArgsInspection"/>
<localInspection displayName="Entity class does not match this entity class"
groupName="Minecraft"
language="JAVA"
Expand Down

0 comments on commit 7ac55fb

Please sign in to comment.