User: kyle wood Date: 27 Mar 25 03:12 Revision: c6aad566b60e0edc811e5e4eec3b281e67f9bc4d Summary: Merge branch '2024.3' into 2025.1 TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=9873&personal=false Index: build.gradle.kts =================================================================== --- build.gradle.kts (revision 99ced62350c3ebd1c82d03b6be852947a2add550) +++ build.gradle.kts (revision c6aad566b60e0edc811e5e4eec3b281e67f9bc4d) @@ -3,7 +3,7 @@ * * https://mcdev.io/ * - * Copyright (C) 2024 minecraft-dev + * 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 @@ -105,6 +105,7 @@ bundledPlugin("ByteCodeViewer") bundledPlugin("org.intellij.intelliLang") bundledPlugin("com.intellij.properties") + bundledPlugin("Git4Idea") bundledPlugin("com.intellij.modules.json") // Optional dependencies @@ -112,6 +113,7 @@ bundledPlugin("org.toml.lang") bundledPlugin("org.jetbrains.plugins.yaml") + testFramework(TestFrameworkType.JUnit5) testFramework(TestFrameworkType.Platform) testFramework(TestFrameworkType.Plugin.Java) Index: gradle.properties =================================================================== --- gradle.properties (revision 99ced62350c3ebd1c82d03b6be852947a2add550) +++ gradle.properties (revision c6aad566b60e0edc811e5e4eec3b281e67f9bc4d) @@ -3,7 +3,7 @@ # # https://mcdev.io/ # -# Copyright (C) 2024 minecraft-dev +# 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 Index: gradle/libs.versions.toml =================================================================== --- gradle/libs.versions.toml (revision 99ced62350c3ebd1c82d03b6be852947a2add550) +++ gradle/libs.versions.toml (revision c6aad566b60e0edc811e5e4eec3b281e67f9bc4d) @@ -7,8 +7,8 @@ fuel = "2.3.1" licenser = "0.6.1" changelog = "2.2.0" -intellij-plugin = "2.1.0" -intellij-ide = "251.14649-EAP-CANDIDATE-SNAPSHOT" +intellij-plugin = "2.4.0" +intellij-ide = "251.23774-EAP-CANDIDATE-SNAPSHOT" idea-ext = "1.1.8" psiPlugin = "251.175" @@ -31,7 +31,7 @@ coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } mappingIo = "net.fabricmc:mapping-io:0.2.1" -mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.1" +mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.4" # GrammarKit jflex-lib = "org.jetbrains.idea:jflex:1.7.0-b7f882a" Index: src/main/kotlin/platform/mixin/util/AsmUtil.kt =================================================================== --- src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision 99ced62350c3ebd1c82d03b6be852947a2add550) +++ src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision c6aad566b60e0edc811e5e4eec3b281e67f9bc4d) @@ -3,7 +3,7 @@ * * https://mcdev.io/ * - * Copyright (C) 2024 minecraft-dev + * 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 @@ -57,12 +57,15 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementFactory import com.intellij.psi.PsiEllipsisType +import com.intellij.psi.PsiEnumConstant import com.intellij.psi.PsiField import com.intellij.psi.PsiFileFactory import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiKeyword import com.intellij.psi.PsiLambdaExpression import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiMethodReferenceExpression import com.intellij.psi.PsiModifier import com.intellij.psi.PsiModifierList @@ -96,6 +99,7 @@ import org.objectweb.asm.tree.InsnList import org.objectweb.asm.tree.InsnNode import org.objectweb.asm.tree.InvokeDynamicInsnNode +import org.objectweb.asm.tree.LineNumberNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode import org.objectweb.asm.tree.VarInsnNode @@ -232,11 +236,11 @@ } val classFile = parentDir.findChild("${fqn.substringAfterLast('.')}.class") ?: return@lockedCached null - val node = ClassNode() + val node = ClassNode(Opcodes.ASM7) classFile.inputStream.use { ClassReader(it).accept(node, 0) } node } else { - val node = ClassNode() + val node = ClassNode(Opcodes.ASM7) ClassReader(bytes).accept(node, 0) node } @@ -636,9 +640,9 @@ get() = this.name == "" /** - * Finds the super() call in this method node, assuming it is a constructor + * Finds the `this()` or `super()` call in this method node, assuming it is a constructor */ -fun MethodNode.findSuperConstructorCall(): AbstractInsnNode? { +fun MethodNode.findDelegateConstructorCall(): MethodInsnNode? { val insns = instructions ?: return null var superCtorCall = insns.first var newCount = 0 @@ -646,8 +650,8 @@ if (superCtorCall.opcode == Opcodes.NEW) { newCount++ } else if (superCtorCall.opcode == Opcodes.INVOKESPECIAL) { - val methodCall = superCtorCall as MethodInsnNode - if (methodCall.name == "") { + superCtorCall as MethodInsnNode + if (superCtorCall.name == "") { if (newCount == 0) { return superCtorCall } else { @@ -660,13 +664,18 @@ return null } -private fun findContainingMethod(clazz: ClassNode, lambdaMethod: MethodNode): Pair? { +private fun findContainingMethod(clazz: ClassNode, lambdaMethod: MethodNode): Pair? { if (!lambdaMethod.hasAccess(Opcodes.ACC_SYNTHETIC)) { return null } clazz.methods?.forEach { method -> var lambdaCount = 0 + var lineNumber: Int? = null + val lambdaCountPerLine = mutableMapOf() method.instructions?.iterator()?.forEach nextInsn@{ insn -> + if (insn is LineNumberNode) { + lineNumber = insn.line + } if (insn !is InvokeDynamicInsnNode) return@nextInsn if (insn.bsm.owner != "java/lang/invoke/LambdaMetafactory") return@nextInsn val invokedMethod = when (insn.bsm.name) { @@ -691,9 +700,13 @@ } lambdaCount++ + val lambdaCountThisLine = + lineNumber?.let { lambdaCountPerLine.merge(it, 1, Int::plus) } ?: lambdaCount if (invokedMethod.name == lambdaMethod.name && invokedMethod.desc == lambdaMethod.desc) { - return@findContainingMethod method to (lambdaCount - 1) + val locationInfo = + SourceCodeLocationInfo(lambdaCount - 1, lineNumber, lambdaCountThisLine - 1) + return@findContainingMethod method to locationInfo } } } @@ -701,49 +714,50 @@ return null } -private fun findAssociatedLambda(psiClass: PsiClass, clazz: ClassNode, lambdaMethod: MethodNode): PsiElement? { +private fun findAssociatedLambda(project: Project, scope: GlobalSearchScope, clazz: ClassNode, lambdaMethod: MethodNode): PsiElement? { return RecursionManager.doPreventingRecursion(lambdaMethod, false) { val pair = findContainingMethod(clazz, lambdaMethod) ?: return@doPreventingRecursion null - val (containingMethod, index) = pair - val parent = findAssociatedLambda(psiClass, clazz, containingMethod) - ?: psiClass.findMethods(containingMethod.memberReference).firstOrNull() - ?: return@doPreventingRecursion null - var i = 0 - var result: PsiElement? = null - parent.accept( + val (containingMethod, locationInfo) = pair + val containingBodyElements = findAssociatedLambda(project, scope, clazz, containingMethod)?.let(::listOf) + ?: containingMethod.findBodyElements(clazz, project, scope).ifEmpty { return@doPreventingRecursion null } + + val psiFile = containingBodyElements.first().containingFile ?: return@doPreventingRecursion null + val matcher = locationInfo.createMatcher(psiFile) + for (bodyElement in containingBodyElements) { + bodyElement.accept( - object : JavaRecursiveElementWalkingVisitor() { - override fun visitAnonymousClass(aClass: PsiAnonymousClass) { - // skip anonymous classes - } + object : JavaRecursiveElementWalkingVisitor() { + override fun visitAnonymousClass(aClass: PsiAnonymousClass) { + // skip anonymous classes + } - override fun visitClass(aClass: PsiClass) { - // skip inner classes - } + override fun visitClass(aClass: PsiClass) { + // skip inner classes + } - override fun visitLambdaExpression(expression: PsiLambdaExpression) { + override fun visitLambdaExpression(expression: PsiLambdaExpression) { - if (i++ == index) { - result = expression + if (matcher.accept(expression)) { - stopWalking() - } - // skip walking inside the lambda - } + stopWalking() + } + // skip walking inside the lambda + } - override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) { - // walk inside the reference first, visits the qualifier first (it's first in the bytecode) - super.visitMethodReferenceExpression(expression) + override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) { + // walk inside the reference first, visits the qualifier first (it's first in the bytecode) + super.visitMethodReferenceExpression(expression) - if (expression.hasSyntheticMethod) { + if (expression.hasSyntheticMethod) { - if (i++ == index) { - result = expression + if (matcher.accept(expression)) { - stopWalking() - } - } - } - }, - ) + stopWalking() + } + } + } + }, + ) - result - } + } + + matcher.result -} + } +} fun MethodNode.cached( clazz: ClassNode, @@ -966,9 +980,70 @@ // don't walk into stub compiled elements to look for lambdas return null } - return findAssociatedLambda(psiClass, clazz, this) + return findAssociatedLambda(project, scope, clazz, this) } +fun MethodNode.findBodyElements(clazz: ClassNode, project: Project, scope: GlobalSearchScope): List { + if (isClinit) { + val psiClass = clazz.findSourceClass(project, scope, canDecompile = true) ?: return emptyList() + val result = mutableListOf() + for (element in psiClass.children) { + when (element) { + is PsiEnumConstant -> element.argumentList?.expressions?.let { result += it } + is PsiField -> { + if (element.hasModifierProperty(PsiModifier.STATIC)) { + element.initializer?.let { result += it } + } + } + is PsiClassInitializer -> { + if (element.hasModifierProperty(PsiModifier.STATIC)) { + result += element.body + } + } + } + } + return result + } + + val sourceMethod = findSourceElement(clazz, project, scope, canDecompile = true) ?: return emptyList() + + if (isConstructor && findDelegateConstructorCall()?.owner != clazz.name && sourceMethod is PsiMethod) { + val result = mutableListOf() + val body = sourceMethod.body + if (body != null) { + val children = body.children + val superCtorIndex = children.indexOfFirst { + it is PsiMethodCallExpression && it.methodExpression.text == PsiKeyword.SUPER + } + result += children.take(superCtorIndex + 1) + sourceMethod.containingClass?.children?.forEach { element -> + when (element) { + is PsiField -> { + if (!element.hasModifierProperty(PsiModifier.STATIC)) { + element.initializer?.let { result += it } + } + } + is PsiClassInitializer -> { + if (!element.hasModifierProperty(PsiModifier.STATIC)) { + result += element.body + } + } + } + } + result += children.drop(superCtorIndex + 1) + return result + } + } + + val body = when (sourceMethod) { + is PsiMethod -> sourceMethod.body + is PsiLambdaExpression -> sourceMethod.body + else -> null + } + + return listOfNotNull(body) +} + /** * Constructs a fake method node which could have been reached via this method instruction */ Index: src/main/kotlin/util/psi-utils.kt =================================================================== --- src/main/kotlin/util/psi-utils.kt (revision 99ced62350c3ebd1c82d03b6be852947a2add550) +++ src/main/kotlin/util/psi-utils.kt (revision c6aad566b60e0edc811e5e4eec3b281e67f9bc4d) @@ -3,7 +3,7 @@ * * https://mcdev.io/ * - * Copyright (C) 2024 minecraft-dev + * 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 @@ -24,7 +24,10 @@ import com.demonwav.mcdev.platform.mcp.McpModule import com.demonwav.mcdev.platform.mcp.McpModuleType import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.debugger.impl.DebuggerUtilsEx +import com.intellij.ide.highlighter.JavaClassFileType import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.editor.Document import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore @@ -41,6 +44,7 @@ import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementFactory import com.intellij.psi.PsiElementResolveResult @@ -223,6 +227,41 @@ return modifier in ACCESS_MODIFIERS } +fun PsiElement.findDocument(containingFile: PsiFile = this.containingFile): Document? { + return containingFile.viewProvider.document ?: PsiDocumentManager.getInstance(project).getDocument(containingFile) +} + +/** + * Remaps line numbers if the file is decompiled. Line numbers are 1-indexed + */ +fun PsiFile.remapLineNumber(lineNumber: Int): Int { + val originalFile = this.originalFile + if (originalFile.virtualFile?.fileType != JavaClassFileType.INSTANCE) { + // not decompiled + return lineNumber + } + + val mappedLineNumber = DebuggerUtilsEx.bytecodeToSourceLine(originalFile, lineNumber - 1) + if (mappedLineNumber < 0) { + return lineNumber + } + + return mappedLineNumber + 1 +} + +/** + * Returns the line number of the start of this `PsiElement`'s text range, with line numbers starting at 1 + */ +fun PsiElement.lineNumber(): Int? = findDocument()?.let(this::lineNumber) + +fun PsiElement.lineNumber(document: Document): Int? { + val index = this.textRange.startOffset + if (index > document.textLength) { + return null + } + return document.getLineNumber(index) + 1 +} + infix fun PsiElement.equivalentTo(other: PsiElement?): Boolean { return manager.areElementsEquivalent(this, other) }