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 {