User: joe Date: 11 Aug 25 17:42 Revision: 9cf229cc9313e7d0428c801b92dae70cd5f5ff24 Summary: Add handling for translations in deprecated.json. Closes #2511 TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=10143&personal=false Index: src/main/kotlin/translations/DeprecatedTranslations.kt =================================================================== --- src/main/kotlin/translations/DeprecatedTranslations.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) +++ src/main/kotlin/translations/DeprecatedTranslations.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) @@ -0,0 +1,111 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 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.translations + +import com.google.gson.JsonParser +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.OrderEnumerator +import com.intellij.openapi.roots.ProjectRootModificationTracker +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import java.io.InputStreamReader +import java.io.IOException +import java.nio.charset.StandardCharsets + +class DeprecatedTranslations private constructor(val removed: Set, val renamed: Map) { + val inverseRenamed = renamed.entries.associateBy({ it.value }) { it.key } + + val isEmpty + get() = removed.isEmpty() && renamed.isEmpty() + + companion object { + private val DEFAULT = DeprecatedTranslations(emptySet(), emptyMap()) + private const val FILE_PATH = "assets/minecraft/lang/deprecated.json" + + fun getInstance(project: Project): DeprecatedTranslations { + return CachedValuesManager.getManager(project).getCachedValue(project) { + val file = findFile(project) ?: return@getCachedValue CachedValueProvider.Result( + DEFAULT, + ProjectRootModificationTracker.getInstance(project) + ) + CachedValueProvider.Result(getInstanceFromFile(file), file) + } + } + + private fun findFile(project: Project): VirtualFile? { + for (libraryRoot in OrderEnumerator.orderEntries(project).librariesOnly().classes().roots) { + val file = libraryRoot.findFileByRelativePath(FILE_PATH) ?: continue + if (!file.isDirectory) { + return file + } + } + + return null + } + + private fun getInstanceFromFile(file: VirtualFile): DeprecatedTranslations { + try { + val rootElement = InputStreamReader(file.inputStream, StandardCharsets.UTF_8).use { reader -> + JsonParser.parseReader(reader) + } + + if (rootElement == null || !rootElement.isJsonObject) { + return DEFAULT + } + + val obj = rootElement.asJsonObject + + val removedElt = obj.get("removed") + val removed = if (removedElt != null && removedElt.isJsonArray) { + val removedArr = removedElt.asJsonArray + removedArr.mapNotNullTo(HashSet.newHashSet(removedArr.size())) { e -> + if (e.isJsonPrimitive && e.asJsonPrimitive.isString) { + e.asString + } else { + null + } + } + } else { + emptySet() + } + + val renamedElt = obj.get("renamed") + val renamed = if (renamedElt != null && renamedElt.isJsonObject) { + val renamedObj = renamedElt.asJsonObject + renamedObj.asMap().asSequence().mapNotNull { (k, v) -> + if (v.isJsonPrimitive && v.asJsonPrimitive.isString) { + k to v.asString + } else { + null + } + }.toMap(HashMap.newHashMap(renamedObj.size())) + } else { + emptyMap() + } + + return DeprecatedTranslations(removed, renamed) + } catch (_: IOException) { + return DEFAULT + } + } + } +} Index: src/main/kotlin/translations/TranslationFiles.kt =================================================================== --- src/main/kotlin/translations/TranslationFiles.kt (revision 63357e498e49aa6e7fcbac7744b8eb35e0e727cf) +++ src/main/kotlin/translations/TranslationFiles.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) @@ -238,9 +238,6 @@ is FileEntry.Comment -> result.append("# ${entry.text}\n") is FileEntry.Translation -> result.append("${entry.key}=${entry.text}\n") FileEntry.EmptyLine -> result.append('\n') - // TODO: IntelliJ shows a false error here without the `else`. The compiler doesn't care because - // FileEntry is a sealed class. When this bug in IntelliJ is fixed, remove this `else`. - else -> {} } } @@ -282,9 +279,6 @@ result.append("\"${StringUtil.escapeStringCharacters(entry.text)}\",\n") } FileEntry.EmptyLine -> result.append('\n') - // TODO: IntelliJ shows a false error here without the `else`. The compiler doesn't care because - // FileEntry is a sealed class. When this bug in IntelliJ is fixed, remove this `else`. - else -> {} } } Index: src/main/kotlin/translations/identification/TranslationIdentifier.kt =================================================================== --- src/main/kotlin/translations/identification/TranslationIdentifier.kt (revision 63357e498e49aa6e7fcbac7744b8eb35e0e727cf) +++ src/main/kotlin/translations/identification/TranslationIdentifier.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) @@ -22,6 +22,7 @@ import com.demonwav.mcdev.platform.mcp.mappings.getMappedClass import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethod +import com.demonwav.mcdev.translations.DeprecatedTranslations import com.demonwav.mcdev.translations.TranslationConstants import com.demonwav.mcdev.translations.identification.TranslationInstance.FormattingError import com.demonwav.mcdev.translations.index.TranslationIndex @@ -89,10 +90,13 @@ else -> element.evaluateString() }?.replace(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, "") ?: return null - val shouldFold = element is ULiteralExpression && element.isString + val deprecations = DeprecatedTranslations.getInstance(project) + val key = prefix + translationKey + suffix + val shouldFold = element is ULiteralExpression && element.isString && key !in deprecations.removed && key !in deprecations.renamed + val entries = TranslationIndex.getAllDefaultEntries(project).merge("") - val translation = entries[prefix + translationKey + suffix]?.text + val translation = entries[deprecations.inverseRenamed[key] ?: key]?.text ?: return TranslationInstance( // translation doesn't exist null, index, Index: src/main/kotlin/translations/inspections/DeprecatedTranslationInspection.kt =================================================================== --- src/main/kotlin/translations/inspections/DeprecatedTranslationInspection.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) +++ src/main/kotlin/translations/inspections/DeprecatedTranslationInspection.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) @@ -0,0 +1,60 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 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.translations.inspections + +import com.demonwav.mcdev.translations.DeprecatedTranslations +import com.demonwav.mcdev.translations.identification.TranslationIdentifier +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElementVisitor +import com.intellij.uast.UastHintedVisitorAdapter +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor + +class DeprecatedTranslationInspection : TranslationInspection() { + override fun getStaticDescription() = "Detects usage of translations that are removed or renamed in deprecated.json" + + private val typesHint: Array> = arrayOf(UExpression::class.java) + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + val deprecations = DeprecatedTranslations.getInstance(holder.project) + + override fun visitExpression(node: UExpression): Boolean { + val result = TranslationIdentifier.identify(node) + if (result != null && result.text != null) { + val isRemoved = result.key.full in deprecations.removed + val isRenamed = result.key.full in deprecations.renamed + if (isRemoved || isRenamed) { + val type = if (isRemoved) "removed" else "renamed" + holder.registerProblem( + node.sourcePsi!!, + "Usage of $type translation in deprecated.json" + ) + } + } + + return super.visitExpression(node) + } + } +} Index: src/main/kotlin/translations/reference/TranslationGotoSymbolContributor.kt =================================================================== --- src/main/kotlin/translations/reference/TranslationGotoSymbolContributor.kt (revision 63357e498e49aa6e7fcbac7744b8eb35e0e727cf) +++ src/main/kotlin/translations/reference/TranslationGotoSymbolContributor.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) @@ -20,6 +20,7 @@ package com.demonwav.mcdev.translations.reference +import com.demonwav.mcdev.translations.DeprecatedTranslations import com.demonwav.mcdev.translations.index.TranslationIndex import com.demonwav.mcdev.translations.index.TranslationInverseIndex import com.demonwav.mcdev.translations.index.merge @@ -57,7 +58,8 @@ } else { GlobalSearchScope.projectScope(project) } - val elements = TranslationInverseIndex.findElements(name, scope) + val deprecations = DeprecatedTranslations.getInstance(project) + val elements = TranslationInverseIndex.findElements(deprecations.inverseRenamed[name] ?: name, scope) return elements.mapToArray { it as NavigationItem } } Index: src/main/kotlin/translations/reference/TranslationReference.kt =================================================================== --- src/main/kotlin/translations/reference/TranslationReference.kt (revision 63357e498e49aa6e7fcbac7744b8eb35e0e727cf) +++ src/main/kotlin/translations/reference/TranslationReference.kt (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) @@ -21,12 +21,14 @@ package com.demonwav.mcdev.translations.reference import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.translations.DeprecatedTranslations import com.demonwav.mcdev.translations.TranslationConstants import com.demonwav.mcdev.translations.TranslationFiles import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.index.TranslationIndex import com.demonwav.mcdev.translations.index.TranslationInverseIndex import com.demonwav.mcdev.translations.lang.gen.psi.LangEntry +import com.demonwav.mcdev.util.filterNotNull import com.demonwav.mcdev.util.mapToArray import com.demonwav.mcdev.util.toTypedArray import com.intellij.codeInsight.lookup.LookupElementBuilder @@ -52,8 +54,9 @@ ) : PsiReferenceBase.Poly(element, textRange, false), PsiPolyVariantReference { override fun multiResolve(incompleteCode: Boolean): Array { val project = myElement.project + val deprecations = DeprecatedTranslations.getInstance(project) val entries = TranslationInverseIndex.findElements( - key.full, + deprecations.inverseRenamed[key.full] ?: key.full, GlobalSearchScope.allScope(project), TranslationConstants.DEFAULT_LOCALE, ) @@ -63,16 +66,18 @@ override fun getVariants(): Array { val project = myElement.project val defaultTranslations = TranslationIndex.getAllDefaultTranslations(project) + val deprecations = DeprecatedTranslations.getInstance(project) val pattern = Regex("${Regex.escape(key.prefix)}(.*?)${Regex.escape(key.suffix)}") return defaultTranslations - .filter { it.key.isNotEmpty() } - .mapNotNull { entry -> pattern.matchEntire(entry.key)?.let { entry to it } } - .map { (entry, match) -> + .map { it.key } + .filter { it.isNotEmpty() && it !in deprecations.removed && it !in deprecations.renamed } + .mapNotNull { key -> pattern.matchEntire(key)?.let { key to it } } + .map { (key, match) -> LookupElementBuilder - .create(if (match.groups.size <= 1) entry.key else match.groupValues[1]) + .create(if (match.groups.size <= 1) key else match.groupValues[1]) .withIcon(PlatformAssets.MINECRAFT_ICON) .withTypeText(TranslationConstants.DEFAULT_LOCALE) - .withPresentableText(entry.key) + .withPresentableText(key) } .toTypedArray() } Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 63357e498e49aa6e7fcbac7744b8eb35e0e727cf) +++ src/main/resources/META-INF/plugin.xml (revision 9cf229cc9313e7d0428c801b92dae70cd5f5ff24) @@ -855,6 +855,13 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.translations.inspections.WrongTypeInTranslationArgsInspection"/> +