User: joe
Date: 28 May 25 20:21
Revision: 9c94ff3f8046988901d53dad93032951d35a48ca
Summary:
Add AnonymousAndLocalClassDesugarer
TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=10033&personal=false
Index: src/main/kotlin/platform/mixin/handlers/desugar/AnonymousAndLocalClassDesugarer.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/desugar/AnonymousAndLocalClassDesugarer.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
+++ src/main/kotlin/platform/mixin/handlers/desugar/AnonymousAndLocalClassDesugarer.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -0,0 +1,619 @@
+/*
+ * 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.platform.mixin.handlers.desugar
+
+import com.demonwav.mcdev.util.childrenOfType
+import com.demonwav.mcdev.util.findContainingClass
+import com.demonwav.mcdev.util.findContainingMethod
+import com.demonwav.mcdev.util.fullQualifiedName
+import com.intellij.codeInsight.ChangeContextUtil
+import com.intellij.codeInsight.daemon.impl.quickfix.AddDefaultConstructorFix
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Key
+import com.intellij.psi.CommonClassNames
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.JavaRecursiveElementWalkingVisitor
+import com.intellij.psi.JavaTokenType
+import com.intellij.psi.PsiAnonymousClass
+import com.intellij.psi.PsiCall
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiClassObjectAccessExpression
+import com.intellij.psi.PsiDiamondType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiElementFactory
+import com.intellij.psi.PsiExpressionList
+import com.intellij.psi.PsiExpressionStatement
+import com.intellij.psi.PsiField
+import com.intellij.psi.PsiIdentifier
+import com.intellij.psi.PsiJavaCodeReferenceElement
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiMethodCallExpression
+import com.intellij.psi.PsiMethodReferenceExpression
+import com.intellij.psi.PsiModifier
+import com.intellij.psi.PsiNewExpression
+import com.intellij.psi.PsiReferenceExpression
+import com.intellij.psi.PsiReferenceParameterList
+import com.intellij.psi.PsiTypeElement
+import com.intellij.psi.PsiTypeParameter
+import com.intellij.psi.PsiVariable
+import com.intellij.psi.codeStyle.VariableKind
+import com.intellij.psi.impl.PsiDiamondTypeUtil
+import com.intellij.psi.util.PsiTreeUtil
+import com.intellij.psi.util.PsiUtil
+import com.intellij.psi.util.parents
+import com.intellij.refactoring.anonymousToInner.AnonymousToInnerHandler
+import com.intellij.refactoring.util.RefactoringChangeUtil
+import com.intellij.util.CommonJavaRefactoringUtil
+import com.intellij.util.JavaPsiConstructorUtil
+import com.siyeh.ig.psiutils.VariableNameGenerator
+import org.objectweb.asm.Opcodes
+
+object AnonymousAndLocalClassDesugarer : Desugarer() {
+ private val VARIABLE_INFOS_KEY = Key.create>("mcdev.desugar.variableInfos")
+
+ override fun desugar(project: Project, file: PsiJavaFile, context: DesugarContext) {
+ for (clazz in file.classes) {
+ extractRecursive(project, context, clazz)
+ }
+ }
+
+ private fun extractRecursive(project: Project, context: DesugarContext, clazz: PsiClass) {
+ for (innerClass in DesugarUtil.allClassesShallow(clazz)) {
+ extractRecursive(project, context, innerClass)
+ }
+
+ if (PsiUtil.isLocalOrAnonymousClass(clazz)) {
+ extractLocalOrAnonymousClass(project, context, clazz)
+ }
+ }
+
+ private fun extractLocalOrAnonymousClass(project: Project, context: DesugarContext, localClass: PsiClass) {
+ val targetClass = AnonymousToInnerHandler.findTargetContainer(localClass) as? PsiClass ?: return
+ val newClassName = "$" + (localClass.fullQualifiedName?.substringAfterLast("$") ?: return)
+
+ val variableInfos = if (localClass.hasModifierProperty(PsiModifier.STATIC)) {
+ emptyArray()
+ } else {
+ collectUsedVariables(localClass)
+ }
+
+ val typeParametersToCreate = calculateTypeParametersToCreate(targetClass, localClass, variableInfos)
+ ChangeContextUtil.encodeContextInfo(localClass, false)
+ renameReferences(project, context, localClass, variableInfos)
+ updateLocalClassConstructors(project, localClass, variableInfos)
+ val newClass = targetClass.add(createClass(project, localClass, targetClass, variableInfos, typeParametersToCreate, newClassName)) as PsiClass
+ ChangeContextUtil.decodeContextInfo(newClass, targetClass, RefactoringChangeUtil.createThisExpression(targetClass.manager, targetClass))
+ newClass.putCopyableUserData(VARIABLE_INFOS_KEY, variableInfos)
+
+ if (localClass is PsiAnonymousClass) {
+ migrateAnonymousClassCreation(project, newClassName, localClass, variableInfos, typeParametersToCreate)
+ } else {
+ migrateLocalClassCreation(localClass, newClass)
+ }
+ }
+
+ private fun collectUsedVariables(localClass: PsiClass): Array {
+ val originalAnonymousParams = mutableListOf()
+ val variableInfoMap = linkedMapOf()
+
+ if (localClass is PsiAnonymousClass) {
+ val resolvedCtor = (localClass.parent as? PsiNewExpression)?.resolveConstructor()
+ if (resolvedCtor != null) {
+ for (param in resolvedCtor.parameterList.parameters) {
+ val paramName = VariableNameGenerator(localClass, VariableKind.PARAMETER)
+ .byName(param.name)
+ .skipNames(originalAnonymousParams.map { it.paramName })
+ .generate(false)
+ originalAnonymousParams += VariableInfo(param, paramName, null, passToSuper = true)
+ }
+
+ val parentVariableInfos = resolvedCtor.containingClass?.getCopyableUserData(VARIABLE_INFOS_KEY)
+ if (parentVariableInfos != null) {
+ repeat(parentVariableInfos.size.coerceAtMost(originalAnonymousParams.size)) {
+ originalAnonymousParams.removeLast()
+ }
+ for (parentInfo in parentVariableInfos) {
+ val paramName = VariableNameGenerator(localClass, VariableKind.PARAMETER)
+ .byName(parentInfo.variable.name)
+ .skipNames(originalAnonymousParams.map { it.paramName })
+ .generate(false)
+ variableInfoMap[parentInfo.variable] =
+ VariableInfo(parentInfo.variable, paramName, parentInfo.fieldName, passToSuper = true)
+ }
+ }
+ }
+ }
+
+ localClass.accept(object : JavaRecursiveElementWalkingVisitor() {
+ override fun visitReferenceExpression(expression: PsiReferenceExpression) {
+ if (expression.qualifierExpression == null) {
+ val resolved = expression.resolve()
+ if (resolved is PsiVariable && resolved !is PsiField) {
+ val containingClass = resolved.findContainingClass()
+ if (PsiTreeUtil.isAncestor(containingClass, localClass, true)) {
+ saveVariable(localClass, variableInfoMap, resolved, expression)
+ }
+ }
+ }
+ super.visitReferenceExpression(expression)
+ }
+ })
+
+ return (originalAnonymousParams + variableInfoMap.values).toTypedArray()
+ }
+
+ private fun saveVariable(
+ localClass: PsiClass,
+ variableInfoMap: MutableMap,
+ variable: PsiVariable,
+ usage: PsiReferenceExpression
+ ) {
+ if (usage.isInsideAnonymousClassParameter(localClass)) {
+ return
+ }
+
+ variableInfoMap.getOrPut(variable) {
+ val variableName = variable.name ?: return
+ VariableInfo(variable, variableName, "val$$variableName")
+ }
+ }
+
+ private fun calculateTypeParametersToCreate(
+ targetClass: PsiClass,
+ localClass: PsiClass,
+ variableInfos: Array
+ ): Collection {
+ val typeParameters = linkedSetOf()
+
+ val visitor = object : JavaRecursiveElementWalkingVisitor() {
+ override fun visitReferenceElement(reference: PsiJavaCodeReferenceElement) {
+ super.visitReferenceElement(reference)
+ val resolved = reference.resolve()
+ if (resolved is PsiTypeParameter) {
+ val owner = resolved.owner
+ if (owner != null && !PsiTreeUtil.isAncestor(localClass, owner, false) &&
+ !PsiTreeUtil.isAncestor(owner, targetClass, false)) {
+ typeParameters += resolved
+ }
+ }
+ }
+ }
+
+ localClass.accept(visitor)
+ for (info in variableInfos) {
+ info.variable.typeElement?.accept(visitor)
+ }
+
+ return typeParameters
+ }
+
+ private fun updateLocalClassConstructors(
+ project: Project,
+ localClass: PsiClass,
+ variableInfos: Array
+ ) {
+ if (localClass is PsiAnonymousClass || variableInfos.isEmpty()) {
+ return
+ }
+
+ var constructors = localClass.constructors
+ if (constructors.isEmpty()) {
+ constructors = arrayOf(AddDefaultConstructorFix.addDefaultConstructor(localClass))
+ }
+
+ val constructorCalls = mutableMapOf>()
+
+ if (variableInfos.isNotEmpty()) {
+ for (reference in DesugarUtil.findReferencesInFile(localClass)) {
+ val methodRef = reference.element.parent as? PsiMethodReferenceExpression ?: continue
+ if (methodRef.isConstructor) {
+ DesugarUtil.desugarMethodReferenceToLambda(methodRef)
+ }
+ }
+
+ for (constructor in constructors) {
+ for (reference in DesugarUtil.findReferencesInFile(constructor)) {
+ var refElement = reference.element
+ if (refElement is PsiMethod || (refElement is PsiClass && refElement !is PsiAnonymousClass)) {
+ constructorCalls.getOrPut(constructor) { mutableListOf() } += refElement
+ } else {
+ refElement = refElement.parent
+ if (refElement is PsiAnonymousClass) {
+ refElement = refElement.parent
+ }
+ if (refElement is PsiCall) {
+ constructorCalls.getOrPut(constructor) { mutableListOf() } += refElement
+ }
+ }
+ }
+ }
+ }
+
+ val factory = JavaPsiFacade.getElementFactory(project)
+
+ for (constructor in constructors) {
+ val callSites = constructorCalls[constructor] ?: emptyList()
+
+ fillParameterList(project, constructor, variableInfos)
+
+ val constructorHasThisCall = JavaPsiConstructorUtil.isChainedConstructorCall(
+ JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(constructor)
+ )
+ if (!constructorHasThisCall) {
+ createAssignmentStatements(constructor, variableInfos)
+ }
+
+ for (callSite in callSites) {
+ val argumentList = when (callSite) {
+ is PsiCall -> callSite.argumentList ?: continue
+ is PsiMethod -> {
+ if (!callSite.isConstructor) {
+ continue
+ }
+ val body = callSite.body ?: continue
+ val superCall = body.addAfter(factory.createStatementFromText("super();", null), body.lBrace)
+ as PsiExpressionStatement
+ (superCall.expression as PsiMethodCallExpression).argumentList
+ }
+ is PsiClass -> {
+ val ctor = AddDefaultConstructorFix.addDefaultConstructor(callSite)
+ val body = ctor.body ?: continue
+ val superCall = body.addAfter(factory.createStatementFromText("super();", null), body.lBrace)
+ as PsiExpressionStatement
+ (superCall.expression as PsiMethodCallExpression).argumentList
+ }
+ else -> continue
+ }
+ for (info in variableInfos) {
+ argumentList.add(factory.createExpressionFromText(info.paramName, argumentList))
+ }
+ }
+ }
+ }
+
+ private fun fillParameterList(project: Project, constructor: PsiMethod, variableInfos: Array) {
+ val factory = JavaPsiFacade.getElementFactory(project)
+ val parameterList = constructor.parameterList
+ for (info in variableInfos) {
+ val parameter = factory.createParameter(info.paramName, info.variable.type)
+ DesugarUtil.setUnnamedVariable(parameter, true)
+ parameterList.add(parameter)
+ }
+ }
+
+ private fun createAssignmentStatements(constructor: PsiMethod, variableInfos: Array) {
+ val constructorBody = constructor.body ?: return
+ val factory = PsiElementFactory.getInstance(constructor.project)
+
+ if (JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(constructor) == null) {
+ val superCall = factory.createStatementFromText("super();", constructorBody)
+ constructorBody.addAfter(superCall, constructorBody.lBrace)
+ }
+
+ for (i in variableInfos.lastIndex downTo 0) {
+ val info = variableInfos[i]
+ if (info.fieldName != null) {
+ val statement = factory.createStatementFromText("this.${info.fieldName} = ${info.paramName};", constructorBody)
+ constructorBody.addAfter(statement, constructorBody.lBrace)
+ }
+ }
+ }
+
+ private fun createClass(
+ project: Project,
+ localClass: PsiClass,
+ targetClass: PsiClass,
+ variableInfos: Array,
+ typeParametersToCreate: Collection,
+ name: String,
+ ): PsiClass {
+ updateSelfReferences(project, localClass, typeParametersToCreate, name)
+
+ val newClass = if (localClass is PsiAnonymousClass) {
+ createInnerFromAnonymous(project, name, localClass)
+ } else {
+ createInnerFromLocal(project, name, localClass)
+ }
+
+ if (!localClass.isInterface && !localClass.isEnum && !localClass.isRecord) {
+ if (isInStaticContext(localClass, targetClass)) {
+ PsiUtil.setModifierProperty(newClass, PsiModifier.STATIC, true)
+ }
+ }
+
+ val typeParameterList = newClass.typeParameterList!!
+ for (parameter in typeParametersToCreate) {
+ typeParameterList.add(parameter)
+ }
+
+ if (variableInfos.isNotEmpty()) {
+ createFields(project, newClass, variableInfos)
+ }
+
+ if (localClass is PsiAnonymousClass) {
+ createAnonymousClassConstructor(project, newClass, localClass, variableInfos)
+ }
+
+ val lastChild = newClass.lastChild
+ if (PsiUtil.isJavaToken(lastChild, JavaTokenType.SEMICOLON)) {
+ lastChild.delete()
+ }
+
+ return newClass
+ }
+
+ private fun renameReferences(project: Project, context: DesugarContext, localClass: PsiClass, variableInfos: Array) {
+ val factory = JavaPsiFacade.getElementFactory(project)
+ for (info in variableInfos) {
+ for (reference in DesugarUtil.findReferencesInFile(info.variable)) {
+ val element = reference.element as? PsiJavaCodeReferenceElement ?: continue
+ if (element.isInsideAnonymousClassParameter(localClass)) {
+ continue
+ }
+ if (!PsiTreeUtil.isAncestor(localClass, element, false)) {
+ continue
+ }
+ val identifier = element.referenceNameElement as? PsiIdentifier ?: continue
+ val shouldRenameToField = info.fieldName != null && renameReferenceToField(element, context)
+ val newNameIdentifier = factory.createIdentifier(if (shouldRenameToField) info.fieldName else info.paramName)
+ DesugarUtil.setOriginalElement(newNameIdentifier, DesugarUtil.getOriginalElement(identifier))
+ identifier.replace(newNameIdentifier)
+ }
+ }
+ }
+
+ private fun renameReferenceToField(element: PsiJavaCodeReferenceElement, context: DesugarContext): Boolean {
+ val constructor = element.findContainingMethod() ?: return true
+ if (!constructor.isConstructor) {
+ return true
+ }
+
+ if (context.classVersion >= Opcodes.V22) {
+ return false
+ }
+
+ val thisOrSuperCall = JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(constructor) ?: return true
+ return element.textRange.startOffset >= thisOrSuperCall.textRange.endOffset
+ }
+
+ private fun updateSelfReferences(
+ project: Project,
+ localClass: PsiClass,
+ typeParametersToCreate: Collection,
+ name: String,
+ ) {
+ if (localClass is PsiAnonymousClass) {
+ return
+ }
+ if (name == localClass.name && typeParametersToCreate.isEmpty()) {
+ return
+ }
+
+ val factory = JavaPsiFacade.getElementFactory(project)
+ val origCount = localClass.typeParameters.size
+
+ for (reference in DesugarUtil.findReferencesInFile(localClass)) {
+ val originalReference = DesugarUtil.getOriginalElement(reference.element)
+ val renamedReference = reference.handleElementRename(name)
+ DesugarUtil.setOriginalElement(renamedReference, originalReference)
+ if (typeParametersToCreate.isNotEmpty()) {
+ val refElement = reference.element as? PsiJavaCodeReferenceElement ?: continue
+ if ((refElement.parent as? PsiTypeElement)?.parent is PsiClassObjectAccessExpression) {
+ continue
+ }
+ val referenceParameterList = refElement.childrenOfType().firstOrNull()
+ // Do not modify broken or already raw parameter lists
+ if (referenceParameterList != null && referenceParameterList.typeArgumentCount == origCount) {
+ for (parameter in typeParametersToCreate) {
+ val element = factory.createTypeElement(factory.createType(parameter))
+ referenceParameterList.add(element)
+ }
+ }
+ }
+ }
+ }
+
+ private fun createInnerFromLocal(project: Project, name: String, localClass: PsiClass): PsiClass {
+ val factory = JavaPsiFacade.getElementFactory(project)
+ val newClass = localClass.copy() as PsiClass
+ val identifier = factory.createIdentifier(name)
+ val originalNameIdentifier = localClass.nameIdentifier?.let(DesugarUtil::getOriginalElement)
+ newClass.nameIdentifier?.replace(identifier)?.let {
+ DesugarUtil.setOriginalElement(it, originalNameIdentifier)
+ }
+ for (constructor in newClass.methods) {
+ if (constructor.isConstructor) {
+ val originalCtorNameIdentifier = constructor.nameIdentifier?.let(DesugarUtil::getOriginalElement)
+ constructor.nameIdentifier?.replace(identifier)?.let {
+ DesugarUtil.setOriginalElement(it, originalCtorNameIdentifier)
+ }
+ }
+ }
+ return newClass
+ }
+
+ private fun createInnerFromAnonymous(project: Project, name: String, localClass: PsiAnonymousClass): PsiClass {
+ val factory = JavaPsiFacade.getElementFactory(project)
+ val newClass = factory.createClass(name)
+ PsiUtil.setModifierProperty(newClass, PsiModifier.PACKAGE_LOCAL, true)
+ var baseClassRef = localClass.baseClassReference
+ val parameterList = baseClassRef.parameterList
+ if (parameterList != null) {
+ val parameterElement = parameterList.typeParameterElements.singleOrNull()
+ if (parameterElement?.type is PsiDiamondType) {
+ val originalBaseClassRef = DesugarUtil.getOriginalElement(baseClassRef)
+ baseClassRef = PsiDiamondTypeUtil.replaceDiamondWithExplicitTypes(parameterList)
+ as PsiJavaCodeReferenceElement
+ DesugarUtil.setOriginalElement(baseClassRef, originalBaseClassRef)
+ }
+ }
+ val baseClass = baseClassRef.resolve() as PsiClass?
+ if (baseClass?.qualifiedName != CommonClassNames.JAVA_LANG_OBJECT) {
+ val refList = if (baseClass?.isInterface == true) {
+ newClass.implementsList
+ } else {
+ newClass.extendsList
+ }
+ refList?.add(baseClassRef)
+ }
+ val lBrace = localClass.lBrace
+ val rBrace = localClass.rBrace
+ if (lBrace != null) {
+ newClass.addRange(lBrace.nextSibling, rBrace?.prevSibling ?: localClass.lastChild)
+ }
+ DesugarUtil.setOriginalElement(newClass, DesugarUtil.getOriginalElement(localClass))
+ return newClass
+ }
+
+ private fun createFields(project: Project, localClass: PsiClass, variableInfos: Array) {
+ val factory = JavaPsiFacade.getElementFactory(project)
+ for (info in variableInfos) {
+ if (info.fieldName != null) {
+ val field = factory.createField(info.fieldName, info.variable.type)
+ PsiUtil.setModifierProperty(field, PsiModifier.PACKAGE_LOCAL, true)
+ PsiUtil.setModifierProperty(field, PsiModifier.FINAL, true)
+ localClass.add(field)
+ }
+ }
+ }
+
+ private fun createAnonymousClassConstructor(project: Project, newClass: PsiClass, localClass: PsiAnonymousClass, variableInfos: Array) {
+ val factory = JavaPsiFacade.getElementFactory(project)
+ val newExpression = localClass.parent as? PsiNewExpression ?: return
+ val argList = newExpression.argumentList ?: return
+ val originalExpressions = argList.expressions
+ val superConstructor = newExpression.resolveConstructor()
+ val superConstructorThrowsList = superConstructor?.throwsList?.takeIf { it.referencedTypes.isNotEmpty() }
+ if (variableInfos.isNotEmpty() || originalExpressions.isNotEmpty() || superConstructorThrowsList != null) {
+ val constructor = factory.createConstructor()
+ PsiUtil.setModifierProperty(constructor, PsiModifier.PACKAGE_LOCAL, true)
+ if (superConstructorThrowsList != null) {
+ constructor.throwsList.replace(superConstructorThrowsList)
+ }
+ if (variableInfos.any { it.passToSuper }) {
+ createSuperStatement(project, constructor, variableInfos)
+ }
+ if (variableInfos.isNotEmpty()) {
+ fillParameterList(project, constructor, variableInfos)
+ createAssignmentStatements(constructor, variableInfos)
+ }
+ newClass.add(constructor)
+ }
+ }
+
+ private fun createSuperStatement(project: Project, constructor: PsiMethod, variableInfos: Array) {
+ val body = constructor.body ?: return
+ val factory = JavaPsiFacade.getElementFactory(project)
+
+ val statementText = buildString {
+ append("super(")
+ for ((index, info) in variableInfos.withIndex()) {
+ if (info.passToSuper) {
+ if (index > 0) {
+ append(", ")
+ }
+ append(info.paramName)
+ }
+ }
+ append(");")
+ }
+
+ body.add(factory.createStatementFromText(statementText, constructor))
+ }
+
+ private fun migrateAnonymousClassCreation(
+ project: Project,
+ newClassName: String,
+ anonymousClass: PsiAnonymousClass,
+ variableInfos: Array,
+ typeParametersToCreate: Collection,
+ ) {
+ val newExpr = anonymousClass.parent as? PsiNewExpression ?: return
+ val argumentList = newExpr.argumentList ?: return
+
+ val newNewExprText = buildString {
+ append("new ")
+ append(newClassName)
+ if (typeParametersToCreate.isNotEmpty()) {
+ append('<')
+ for ((index, typeParam) in typeParametersToCreate.withIndex()) {
+ if (index > 0) {
+ append(", ")
+ }
+ append(typeParam.name)
+ }
+ append('>')
+ }
+ append("()")
+ }
+
+ val factory = JavaPsiFacade.getElementFactory(project)
+ val newNewExpr = factory.createExpressionFromText(newNewExprText, null)
+ as PsiNewExpression
+ val newArgumentList = newNewExpr.argumentList!!.replace(argumentList) as PsiExpressionList
+ // skip the already existing arguments
+ for (info in variableInfos.drop(newArgumentList.expressionCount)) {
+ val varName = info.variable.name ?: continue
+ newArgumentList.add(factory.createExpressionFromText(varName, null))
+ }
+
+ DesugarUtil.setOriginalElement(newNewExpr, DesugarUtil.getOriginalElement(newExpr))
+
+ newExpr.replace(newNewExpr)
+ }
+
+ private fun migrateLocalClassCreation(localClass: PsiClass, newClass: PsiClass) {
+ val refs = DesugarUtil.findReferencesInFile(localClass)
+ localClass.delete()
+ for (ref in refs) {
+ val element = ref.element
+ if (element.isValid) {
+ ref.bindToElement(newClass)
+ }
+ }
+ }
+
+ private fun isInStaticContext(localClass: PsiClass, targetClass: PsiClass): Boolean {
+ val containingMethod = localClass.parent?.findContainingMethod()
+ if (containingMethod != null && containingMethod.isConstructor) {
+ val delegateCtorCall = JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(containingMethod)
+ ?: return false
+ return localClass.textRange.startOffset < delegateCtorCall.textRange.endOffset
+ }
+
+ val localParent = localClass.parent ?: return true
+ return CommonJavaRefactoringUtil.isInStaticContext(localParent, targetClass)
+ }
+
+ private fun PsiElement.isInsideAnonymousClassParameter(anonymousClass: PsiClass): Boolean {
+ return this.parents(false)
+ .takeWhile { it !== anonymousClass }
+ .any { it is PsiExpressionList && it.parent === anonymousClass }
+ }
+
+ private class VariableInfo(
+ val variable: PsiVariable,
+ val paramName: String,
+ val fieldName: String?,
+ val passToSuper: Boolean = false,
+ )
+}
Index: src/main/kotlin/platform/mixin/handlers/desugar/DesugarContext.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/desugar/DesugarContext.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
+++ src/main/kotlin/platform/mixin/handlers/desugar/DesugarContext.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -0,0 +1,23 @@
+/*
+ * 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.platform.mixin.handlers.desugar
+
+class DesugarContext(val classVersion: Int)
Index: src/main/kotlin/platform/mixin/handlers/desugar/DesugarUtil.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/desugar/DesugarUtil.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/main/kotlin/platform/mixin/handlers/desugar/DesugarUtil.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -21,21 +21,42 @@
package com.demonwav.mcdev.platform.mixin.handlers.desugar
import com.demonwav.mcdev.util.cached
+import com.demonwav.mcdev.util.childrenOfType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
+import com.intellij.openapi.util.TextRange
+import com.intellij.openapi.util.UnfairTextRange
import com.intellij.psi.JavaRecursiveElementWalkingVisitor
+import com.intellij.psi.PsiAnonymousClass
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiLambdaExpression
+import com.intellij.psi.PsiMember
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiMethodReferenceExpression
+import com.intellij.psi.PsiNameIdentifierOwner
+import com.intellij.psi.PsiReference
+import com.intellij.psi.PsiSubstitutor
+import com.intellij.psi.PsiTypeParameter
+import com.intellij.psi.PsiVariable
+import com.intellij.psi.impl.light.LightMemberReference
+import com.intellij.psi.search.LocalSearchScope
+import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.parents
+import com.intellij.refactoring.util.LambdaRefactoringUtil
+import com.intellij.util.JavaPsiConstructorUtil
+import com.intellij.util.Processor
import org.jetbrains.annotations.VisibleForTesting
object DesugarUtil {
private val ORIGINAL_ELEMENT_KEY = Key.create("mcdev.desugar.originalElement")
+ private val UNNAMED_VARIABLE_KEY = Key.create("mcdev.desugar.unnamedVariable")
private val DESUGARERS = arrayOf(
RemoveVarArgsDesugarer,
+ AnonymousAndLocalClassDesugarer,
FieldAssignmentDesugarer,
)
@@ -61,13 +82,21 @@
}
}
- fun desugar(project: Project, clazz: PsiClass): PsiClass? {
+ fun isUnnamedVariable(variable: PsiVariable): Boolean {
+ return variable.getCopyableUserData(UNNAMED_VARIABLE_KEY) == true
+ }
+
+ fun setUnnamedVariable(variable: PsiVariable, value: Boolean) {
+ variable.putCopyableUserData(UNNAMED_VARIABLE_KEY, value)
+ }
+
+ fun desugar(project: Project, clazz: PsiClass, context: DesugarContext): PsiClass? {
val file = clazz.containingFile as? PsiJavaFile ?: return null
return file.cached {
val desugaredFile = file.copy() as PsiJavaFile
setOriginalRecursive(desugaredFile, file)
for (desugarer in DESUGARERS) {
- desugarer.desugar(project, desugaredFile)
+ desugarer.desugar(project, desugaredFile, context)
}
getOriginalToDesugaredMap(desugaredFile)[clazz]?.filterIsInstance()?.firstOrNull()
}
@@ -95,4 +124,90 @@
setOriginalElement(desugaredElement, originalElement)
}
}
+
+ internal fun allClasses(file: PsiJavaFile): List {
+ return file.childrenOfType().filter { it !is PsiTypeParameter }
-}
+ }
+
+ internal fun allClassesShallow(clazz: PsiClass): List {
+ val allClasses = mutableListOf()
+ clazz.acceptChildren(object : JavaRecursiveElementWalkingVisitor() {
+ override fun visitClass(aClass: PsiClass) {
+ if (aClass !is PsiTypeParameter) {
+ allClasses += aClass
+ }
+ }
+ })
+ return allClasses
+ }
+
+ internal fun findReferencesInFile(element: PsiElement): List {
+ fun PsiMember.createSyntheticReference(): PsiReference {
+ return object : LightMemberReference(manager, this, PsiSubstitutor.EMPTY) {
+ override fun getElement() = this@createSyntheticReference
+ override fun getRangeInElement(): TextRange {
+ val identifier = (this@createSyntheticReference as? PsiNameIdentifierOwner)?.nameIdentifier
+ if (identifier != null) {
+ val startOffsetInParent = identifier.startOffsetInParent
+ return if (startOffsetInParent >= 0) {
+ TextRange.from(startOffsetInParent, identifier.textLength)
+ } else {
+ UnfairTextRange(-1, -1)
+ }
+ }
+
+ return super.getRangeInElement()
+ }
+ }
+ }
+
+ val file = element.containingFile as? PsiJavaFile ?: return emptyList()
+ val results = mutableListOf()
+
+ ReferencesSearch.search(element, LocalSearchScope(file)).forEach(Processor {
+ results += it
+ true
+ })
+
+ // subclass constructor references don't work for non-physical files
+ if (element is PsiMethod && element.isConstructor) {
+ val clazz = element.containingClass
+ if (clazz != null) {
+ for (subClass in allClasses(file)) {
+ if (subClass !is PsiAnonymousClass && subClass.isInheritor(clazz, false)) {
+ val countImplicitSuperCalls = element.parameterList.isEmpty
+ val constructors = subClass.constructors
+ for (constructor in constructors) {
+ val thisOrSuperCall = JavaPsiConstructorUtil.findThisOrSuperCallInConstructor(constructor)
+ if (JavaPsiConstructorUtil.isSuperConstructorCall(thisOrSuperCall)) {
+ val reference = thisOrSuperCall!!.methodExpression.reference
+ if (reference != null && reference.isReferenceTo(element)) {
+ results += reference
+ }
+ } else if (thisOrSuperCall == null && countImplicitSuperCalls) {
+ results += constructor.createSyntheticReference()
+ }
+ }
+
+ if (constructors.isEmpty() && countImplicitSuperCalls) {
+ results += subClass.createSyntheticReference()
+ }
+ }
+ }
+ }
+ }
+
+ return results
+ }
+
+ internal fun desugarMethodReferenceToLambda(methodReference: PsiMethodReferenceExpression): PsiLambdaExpression? {
+ val originalMethodRef = getOriginalElement(methodReference)
+ val lambda = LambdaRefactoringUtil.convertMethodReferenceToLambda(methodReference, false, true)
+ ?: return null
+ setOriginalElement(lambda, originalMethodRef)
+ for (parameter in lambda.parameterList.parameters) {
+ setUnnamedVariable(parameter, true)
+ }
+ return lambda
+ }
+}
Index: src/main/kotlin/platform/mixin/handlers/desugar/Desugarer.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/desugar/Desugarer.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/main/kotlin/platform/mixin/handlers/desugar/Desugarer.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -21,16 +21,8 @@
package com.demonwav.mcdev.platform.mixin.handlers.desugar
import com.intellij.openapi.project.Project
-import com.intellij.psi.PsiClass
import com.intellij.psi.PsiJavaFile
-import com.intellij.psi.util.childrenOfType
abstract class Desugarer {
- abstract fun desugar(project: Project, file: PsiJavaFile)
-
- companion object {
- @JvmStatic
- protected val PsiJavaFile.allClasses: List
- get() = this.childrenOfType()
+ abstract fun desugar(project: Project, file: PsiJavaFile, context: DesugarContext)
- }
+}
-}
Index: src/main/kotlin/platform/mixin/handlers/desugar/FieldAssignmentDesugarer.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/desugar/FieldAssignmentDesugarer.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/main/kotlin/platform/mixin/handlers/desugar/FieldAssignmentDesugarer.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -36,13 +36,13 @@
import com.intellij.util.JavaPsiConstructorUtil
object FieldAssignmentDesugarer : Desugarer() {
- override fun desugar(project: Project, file: PsiJavaFile) {
+ override fun desugar(project: Project, file: PsiJavaFile, context: DesugarContext) {
val staticStatementsToInsertPre = mutableListOf()
val staticStatementsToInsertPost = mutableListOf()
val nonStaticStatementsToInsert = mutableListOf()
var seenStaticInitializer = false
- for (aClass in file.allClasses) {
+ for (aClass in DesugarUtil.allClasses(file)) {
for (child in aClass.children) {
when (child) {
is PsiField -> {
Index: src/main/kotlin/platform/mixin/handlers/desugar/RemoveVarArgsDesugarer.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/desugar/RemoveVarArgsDesugarer.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/main/kotlin/platform/mixin/handlers/desugar/RemoveVarArgsDesugarer.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -38,7 +38,7 @@
import com.siyeh.ig.psiutils.MethodCallUtils
object RemoveVarArgsDesugarer : Desugarer() {
- override fun desugar(project: Project, file: PsiJavaFile) {
+ override fun desugar(project: Project, file: PsiJavaFile, context: DesugarContext) {
val varArgsStarts = mutableListOf>()
PsiTreeUtil.processElements(file) { element ->
if (element is PsiCall && MethodCallUtils.isVarArgCall(element)) {
Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
===================================================================
--- src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -20,6 +20,7 @@
package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint
+import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarContext
import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarUtil
import com.demonwav.mcdev.platform.mixin.reference.MixinSelector
import com.demonwav.mcdev.platform.mixin.reference.isMiscDynamicSelector
@@ -226,7 +227,8 @@
val targetPsiFile = targetPsiClass.containingFile ?: return emptyList()
// Desugar the target class
- val desugaredTargetClass = DesugarUtil.desugar(project, targetPsiClass) ?: return emptyList()
+ val desugaredTargetClass = DesugarUtil.desugar(project, targetPsiClass, DesugarContext(targetClass.version))
+ ?: return emptyList()
// Find the element in the desugared class, first by directly searching and then by searching in the original
// and reverse mapping it into the desugared class.
Index: src/main/kotlin/platform/mixin/util/LocalVariables.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/LocalVariables.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/main/kotlin/platform/mixin/util/LocalVariables.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -48,6 +48,7 @@
import com.demonwav.mcdev.facet.MinecraftFacet
import com.demonwav.mcdev.platform.mixin.MixinModuleType
+import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarUtil
import com.demonwav.mcdev.util.SemanticVersion
import com.demonwav.mcdev.util.cached
import com.demonwav.mcdev.util.mapToArray
@@ -118,7 +119,7 @@
}
for (parameter in method.parameterList.parameters) {
- val mixinName = if (argsOnly) "var$argsIndex" else parameter.name
+ val mixinName = if (argsOnly) "arg$argsIndex" else parameter.name
args += SourceLocalVariable(
parameter.name,
parameter.type,
@@ -217,7 +218,8 @@
name,
instruction.variable.type,
localIndex,
- variable = instruction.variable
+ variable = instruction.variable,
+ mixinName = if (DesugarUtil.isUnnamedVariable(instruction.variable)) "var$localIndex" else name,
)
if (instruction.variable.isDoubleSlot && localIndex + 1 < localsHere.size) {
localsHere[localIndex + 1] = null
@@ -250,7 +252,6 @@
val localsHere = this.locals[offset] ?: emptyArray()
var changed = false
val nextLocals = this.locals[nextOffset]
- @Suppress("KotlinConstantConditions") // kotlin is wrong
if (nextLocals == null) {
this.locals[nextOffset] = localsHere.clone()
changed = true
@@ -270,7 +271,6 @@
}
}
}
- @Suppress("KotlinConstantConditions") // kotlin is wrong
if (changed) {
instructionQueue.add(nextOffset)
}
@@ -325,8 +325,8 @@
is PsiVariable -> if (element.isDoubleSlot) 2 else 1
// arrays have copy of array, length and index variables, iterables have the iterator variable
is PsiForeachStatement -> {
- val param = element.iterationParameter as? PsiParameter
- if (param?.type is PsiArrayType) 3 else 1
+ val param = element.iterationParameter
+ if (param.type is PsiArrayType) 3 else 1
}
else -> 0
}
Index: src/main/kotlin/util/class-utils.kt
===================================================================
--- src/main/kotlin/util/class-utils.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/main/kotlin/util/class-utils.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -20,6 +20,7 @@
package com.demonwav.mcdev.util
+import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarUtil
import com.intellij.codeInsight.daemon.impl.quickfix.AddMethodFix
import com.intellij.navigation.AnonymousElementProvider
import com.intellij.openapi.project.Project
@@ -56,6 +57,7 @@
val PsiClass.fullQualifiedName
get(): String? {
+ (DesugarUtil.getOriginalElement(this) as? PsiClass)?.let { return it.fullQualifiedName }
return try {
outerQualifiedName ?: buildQualifiedName(StringBuilder()).toString()
} catch (e: ClassNameResolutionFailedException) {
Index: src/test/kotlin/platform/mixin/desugar/AbstractDesugarTest.kt
===================================================================
--- src/test/kotlin/platform/mixin/desugar/AbstractDesugarTest.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/test/kotlin/platform/mixin/desugar/AbstractDesugarTest.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -21,6 +21,7 @@
package com.demonwav.mcdev.platform.mixin.desugar
import com.demonwav.mcdev.framework.BaseMinecraftTest
+import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarContext
import com.demonwav.mcdev.platform.mixin.handlers.desugar.DesugarUtil
import com.demonwav.mcdev.platform.mixin.handlers.desugar.Desugarer
import com.intellij.openapi.command.WriteCommandAction
@@ -35,15 +36,16 @@
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Assertions.assertTrue
+import org.objectweb.asm.Opcodes
abstract class AbstractDesugarTest : BaseMinecraftTest() {
abstract val desugarer: Desugarer
- protected fun doTestNoChange(@Language("JAVA") code: String) {
- doTest(code, code)
+ protected fun doTestNoChange(@Language("JAVA") code: String, classVersion: Int = Opcodes.V21) {
+ doTest(code, code, classVersion)
}
- protected fun doTest(@Language("JAVA") before: String, @Language("JAVA") after: String) {
+ protected fun doTest(@Language("JAVA") before: String, @Language("JAVA") after: String, classVersion: Int = Opcodes.V21) {
WriteCommandAction.runWriteCommandAction(project) {
val codeStyleManager = CodeStyleManager.getInstance(project)
val javaCodeStyleManager = JavaCodeStyleManager.getInstance(project)
@@ -71,7 +73,7 @@
val desugaredFile = testFile.copy() as PsiJavaFile
DesugarUtil.setOriginalRecursive(desugaredFile, testFile)
- desugarer.desugar(project, desugaredFile)
+ desugarer.desugar(project, desugaredFile, DesugarContext(classVersion))
assertEquals(
expectedText,
codeStyleManager.reformat(javaCodeStyleManager.shortenClassReferences(desugaredFile.copy())).text
Index: src/test/kotlin/platform/mixin/desugar/AnonymousAndLocalClassDesugarTest.kt
===================================================================
--- src/test/kotlin/platform/mixin/desugar/AnonymousAndLocalClassDesugarTest.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
+++ src/test/kotlin/platform/mixin/desugar/AnonymousAndLocalClassDesugarTest.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -0,0 +1,1097 @@
+/*
+ * 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.platform.mixin.desugar
+
+import com.demonwav.mcdev.platform.mixin.handlers.desugar.AnonymousAndLocalClassDesugarer
+import org.junit.jupiter.api.Test
+import org.objectweb.asm.Opcodes
+
+@Suppress("Convert2Lambda", "CopyConstructorMissesField", "SillyAssignment")
+class AnonymousAndLocalClassDesugarTest : AbstractDesugarTest() {
+ override val desugarer = AnonymousAndLocalClassDesugarer
+
+ @Test
+ fun testSimpleAnonymous() {
+ doTest(
+ """
+ class Test {
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ System.out.println("Hello World!");
+ }
+ };
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ Runnable r = new $1();
+
+ class $1 implements Runnable {
+ @Override
+ public void run() {
+ System.out.println("Hello World!");
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousWithParameters() {
+ doTest(
+ """
+ public class Test {
+ Test test = new Test(this) {
+ };
+
+ public Test(Test test) {
+ }
+ }
+ """.trimIndent(),
+ """
+ public class Test {
+ Test test = new $1(this);
+
+ public Test(Test test) {
+ }
+
+ class $1 extends Test {
+ $1(Test test1) {
+ super(test1);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousWithCapture() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello, World!";
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ System.out.println(hello);
+ }
+ };
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello, World!";
+ Runnable r = new $1(hello);
+ }
+
+ class $1 implements Runnable {
+ final String val${'$'}hello;
+
+ $1(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ }
+
+ @Override
+ public void run() {
+ System.out.println(val${'$'}hello);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousWithParameterAndCapture() {
+ doTest(
+ """
+ public class Test {
+ void test() {
+ String hello = "Hello, World!";
+ Test test = new Test(this) {
+ String x = hello;
+ };
+ }
+
+ public Test(Test test) {
+ }
+ }
+ """.trimIndent(),
+ """
+ public class Test {
+ void test() {
+ String hello = "Hello, World!";
+ Test test = new $1(this, hello);
+ }
+
+ public Test(Test test) {
+ }
+
+ class $1 extends Test {
+ final String val${'$'}hello;
+ String x = val${'$'}hello;
+
+ $1(Test test1, String hello) {
+ this.val${'$'}hello = hello;
+ super(test1);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testStaticAnonymous() {
+ doTest(
+ """
+ class Test {
+ static void test() {
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ }
+ };
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ static void test() {
+ Runnable r = new $1();
+ }
+
+ static class $1 implements Runnable {
+ @Override
+ public void run() {
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testStaticAnonymousAsDelegateConstructorParameter() {
+ doTest(
+ """
+ class Test {
+ public Test() {
+ this(new Object() {
+ });
+ }
+
+ public Test(Object x) {
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ public Test() {
+ this(new $1());
+ }
+
+ public Test(Object x) {
+ }
+
+ static class $1 {
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testSimpleLocal() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ class Local {
+ }
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ }
+
+ class $1Local {
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testSimpleLocalWithCapture() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello, World!";
+ class Local {
+ void print() {
+ System.out.println(hello);
+ }
+ }
+ new Local().print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello, World!";
+ new $1Local(hello).print();
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+
+ $1Local(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalWithExistingConstructorAndCapture() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ class Local {
+ private final String name;
+
+ public Local(String name) {
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(hello + " " + name);
+ }
+ }
+ new Local("World").print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ new $1Local("World", hello).print();
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+ private final String name;
+
+ public $1Local(String name, String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello + " " + name);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalWithMultipleExistingConstructorsAndCapture() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ class Local {
+ private final String name;
+
+ public Local() {
+ this.name = "World";
+ }
+
+ public Local(String name) {
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(hello + " " + name);
+ }
+ }
+ new Local().print();
+ new Local("World").print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ new $1Local(hello).print();
+ new $1Local("World", hello).print();
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+ private final String name;
+
+ public $1Local(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ this.name = "World";
+ }
+
+ public $1Local(String name, String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello + " " + name);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalWithDelegateConstructorAndCapture() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ class Local {
+ private final String name;
+
+ public Local() {
+ this("World");
+ }
+
+ public Local(String name) {
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(hello + " " + name);
+ }
+ }
+ new Local().print();
+ new Local("World").print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ new $1Local(hello).print();
+ new $1Local("World", hello).print();
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+ private final String name;
+
+ public $1Local(String hello) {
+ this("World", hello);
+ }
+
+ public $1Local(String name, String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello + " " + name);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousExtendsLocal() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+ class Local {
+ void print() {
+ System.out.println(hello);
+ }
+ }
+ new Local() {
+ @Override
+ void print() {
+ super.print();
+ System.out.println(world);
+ }
+ }.print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+ new $1(hello, world).print();
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+
+ $1Local(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello);
+ }
+ }
+
+ class $1 extends $1Local {
+ final String val${'$'}hello;
+ final String val${'$'}world;
+
+ $1(String hello1, String world) {
+ this.val${'$'}hello = hello1;
+ this.val${'$'}world = world;
+ super(hello1);
+ }
+
+ @Override
+ void print() {
+ super.print();
+ System.out.println(val${'$'}world);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousExtendsLocalWithParameters() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+ class Local {
+ private final String name;
+
+ public Local(String name) {
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(hello + " " + name);
+ }
+ }
+ new Local(hello) {
+ @Override
+ void print() {
+ super.print();
+ System.out.println(hello + " " + world);
+ }
+ }.print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+ new $1(hello, hello, world).print();
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+ private final String name;
+
+ public $1Local(String name, String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello + " " + name);
+ }
+ }
+
+ class $1 extends $1Local {
+ final String val${'$'}hello;
+ final String val${'$'}world;
+
+ $1(String name, String hello1, String world) {
+ this.val${'$'}hello = hello1;
+ this.val${'$'}world = world;
+ super(name, hello1);
+ }
+
+ @Override
+ void print() {
+ super.print();
+ System.out.println(val${'$'}hello + " " + val${'$'}world);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalExtendsLocal() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+ class Local1 {
+ void print() {
+ System.out.println(hello);
+ }
+ }
+ class Local2 extends Local1 {
+ @Override
+ void print() {
+ super.print();
+ System.out.println(world);
+ }
+ }
+ new Local2().print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+ new $1Local2(hello, world).print();
+ }
+
+ class $1Local1 {
+ final String val${'$'}hello;
+
+ $1Local1(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello);
+ }
+ }
+
+ class $1Local2 extends $1Local1 {
+ final String val${'$'}hello;
+ final String val${'$'}world;
+
+ $1Local2(String hello, String world) {
+ this.val${'$'}hello = hello;
+ this.val${'$'}world = world;
+ super(hello);
+ }
+
+ @Override
+ void print() {
+ super.print();
+ System.out.println(val${'$'}world);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalExtendsLocalWithParameters() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+
+ class Local1 {
+ private final String name;
+
+ public Local1(String name) {
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(hello + " " + name);
+ }
+ }
+
+ class Local2 extends Local1 {
+ public Local2(String name) {
+ super(name);
+ }
+
+ @Override
+ void print() {
+ super.print();
+ System.out.println(hello + " " + world);
+ }
+ }
+
+ new Local2(hello).print();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+ String world = "World";
+
+ new $1Local2(hello, hello, world).print();
+ }
+
+ class $1Local1 {
+ final String val${'$'}hello;
+ private final String name;
+
+ public $1Local1(String name, String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ this.name = name;
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello + " " + name);
+ }
+ }
+
+ class $1Local2 extends $1Local1 {
+ final String val${'$'}hello;
+ final String val${'$'}world;
+
+ public $1Local2(String name, String hello, String world) {
+ this.val${'$'}hello = hello;
+ this.val${'$'}world = world;
+ super(name, hello);
+ }
+
+ @Override
+ void print() {
+ super.print();
+ System.out.println(val${'$'}hello + " " + val${'$'}world);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousInsideGenericClass() {
+ doTest(
+ """
+ class Test {
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ }
+
+ T test() {
+ }
+ };
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ Runnable r = new $1();
+
+ class $1 implements Runnable {
+ @Override
+ public void run() {
+ }
+
+ T test() {
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousInsideUnsuedGenericMethod() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ new Object() {
+ };
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ new $1();
+ }
+
+ class $1 {
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousInsideGenericMethod() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ new Object() {
+ T test() {
+ }
+ };
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ new $1();
+ }
+
+ class $1 {
+ T test() {
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalInsideGenericClass() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ class Local {
+ T test() {
+ }
+ }
+ new Local();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ new $1Local();
+ }
+
+ class $1Local {
+ T test() {
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalInsideUnsuedGenericMethod() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ class Local {
+ }
+ new Local();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ new $1Local();
+ }
+
+ class $1Local {
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalInsideGenericMethod() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ class Local {
+ T test() {
+ }
+ }
+ new Local();
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ new $1Local();
+ }
+
+ class $1Local {
+ T test() {
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testAnonymousExtendsGenericLocal() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ class Local {
+ }
+ Local local = new Local<>() {
+ };
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ $1Local local = new $1();
+ }
+
+ class $1Local {
+ }
+
+ class $1 extends $1Local {
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testLocalWithCaptureCreatedWithConstructorReference() {
+ doTest(
+ """
+ import java.util.function.Supplier;
+
+ class Test {
+ void test() {
+ String hello = "Hello";
+
+ class Local {
+ void print() {
+ System.out.println(hello);
+ }
+ }
+
+ Supplier supplier = Local::new;
+ }
+ }
+ """.trimIndent(),
+ """
+ import java.util.function.Supplier;
+
+ class Test {
+ void test() {
+ String hello = "Hello";
+
+ Supplier<$1Local> supplier = () -> new $1Local(hello);
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+
+ $1Local(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ }
+
+ void print() {
+ System.out.println(val${'$'}hello);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testCaptureUsedInConstructor() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+
+ class Local {
+ Local() {
+ System.out.println(hello);
+ }
+
+ Local(String s) {
+ this();
+ System.out.println(hello);
+ }
+
+ Local(int i) {
+ this(hello);
+ }
+ }
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+
+ $1Local(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ System.out.println(val${'$'}hello);
+ }
+
+ $1Local(String s, String hello) {
+ this(hello);
+ System.out.println(val${'$'}hello);
+ }
+
+ $1Local(int i, String hello) {
+ this(hello, hello);
+ }
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun testCaptureUsedInConstructor22() {
+ doTest(
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+
+ class Local {
+ Local() {
+ System.out.println(hello);
+ }
+
+ Local(String s) {
+ this();
+ System.out.println(hello);
+ }
+
+ Local(int i) {
+ this(hello);
+ }
+ }
+ }
+ }
+ """.trimIndent(),
+ """
+ class Test {
+ void test() {
+ String hello = "Hello";
+
+ }
+
+ class $1Local {
+ final String val${'$'}hello;
+
+ $1Local(String hello) {
+ this.val${'$'}hello = hello;
+ super();
+ System.out.println(hello);
+ }
+
+ $1Local(String s, String hello) {
+ this(hello);
+ System.out.println(hello);
+ }
+
+ $1Local(int i, String hello) {
+ this(hello, hello);
+ }
+ }
+ }
+ """.trimIndent(),
+ classVersion = Opcodes.V22
+ )
+ }
+}
Index: src/test/kotlin/platform/mixin/desugar/FieldAssignmentDesugarTest.kt
===================================================================
--- src/test/kotlin/platform/mixin/desugar/FieldAssignmentDesugarTest.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/test/kotlin/platform/mixin/desugar/FieldAssignmentDesugarTest.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -23,6 +23,7 @@
import com.demonwav.mcdev.platform.mixin.handlers.desugar.FieldAssignmentDesugarer
import org.junit.jupiter.api.Test
+@Suppress("InstantiationOfUtilityClass")
class FieldAssignmentDesugarTest : AbstractDesugarTest() {
override val desugarer = FieldAssignmentDesugarer
Index: src/test/kotlin/platform/mixin/desugar/RemoveVarArgsDesugarTest.kt
===================================================================
--- src/test/kotlin/platform/mixin/desugar/RemoveVarArgsDesugarTest.kt (revision 29e926b5e11b36dac39efc95f989c947d1d5cf59)
+++ src/test/kotlin/platform/mixin/desugar/RemoveVarArgsDesugarTest.kt (revision 9c94ff3f8046988901d53dad93032951d35a48ca)
@@ -108,6 +108,7 @@
@Test
fun testGenericClass() {
+ @Suppress("unchecked")
doTest(
"""
class Test {