Skip to content

Commit 08a1f4b

Browse files
committed
WIP inlay hints for mixin elements in target classes
1 parent 28a411b commit 08a1f4b

File tree

13 files changed

+871
-10
lines changed

13 files changed

+871
-10
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform
22+
23+
import com.demonwav.mcdev.util.mapFirstNotNull
24+
import com.intellij.openapi.extensions.ExtensionPointName
25+
import com.intellij.openapi.project.Project
26+
import com.intellij.openapi.roots.libraries.Library
27+
28+
interface LibraryModIdProvider {
29+
fun getModId(project: Project, library: Library): String?
30+
31+
companion object {
32+
val EP_NAME = ExtensionPointName<LibraryModIdProvider>("com.demonwav.minecraft-dev.libraryModIdProvider")
33+
34+
fun getModId(project: Project, library: Library): String? {
35+
return EP_NAME.extensionList.mapFirstNotNull { it.getModId(project, library) }
36+
}
37+
}
38+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.fabric
22+
23+
import com.demonwav.mcdev.platform.LibraryModIdProvider
24+
import com.intellij.java.library.JavaLibraryUtil
25+
import com.intellij.json.psi.JsonFile
26+
import com.intellij.json.psi.JsonObject
27+
import com.intellij.json.psi.JsonStringLiteral
28+
import com.intellij.openapi.project.Project
29+
import com.intellij.openapi.roots.OrderEnumerator
30+
import com.intellij.openapi.roots.OrderRootType
31+
import com.intellij.openapi.roots.libraries.Library
32+
import com.intellij.openapi.util.RecursionManager
33+
import com.intellij.psi.PsiManager
34+
import org.jetbrains.lang.manifest.psi.ManifestFile
35+
36+
class FabricLibraryModIdProvider : LibraryModIdProvider {
37+
override fun getModId(project: Project, library: Library): String? {
38+
val roots = library.getFiles(OrderRootType.CLASSES)
39+
40+
// try to find mod id in fabric.mod.json first
41+
for (root in roots) {
42+
val fabricModJsonFile = root.findChild("fabric.mod.json") ?: continue
43+
val fabricModJson = PsiManager.getInstance(project).findFile(fabricModJsonFile) as? JsonFile ?: continue
44+
val rootObject = fabricModJson.topLevelValue as? JsonObject ?: continue
45+
val idProperty = rootObject.findProperty("id") ?: continue
46+
val id = idProperty.value as? JsonStringLiteral ?: continue
47+
return id.value
48+
}
49+
50+
// check if we are in a split-source-set -client library, and then check for the mod id of the -common library
51+
val mavenCoords = JavaLibraryUtil.getMavenCoordinates(library) ?: return null
52+
if (!mavenCoords.artifactId.endsWith("-client")) {
53+
return null
54+
}
55+
val commonArtifactId =
56+
mavenCoords.artifactId.substring(0, mavenCoords.artifactId.length - "-client".length) + "-common"
57+
58+
for (root in roots) {
59+
val manifestFile = root.findChild("META-INF")?.findChild("MANIFEST.MF") ?: continue
60+
val manifestPsi = PsiManager.getInstance(project).findFile(manifestFile) as? ManifestFile ?: continue
61+
val environmentNameHeader = manifestPsi.getHeader("Fabric-Loom-Split-Environment-Name") ?: continue
62+
val environmentName = environmentNameHeader.headerValue?.unwrappedText ?: continue
63+
if (environmentName != "client") {
64+
continue
65+
}
66+
67+
var commonLibrary: Library? = null
68+
OrderEnumerator.orderEntries(project).forEachLibrary { lib ->
69+
val commonMavenCoords = JavaLibraryUtil.getMavenCoordinates(lib) ?: return@forEachLibrary true
70+
if (
71+
commonMavenCoords.groupId != mavenCoords.groupId ||
72+
commonMavenCoords.artifactId != commonArtifactId ||
73+
commonMavenCoords.version != mavenCoords.version
74+
) {
75+
return@forEachLibrary true
76+
}
77+
commonLibrary = lib
78+
false
79+
}
80+
81+
if (commonLibrary != null) {
82+
val commonModId = RecursionManager.doPreventingRecursion(commonLibrary!!, false) {
83+
getModId(project, commonLibrary!!)
84+
}
85+
if (commonModId != null) {
86+
return commonModId
87+
}
88+
}
89+
}
90+
91+
return null
92+
}
93+
}

src/main/kotlin/platform/mixin/handlers/InjectAnnotationHandler.kt

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package com.demonwav.mcdev.platform.mixin.handlers
2222

23+
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver
2324
import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
2425
import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
2526
import com.demonwav.mcdev.platform.mixin.util.LocalVariables
@@ -28,16 +29,37 @@ import com.demonwav.mcdev.platform.mixin.util.callbackInfoType
2829
import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType
2930
import com.demonwav.mcdev.platform.mixin.util.hasAccess
3031
import com.demonwav.mcdev.platform.mixin.util.toPsiType
32+
import com.demonwav.mcdev.util.McdevDfaUtil
3133
import com.demonwav.mcdev.util.Parameter
34+
import com.demonwav.mcdev.util.findAnnotations
3235
import com.demonwav.mcdev.util.findModule
3336
import com.demonwav.mcdev.util.firstIndexOrNull
3437
import com.intellij.psi.JavaPsiFacade
38+
import com.intellij.psi.JavaRecursiveElementWalkingVisitor
3539
import com.intellij.psi.PsiAnnotation
40+
import com.intellij.psi.PsiClass
41+
import com.intellij.psi.PsiElement
42+
import com.intellij.psi.PsiExpression
43+
import com.intellij.psi.PsiField
44+
import com.intellij.psi.PsiLambdaExpression
45+
import com.intellij.psi.PsiMember
3646
import com.intellij.psi.PsiMethod
3747
import com.intellij.psi.PsiQualifiedReference
48+
import com.intellij.psi.PsiStatement
3849
import com.intellij.psi.PsiTypes
50+
import com.intellij.psi.controlFlow.ConditionalThrowToInstruction
51+
import com.intellij.psi.controlFlow.ControlFlow
52+
import com.intellij.psi.controlFlow.ControlFlowFactory
53+
import com.intellij.psi.controlFlow.ControlFlowUtil
54+
import com.intellij.psi.controlFlow.LocalsOrMyInstanceFieldsControlFlowPolicy
55+
import com.intellij.psi.controlFlow.ReturnInstruction
56+
import com.intellij.psi.util.PsiTreeUtil
3957
import com.intellij.psi.util.parentOfType
58+
import com.intellij.psi.util.parents
59+
import com.intellij.util.takeWhileInclusive
4060
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
61+
import com.siyeh.ig.psiutils.SideEffectChecker
62+
import java.util.BitSet
4163
import org.objectweb.asm.Opcodes
4264
import org.objectweb.asm.Type
4365
import org.objectweb.asm.tree.ClassNode
@@ -134,4 +156,188 @@ class InjectAnnotationHandler : InjectorAnnotationHandler() {
134156
override val allowCoerce = true
135157

136158
override val mixinExtrasExpressionContextType = ExpressionContext.Type.INJECT
159+
160+
override fun createTargetInlay(
161+
context: MixinAnnotationHandler.TargetInlayContext
162+
): MixinAnnotationHandler.TargetInlayProperties? {
163+
val inlayProps = super.createTargetInlay(context) ?: return null
164+
val at = context.annotation.findAttributeValue("at")?.findAnnotations()?.getOrNull(context.navigationIndex)
165+
?: return null
166+
return moveInlayAcrossNonSideEffectCodeToPrettierSpot(context, AtResolver.getShift(at) > 0, inlayProps)
167+
}
168+
169+
companion object {
170+
/**
171+
* Note: this doesn't just have to check for side effects, it also has to check for whether where we're shifting
172+
* to is reached iff the injection point is reached.
173+
*/
174+
fun moveInlayAcrossNonSideEffectCodeToPrettierSpot(
175+
context: MixinAnnotationHandler.TargetInlayContext,
176+
isAfter: Boolean,
177+
inlayProps: MixinAnnotationHandler.TargetInlayProperties
178+
): MixinAnnotationHandler.TargetInlayProperties {
179+
val targetElement = context.targetElement
180+
181+
if (
182+
inlayProps.placement != MixinAnnotationHandler.TargetInlayPlacement.BEFORE &&
183+
inlayProps.placement != MixinAnnotationHandler.TargetInlayPlacement.AFTER
184+
) {
185+
return inlayProps
186+
}
187+
188+
val controlFlowBlock = McdevDfaUtil.getControlFlowContext(targetElement) ?: return inlayProps
189+
val project = controlFlowBlock.project
190+
val controlFlow = ControlFlowFactory.getInstance(project)
191+
.getControlFlow(controlFlowBlock, LocalsOrMyInstanceFieldsControlFlowPolicy.getInstance())
192+
193+
val topmostParent = targetElement.parents(withSelf = true)
194+
.takeWhile { it !is PsiClass && (it !is PsiMember || it is PsiField) && it !is PsiLambdaExpression }
195+
.firstOrNull { it is PsiStatement || it is PsiField }
196+
?: targetElement.parents(withSelf = true).takeWhile { it is PsiExpression }.lastOrNull()
197+
?: targetElement
198+
val parents = targetElement.parents(withSelf = true).takeWhileInclusive { it != topmostParent }.toList()
199+
200+
// workaround for SideEffectChecker requiring PsiExpression for all methods which take a list
201+
val sideEffects = mutableListOf<PsiElement>()
202+
topmostParent.accept(object : JavaRecursiveElementWalkingVisitor() {
203+
override fun visitElement(element: PsiElement) {
204+
if (SideEffectChecker.mayHaveSideEffects(element) { it != element }) {
205+
sideEffects += element
206+
}
207+
super.visitElement(element)
208+
}
209+
210+
override fun visitExpression(expression: PsiExpression) {
211+
SideEffectChecker.checkSideEffects(expression, sideEffects)
212+
}
213+
})
214+
val sideEffectOffsets = BitSet()
215+
for (sideEffect in sideEffects) {
216+
val offset = controlFlow.getEndOffset(sideEffect)
217+
if (offset >= 0) {
218+
sideEffectOffsets.set(offset)
219+
}
220+
}
221+
222+
var anchor = inlayProps.anchor
223+
224+
if (isAfter) {
225+
val targetOffset = controlFlow.getEndOffset(targetElement)
226+
if (targetOffset >= 0) {
227+
val afterAnchor = parents.takeWhile { parent ->
228+
if (!PsiTreeUtil.isAncestor(controlFlowBlock, parent, false)) {
229+
return@takeWhile true
230+
}
231+
val endOffset = controlFlow.getEndOffset(parent)
232+
if (endOffset < 0) {
233+
return@takeWhile true
234+
}
235+
isAlwaysReachedAndNotViaSideEffects(
236+
controlFlow,
237+
targetOffset,
238+
endOffset,
239+
sideEffectOffsets,
240+
skipFirst = true
241+
)
242+
}.lastOrNull()
243+
if (afterAnchor != null) {
244+
anchor = afterAnchor
245+
}
246+
}
247+
} else {
248+
val targetOffset = controlFlow.getStartOffset(targetElement)
249+
if (targetOffset >= 0) {
250+
val beforeAnchor = parents.takeWhile { parent ->
251+
if (!PsiTreeUtil.isAncestor(controlFlowBlock, parent, false)) {
252+
return@takeWhile true
253+
}
254+
val startOffset = controlFlow.getStartOffset(parent)
255+
if (startOffset < 0) {
256+
return@takeWhile true
257+
}
258+
isAlwaysReachedAndNotViaSideEffects(
259+
controlFlow,
260+
startOffset,
261+
targetOffset,
262+
sideEffectOffsets,
263+
skipFirst = false
264+
)
265+
}.lastOrNull()
266+
if (beforeAnchor != null) {
267+
anchor = beforeAnchor
268+
}
269+
}
270+
}
271+
272+
val placement = if (anchor is PsiStatement || anchor is PsiField) {
273+
if (isAfter) {
274+
MixinAnnotationHandler.TargetInlayPlacement.NEXT_LINE
275+
} else {
276+
MixinAnnotationHandler.TargetInlayPlacement.PREVIOUS_LINE
277+
}
278+
} else {
279+
inlayProps.placement
280+
}
281+
282+
return inlayProps.copy(anchor = anchor, placement = placement)
283+
}
284+
285+
private fun isAlwaysReachedAndNotViaSideEffects(
286+
controlFlow: ControlFlow,
287+
from: Int,
288+
to: Int,
289+
sideEffects: BitSet,
290+
skipFirst: Boolean,
291+
): Boolean {
292+
val graph = ControlFlowUtil.getEdges(controlFlow, 0).groupBy({ it.myFrom }) { it.myTo }
293+
294+
val visited = BitSet()
295+
fun checkToUnreachableNotViaFrom(index: Int): Boolean {
296+
if (index == from) {
297+
return true
298+
}
299+
if (index == to) {
300+
return false
301+
}
302+
if (visited.get(index)) {
303+
return true
304+
}
305+
visited.set(index)
306+
val successors = graph[index] ?: return true
307+
return successors.all { checkToUnreachableNotViaFrom(it) }
308+
}
309+
if (!checkToUnreachableNotViaFrom(0)) {
310+
return false
311+
}
312+
313+
visited.clear()
314+
fun dfs(index: Int): Boolean {
315+
if (index == to || visited.get(index)) {
316+
return true
317+
}
318+
if (index < 0 || index >= controlFlow.instructions.size) {
319+
return false
320+
}
321+
322+
visited.set(index)
323+
324+
val insn = controlFlow.instructions[index]
325+
val successors = if (insn is ConditionalThrowToInstruction) {
326+
listOf(index + 1)
327+
} else {
328+
graph[index] ?: emptyList()
329+
}
330+
331+
if (!skipFirst || index != from) {
332+
if (sideEffects.get(index) || insn is ReturnInstruction) {
333+
return false
334+
}
335+
}
336+
337+
return successors.all { dfs(it) }
338+
}
339+
340+
return dfs(from)
341+
}
342+
}
137343
}

src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler {
171171
return "Cannot resolve any target instructions in target class"
172172
}
173173

174+
override fun createTargetInlay(
175+
context: MixinAnnotationHandler.TargetInlayContext
176+
): MixinAnnotationHandler.TargetInlayProperties? {
177+
val at = context.annotation.findAttributeValue(getAtKey(context.annotation))
178+
?.findAnnotations()
179+
?.getOrNull(context.navigationIndex)
180+
?: return null
181+
return AtResolver.getInjectionPoint(at)?.createTargetInlay(at, context)
182+
}
183+
174184
open val allowCoerce = false
175185

176186
override val isEntryPoint = true

0 commit comments

Comments
 (0)