User: llamalad7 Date: 25 May 25 11:26 Revision: a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf Summary: New: Support injection point specifiers everywhere. This includes several related things like: - Rework `CollectVisitor`s to work with `Sequence`s rather than having a special case for only wanting the first result - Rework `CollectResultFilter`s to filter based on the whole sequence (this is needed to support `:LAST`) - Offer proper completion options for specifiers in the right context - Fix `At#value` completions disappearing after typing `:` TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=10026&personal=false Index: src/main/kotlin/platform/mixin/completion/InjectionPointTypedHandlerDelegate.kt =================================================================== --- src/main/kotlin/platform/mixin/completion/InjectionPointTypedHandlerDelegate.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) +++ src/main/kotlin/platform/mixin/completion/InjectionPointTypedHandlerDelegate.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -0,0 +1,44 @@ +/* + * 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.completion + +import com.demonwav.mcdev.platform.mixin.reference.InjectionPointReference +import com.demonwav.mcdev.util.reference.findContextElement +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInsight.editorActions.TypedHandlerDelegate +import com.intellij.lang.java.JavaLanguage +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile + +class InjectionPointTypedHandlerDelegate : TypedHandlerDelegate() { + override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result { + if (charTyped != ':' || !file.language.isKindOf(JavaLanguage.INSTANCE)) { + return Result.CONTINUE + } + AutoPopupController.getInstance(project).autoPopupMemberLookup(editor) { + val offset = editor.caretModel.offset + val element = it.findElementAt(offset)?.findContextElement() + InjectionPointReference.ELEMENT_PATTERN.accepts(element) + } + return Result.CONTINUE + } +} Index: src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -221,7 +221,7 @@ val filteredLocals = localInfo.matchLocals( module, targetClass, targetMethod, actualInsn, - CollectVisitor.Mode.MATCH_ALL + CollectVisitor.Mode.RESOLUTION ) ?: return@addMember false filteredLocals.any { it.index == virtualInsn.`var` } } Index: src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -141,7 +141,7 @@ annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - mode: CollectVisitor.Mode = CollectVisitor.Mode.MATCH_ALL, + mode: CollectVisitor.Mode = CollectVisitor.Mode.RESOLUTION, ): List> { val cache = annotation.cached(PsiModificationTracker.MODIFICATION_COUNT) { ConcurrentHashMap, List>>() Index: src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -48,7 +48,7 @@ val at = annotation.findAttributeValue("at") as? PsiAnnotation val atCode = at?.findAttributeValue("value")?.constantStringValue val isLoadStore = atCode != null && InjectionPoint.byAtCode(atCode) is AbstractLoadInjectionPoint - val mode = if (isLoadStore) CollectVisitor.Mode.COMPLETION else CollectVisitor.Mode.MATCH_ALL + val mode = if (isLoadStore) CollectVisitor.Mode.COMPLETION else CollectVisitor.Mode.RESOLUTION val targets = resolveInstructions(annotation, targetClass, targetMethod, mode) val targetParamsGroup = ParameterGroup( Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -25,7 +25,7 @@ import com.demonwav.mcdev.platform.mixin.reference.isMiscDynamicSelector import com.demonwav.mcdev.platform.mixin.reference.parseMixinSelector import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE +import com.demonwav.mcdev.platform.mixin.util.InjectionPointSpecifier import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.SHIFT import com.demonwav.mcdev.platform.mixin.util.findSourceClass import com.demonwav.mcdev.platform.mixin.util.findSourceElement @@ -55,7 +55,6 @@ import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiUtil -import com.intellij.psi.util.parentOfType import com.intellij.psi.util.parents import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -92,10 +91,9 @@ var atCode = at.qualifiedName?.let { InjectionPointAnnotation.atCodeFor(it) } ?: at.findDeclaredAttributeValue("value")?.constantStringValue ?: return null - // remove slice selector - val isInSlice = at.parentOfType()?.hasQualifiedName(SLICE) ?: false - if (isInSlice) { - if (SliceSelector.entries.any { atCode.endsWith(":${it.name}") }) { + // remove specifier + if (InjectionPointSpecifier.isAllowed(at)) { + if (InjectionPointSpecifier.entries.any { atCode.endsWith(":${it.name}") }) { atCode = atCode.substringBeforeLast(':') } } @@ -181,7 +179,7 @@ at, target, getTargetClass(target), - CollectVisitor.Mode.MATCH_FIRST, + CollectVisitor.Mode.RESOLUTION, ) if (collectVisitor == null) { // syntax error in target @@ -192,33 +190,25 @@ InsnResolutionInfo.Failure() } } - collectVisitor.visit(targetMethod) - return if (collectVisitor.result.isEmpty()) { - InsnResolutionInfo.Failure(collectVisitor.filterToBlame) - } else { - null + return collectVisitor.visit(targetMethod) as? InsnResolutionInfo.Failure - } + } - } - fun resolveInstructions(mode: CollectVisitor.Mode = CollectVisitor.Mode.MATCH_ALL): List> { - return (getInstructionResolutionInfo(mode) as? InsnResolutionInfo.Success)?.results ?: emptyList() + fun resolveInstructions( + mode: CollectVisitor.Mode = CollectVisitor.Mode.RESOLUTION, + ): Sequence> { + return getInstructionResolutionInfo(mode).results } - fun getInstructionResolutionInfo(mode: CollectVisitor.Mode = CollectVisitor.Mode.MATCH_ALL): InsnResolutionInfo { + fun getInstructionResolutionInfo(mode: CollectVisitor.Mode = CollectVisitor.Mode.RESOLUTION): InsnResolutionInfo<*> { val injectionPoint = getInjectionPoint(at) ?: return InsnResolutionInfo.Failure() val targetAttr = at.findAttributeValue("target") val target = targetAttr?.let { parseMixinSelector(it) } val collectVisitor = injectionPoint.createCollectVisitor(at, target, getTargetClass(target), mode) ?: return InsnResolutionInfo.Failure() - collectVisitor.visit(targetMethod) - val result = collectVisitor.result - return if (result.isEmpty()) { - InsnResolutionInfo.Failure(collectVisitor.filterToBlame) - } else { - InsnResolutionInfo.Success(result) + + return collectVisitor.visit(targetMethod) - } + } - } fun resolveNavigationTargets(): List { // First resolve the actual target in the bytecode using the collect visitor @@ -277,6 +267,7 @@ sourceResults.forEach(matcher::accept) matcher.result } + .toList() } fun collectTargetVariants(completionHandler: (LookupElementBuilder) -> LookupElementBuilder): List { @@ -291,11 +282,13 @@ CollectVisitor.Mode.COMPLETION ) ?: return emptyList() - visitor.visit(targetMethod) - return visitor.result + + return visitor.visit(targetMethod) + .results .mapNotNull { result -> injectionPoint.createLookup(getTargetClass(target), result)?.let { completionHandler(it) } } + .toList() } return doCollectVariants(injectionPoint) } @@ -305,23 +298,19 @@ } } -sealed class InsnResolutionInfo { - class Success(val results: List>) : InsnResolutionInfo() - class Failure(val filterToBlame: String? = null) : InsnResolutionInfo() { +sealed class InsnResolutionInfo(val results: Sequence>) { + class Success(results: Sequence>) : InsnResolutionInfo(results) + class Failure(val filterStats: Map = emptyMap()) : InsnResolutionInfo(emptySequence()) { infix fun combine(other: Failure): Failure { - return if (filterToBlame != null) { - this - } else { - other + val result = LinkedHashMap(this.filterStats) + for ((key, value) in other.filterStats) { + result[key] = (result[key] ?: 0) + value } + return Failure(result) } } } -enum class SliceSelector { - FIRST, LAST, ONE -} - object QualifiedMember { fun resolveQualifier(reference: PsiQualifiedReference): PsiClass? { val qualifier = reference.qualifier ?: return null Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -310,15 +310,15 @@ private val constantInfo: ConstantInfo?, private val expectedType: Type? = null, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - methodNode.instructions?.iterator()?.forEachRemaining { insn -> + override fun accept(methodNode: MethodNode) = sequence { + for (insn in methodNode.instructions ?: emptyList()) { val constant = ( insn.computeConstantValue(constantInfo?.expandConditions ?: emptySet()) - ?: return@forEachRemaining + ?: continue ).let { if (it is NullSentinel) null else it } if (constantInfo != null && constant != constantInfo.constant) { - return@forEachRemaining + continue } if (expectedType != null && constant != null) { @@ -328,7 +328,7 @@ expectedType.className != CommonClassNames.JAVA_LANG_STRING ) ) { - return@forEachRemaining + continue } // then check if we expect any class literal @@ -337,14 +337,14 @@ expectedType.className != CommonClassNames.JAVA_LANG_CLASS ) ) { - return@forEachRemaining + continue } // otherwise we expect a primitive literal if (expectedType.sort in Type.BOOLEAN..Type.DOUBLE && constant::class.javaPrimitiveType?.let(Type::getType) != expectedType ) { - return@forEachRemaining + continue } } Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -204,10 +204,10 @@ private val selector: MixinSelector, private val ldc: String?, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence var seenStringConstant: String? = null - insns.iterator().forEachRemaining { insn -> + for (insn in insns) { if (insn is MethodInsnNode) { // make sure we're coming from a string constant if (seenStringConstant != null) { @@ -225,7 +225,7 @@ } } - private fun processMethodInsn(insn: MethodInsnNode) { + private suspend fun SequenceScope>.processMethodInsn(insn: MethodInsnNode) { // must take a string and return void if (insn.desc != "(Ljava/lang/String;)V") return Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -124,23 +124,24 @@ mode: Mode, private val enforce: EnforceMode, ) : HeadInjectionPoint.MyCollectVisitor(project, clazz, mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence if (!methodNode.isConstructor) { - super.accept(methodNode) - return + yieldAll(super.accept(methodNode)) + return@sequence } - val delegateCtorCall = methodNode.findDelegateConstructorCall() ?: run { - super.accept(methodNode) - return + val delegateCtorCall = methodNode.findDelegateConstructorCall() + if (delegateCtorCall == null) { + yieldAll(super.accept(methodNode)) + return@sequence } if (enforce == EnforceMode.POST_DELEGATE) { - val insn = delegateCtorCall.next ?: return + val insn = delegateCtorCall.next ?: return@sequence addResult(insn, methodNode.findOrConstructSourceMethod(clazz, project)) - return + return@sequence } // Although Mumfrey's original intention was to target the last *unique* field store, @@ -155,7 +156,7 @@ (insn as FieldInsnNode).owner == clazz.name } ?: delegateCtorCall - val lastFieldStoreNext = lastFieldStore.next ?: return + val lastFieldStoreNext = lastFieldStore.next ?: return@sequence addResult(lastFieldStoreNext, methodNode.findOrConstructSourceMethod(clazz, project)) } } Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -196,23 +196,23 @@ private val arrayAccess: ArrayAccessType?, private val fuzz: Int, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return - insns.iterator().forEachRemaining { insn -> - if (insn !is FieldInsnNode) return@forEachRemaining + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence + for (insn in insns) { + if (insn !is FieldInsnNode) continue if (mode != Mode.COMPLETION) { if (opcode != -1 && opcode != insn.opcode) { - return@forEachRemaining + continue } if (!selector.matchField(insn.owner, insn.name, insn.desc)) { - return@forEachRemaining + continue } } val actualInsn = if (arrayAccess == null) { insn } else { findArrayInsn(insn, arrayAccess) - } ?: return@forEachRemaining + } ?: continue val fieldNode = insn.fakeResolve() val psiField = fieldNode.field.findOrConstructSourceField( fieldNode.clazz, Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/HeadInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/HeadInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/HeadInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -62,9 +62,9 @@ protected val clazz: ClassNode, mode: Mode, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return - val firstInsn = Iterable { insns.iterator() }.firstOrNull { it.opcode >= 0 } ?: return + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence + val firstInsn = insns.firstOrNull { it.opcode >= 0 } ?: return@sequence addResult(firstInsn, methodNode.findOrConstructSourceMethod(clazz, project)) } } Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -22,6 +22,8 @@ import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.reference.toMixinString +import com.demonwav.mcdev.platform.mixin.util.InjectionPointSpecifier +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE import com.demonwav.mcdev.platform.mixin.util.SourceCodeLocationInfo import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod @@ -32,6 +34,7 @@ import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.getQualifiedMemberReference import com.demonwav.mcdev.util.internalName +import com.demonwav.mcdev.util.memoized import com.demonwav.mcdev.util.realName import com.demonwav.mcdev.util.shortName import com.intellij.codeInsight.completion.JavaLookupElementBuilder @@ -50,17 +53,16 @@ import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod -import com.intellij.psi.PsiMethodReferenceExpression import com.intellij.psi.PsiSubstitutor import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.util.parentOfType import com.intellij.serviceContainer.BaseKeyedLazyInstance import com.intellij.util.ArrayUtilRt import com.intellij.util.KeyedLazyInstance +import com.intellij.util.containers.sequenceOfNotNull import com.intellij.util.xmlb.annotations.Attribute import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode -import org.objectweb.asm.tree.InsnList import org.objectweb.asm.tree.LineNumberNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode @@ -128,26 +130,39 @@ mode: CollectVisitor.Mode, ): CollectVisitor? { return doCreateCollectVisitor(at, target, targetClass, mode)?.also { - addFilters(at, targetClass, it) + val isInsideSlice = at.parentOfType()?.hasQualifiedName(SLICE) == true + val defaultSpecifier = if (isInsideSlice) InjectionPointSpecifier.FIRST else InjectionPointSpecifier.ALL + addFilters(at, targetClass, it, defaultSpecifier) } } - protected open fun addFilters(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor) { - addStandardFilters(at, targetClass, collectVisitor) + protected open fun addFilters( + at: PsiAnnotation, + targetClass: ClassNode, + collectVisitor: CollectVisitor, + defaultSpecifier: InjectionPointSpecifier + ) { + addStandardFilters(at, targetClass, collectVisitor, defaultSpecifier) } - fun addStandardFilters(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor<*>) { + fun addStandardFilters( + at: PsiAnnotation, + targetClass: ClassNode, + collectVisitor: CollectVisitor, + defaultSpecifier: InjectionPointSpecifier + ) { addShiftSupport(at, targetClass, collectVisitor) addSliceFilter(at, targetClass, collectVisitor) // make sure the ordinal filter is last, so that the ordinal only increments once the other filters have passed addOrdinalFilter(at, targetClass, collectVisitor) + addSpecifierFilter(at, targetClass, collectVisitor, defaultSpecifier) } protected open fun addShiftSupport(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor<*>) { collectVisitor.shiftBy = AtResolver.getShift(at) } - protected open fun addSliceFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor<*>) { + protected open fun addSliceFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor) { // resolve slice annotation, take into account slice id if present val sliceId = at.findDeclaredAttributeValue("slice")?.constantStringValue val parentAnnotation = at.parentOfType() ?: return @@ -168,54 +183,52 @@ if (from == null && to == null) { return } - val fromSelector = from?.findDeclaredAttributeValue("value")?.constantStringValue?.let { atCode -> - SliceSelector.values().firstOrNull { atCode.endsWith(":${it.name}") } - } ?: SliceSelector.FIRST - val toSelector = to?.findDeclaredAttributeValue("value")?.constantStringValue?.let { atCode -> - SliceSelector.values().firstOrNull { atCode.endsWith(":${it.name}") } - } ?: SliceSelector.FIRST fun resolveSliceIndex( sliceAt: PsiAnnotation?, - selector: SliceSelector, - insns: InsnList, method: MethodNode, ): Int? { return sliceAt?.let { - val results = AtResolver(sliceAt, targetClass, method).resolveInstructions() - val insn = if (selector == SliceSelector.LAST) { - results.lastOrNull()?.insn - } else { - results.firstOrNull()?.insn + AtResolver(sliceAt, targetClass, method).resolveInstructions() + .singleOrNull() + ?.let { method.instructions.indexOf(it.insn) } - } + } - insn?.let { insns.indexOf(it) } - } + } - } - // allocate lazy indexes so we don't have to re-run the at resolver for the slices each time - var fromInsnIndex: Int? = null - var toInsnIndex: Int? = null - - collectVisitor.addResultFilter("slice") { result, method -> - val insns = method.instructions ?: return@addResultFilter true - if (fromInsnIndex == null) { - fromInsnIndex = resolveSliceIndex(from, fromSelector, insns, method) ?: 0 + collectVisitor.addResultFilter("slice") { results, method -> + val insns = method.instructions + val fromInsnIndex = resolveSliceIndex(from, method) ?: 0 + val toInsnIndex = resolveSliceIndex(to, method) ?: insns.size() + results.filter { insns.indexOf(it.insn) in fromInsnIndex.. toInsnIndex } - } + } - if (toInsnIndex == null) { - toInsnIndex = resolveSliceIndex(to, toSelector, insns, method) ?: insns.size() - } + } - insns.indexOf(result.insn) in fromInsnIndex!!..toInsnIndex!! + protected open fun addOrdinalFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor) { + val ordinal = at.findDeclaredAttributeValue("ordinal")?.constantValue as? Int ?: return + if (ordinal < 0) return + collectVisitor.addResultFilter("ordinal") { results, _ -> + results.drop(ordinal).take(1) } } - protected open fun addOrdinalFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor<*>) { - val ordinal = at.findDeclaredAttributeValue("ordinal")?.constantValue as? Int ?: return - if (ordinal < 0) return - collectVisitor.addResultFilter("ordinal") { _, _ -> - collectVisitor.ordinal++ == ordinal + protected open fun addSpecifierFilter( + at: PsiAnnotation, + targetClass: ClassNode, + collectVisitor: CollectVisitor, + defaultSpecifier: InjectionPointSpecifier + ) { + val point = at.findDeclaredAttributeValue("value")?.constantStringValue ?: return + val specifier = InjectionPointSpecifier.entries.firstOrNull { point.endsWith(":$it") } ?: defaultSpecifier + collectVisitor.addResultFilter("specifier") { results, _ -> + val single = when (specifier) { + InjectionPointSpecifier.FIRST -> results.firstOrNull() + InjectionPointSpecifier.LAST -> results.lastOrNull() + InjectionPointSpecifier.ONE -> results.singleOrNull() + InjectionPointSpecifier.ALL -> return@addResultFilter results - } + } + sequenceOfNotNull(single) - } + } + } abstract fun createLookup(targetClass: ClassNode, result: CollectVisitor.Result): LookupElementBuilder? @@ -334,31 +347,35 @@ } abstract class CollectVisitor(protected val mode: Mode) { - fun visit(methodNode: MethodNode) { - this.method = methodNode - try { - accept(methodNode) - } catch (e: StopWalkingException) { - // ignore + fun visit(methodNode: MethodNode): InsnResolutionInfo { + val numRetained = IntArray(resultFilters.size + 1) + var results = accept(methodNode).onEach { numRetained[0]++ } + for ((i, filter) in resultFilters.asSequence().map { it.second }.withIndex()) { + results = filter(results, methodNode).onEach { numRetained[i + 1]++ } } + results = results.memoized() + if (results.iterator().hasNext()) { + return InsnResolutionInfo.Success(results) - } + } + val filterStats = resultFilters.asSequence() + .map { it.first } + .zip(numRetained.asSequence().zipWithNext(Int::minus)) + .toMap() + return InsnResolutionInfo.Failure(filterStats) + } fun addResultFilter(name: String, filter: CollectResultFilter) { resultFilters += name to filter } - protected abstract fun accept(methodNode: MethodNode) + protected abstract fun accept(methodNode: MethodNode): Sequence> - private lateinit var method: MethodNode private var nextIndex = 0 private val nextIndexByLine = mutableMapOf() - val result = mutableListOf>() private val resultFilters = mutableListOf>>() - var filterToBlame: String? = null - internal var ordinal = 0 internal var shiftBy = 0 - protected fun addResult( + protected suspend fun SequenceScope>.addResult( insn: AbstractInsnNode, element: T, qualifier: String? = null, @@ -395,23 +412,8 @@ if (insn === shiftedInsn) decorations else emptyMap() ) - var isFiltered = false - for ((name, filter) in resultFilters) { - if (!filter(result, method)) { - isFiltered = true - if (filterToBlame == null) { - filterToBlame = name + yield(result) - } + } - break - } - } - if (!isFiltered) { - this.result.add(result) - if (mode == Mode.MATCH_FIRST) { - stopWalking() - } - } - } private fun getLineNumber(insn: AbstractInsnNode): Int? { var i: AbstractInsnNode? = insn @@ -425,18 +427,7 @@ return null } - @Suppress("MemberVisibilityCanBePrivate") - protected fun stopWalking() { - throw StopWalkingException() - } - - private class StopWalkingException : Exception() { - override fun fillInStackTrace(): Throwable { - return this - } - } - - data class Result( + data class Result( val sourceLocationInfo: SourceCodeLocationInfo, val originalInsn: AbstractInsnNode, val insn: AbstractInsnNode, @@ -447,7 +438,7 @@ val index: Int get() = sourceLocationInfo.index } - enum class Mode { MATCH_ALL, MATCH_FIRST, COMPLETION } + enum class Mode { RESOLUTION, COMPLETION } } fun nodeMatchesSelector( @@ -471,4 +462,5 @@ ) } -typealias CollectResultFilter = (CollectVisitor.Result, MethodNode) -> Boolean +typealias CollectResultFilter = + (Sequence>, MethodNode) -> Sequence> Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeAssignInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -162,14 +162,14 @@ private val fuzz: Int, private val skip: Set, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return - insns.iterator().forEachRemaining { insn -> + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence + for (insn in insns) { if (insn !is MethodInsnNode) { - return@forEachRemaining + continue } - val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: return@forEachRemaining + val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: continue val offset = insns.indexOf(insn) val maxOffset = (offset + fuzz + 1).coerceAtMost(insns.size()) Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -153,14 +153,14 @@ private val project: Project, private val selector: MixinSelector, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return - insns.iterator().forEachRemaining { insn -> + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence + for (insn in insns) { if (insn !is MethodInsnNode) { - return@forEachRemaining + continue } - val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: return@forEachRemaining + val sourceMethod = nodeMatchesSelector(insn, mode, selector, project) ?: continue addResult( insn, sourceMethod, Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -92,9 +92,9 @@ mode: Mode, private val opcode: Int ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return - insns.iterator().forEachRemaining { insn -> + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence + for (insn in insns) { if (insn is JumpInsnNode && (opcode == -1 || insn.opcode == opcode)) { addResult(insn, methodNode.findOrConstructSourceMethod(clazz, project)) } Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -112,31 +112,33 @@ return null } - override fun addOrdinalFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor<*>) { + override fun addOrdinalFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor) { val ordinal = at.findDeclaredAttributeValue("ordinal")?.constantValue as? Int ?: return if (ordinal < 0) return // Replace the ordinal filter with one that takes into account the type of the local variable being modified. // Fixes otherwise incorrect results for completion. + collectVisitor.addResultFilter("ordinal") { results, method -> - val project = at.project - val ordinals = mutableMapOf() + val project = at.project + val ordinals = mutableMapOf() - collectVisitor.addResultFilter("ordinal") { result, method -> + results.filter { result -> - // store returns the instruction after the variable - val varInsn = (if (store) result.originalInsn.previous ?: result.originalInsn else result.originalInsn) - as? VarInsnNode ?: throw IllegalStateException("AbstractLoadInjectionPoint returned non-var insn") - val localType = AsmDfaUtil.getLocalVariableType( - project, - targetClass, - method, - result.originalInsn, - varInsn.`var`, + // store returns the instruction after the variable + val varInsn = (if (store) result.originalInsn.previous ?: result.originalInsn else result.originalInsn) + as? VarInsnNode ?: throw IllegalStateException("AbstractLoadInjectionPoint returned non-var insn") + val localType = AsmDfaUtil.getLocalVariableType( + project, + targetClass, + method, + result.originalInsn, + varInsn.`var`, - ) ?: return@addResultFilter true + ) ?: return@filter true - val desc = localType.descriptor - val ord = ordinals[desc] ?: 0 - ordinals[desc] = ord + 1 - ord == ordinal - } - } + val desc = localType.descriptor + val ord = ordinals[desc] ?: 0 + ordinals[desc] = ord + 1 + ord == ordinal + } + } + } private class MyNavigationVisitor( private val info: LocalInfo, @@ -271,7 +273,7 @@ private val info: LocalInfo, private val store: Boolean, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { + override fun accept(methodNode: MethodNode) = sequence { var opcode = when (info.type) { null -> null !is PsiPrimitiveType -> Opcodes.ALOAD Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -162,14 +162,14 @@ private val project: Project, private val selector: MixinSelector, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return - insns.iterator().forEachRemaining { insn -> - if (insn !is TypeInsnNode) return@forEachRemaining - if (insn.opcode != Opcodes.NEW) return@forEachRemaining - val initCall = findInitCall(insn) ?: return@forEachRemaining + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence + for (insn in insns) { + if (insn !is TypeInsnNode) continue + if (insn.opcode != Opcodes.NEW) continue + val initCall = findInitCall(insn) ?: continue - val sourceMethod = nodeMatchesSelector(initCall, mode, selector, project) ?: return@forEachRemaining + val sourceMethod = nodeMatchesSelector(initCall, mode, selector, project) ?: continue addResult( insn, sourceMethod, Index: src/main/kotlin/platform/mixin/handlers/injectionPoint/ReturnInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/injectionPoint/ReturnInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/injectionPoint/ReturnInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -133,10 +133,10 @@ mode: Mode, private val tailOnly: Boolean, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence val elementFactory = JavaPsiFacade.getElementFactory(project) - fun insnHandler(insn: AbstractInsnNode): Boolean { + suspend fun SequenceScope>.insnHandler(insn: AbstractInsnNode): Boolean { if (insn.opcode !in Opcodes.IRETURN..Opcodes.RETURN) { return false } @@ -160,11 +160,13 @@ insn = insn.previous } } else { - insns.iterator().forEach(::insnHandler) + for (insn in insns) { + insnHandler(insn) - } - } - } -} + } + } + } + } +} class ReturnInjectionPoint : AbstractReturnInjectionPoint(false) class TailInjectionPoint : AbstractReturnInjectionPoint(true) Index: src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt =================================================================== --- src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -219,11 +219,11 @@ private val poolFactory: IdentifierPoolFactory, private val contextType: ExpressionContext.Type, ) : CollectVisitor(mode) { - override fun accept(methodNode: MethodNode) { - val insns = methodNode.instructions ?: return + override fun accept(methodNode: MethodNode) = sequence { + val insns = methodNode.instructions ?: return@sequence val pool = poolFactory(methodNode) - val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, methodNode) ?: return + val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, methodNode) ?: return@sequence val result = IdentityHashMap>>() @@ -247,7 +247,7 @@ } if (result.isEmpty()) { - return + return@sequence } for (insn in insns) { Index: src/main/kotlin/platform/mixin/inspection/MixinAnnotationTargetInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/MixinAnnotationTargetInspection.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/inspection/MixinAnnotationTargetInspection.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -83,13 +83,26 @@ private fun addProblem(annotation: PsiAnnotation, message: String, failure: InsnResolutionInfo.Failure) { val nameElement = annotation.nameReferenceElement ?: return - var betterMessage = message - if (failure.filterToBlame != null) { - betterMessage += " (filtered out by ${failure.filterToBlame})" - } + val betterMessage = addFilterMessage(message, failure.filterStats) val quickFix = RemoveAnnotationQuickFix(annotation, annotation.parentOfType()) - holder.registerProblem(nameElement, message, quickFix) + holder.registerProblem(nameElement, betterMessage, quickFix) } + + private fun addFilterMessage(message: String, stats: Map): String { + if (stats.isEmpty()) { + return message - } + } + return buildString { + append(message) + append(" (") + val it = stats.entries.asSequence().filter { it.value != 0 }.iterator() + append(it.next().let { (k, v) -> "$v filtered out by $k" }) + for ((k, v) in it) { + append(", $v more by $k") - } + } + append(')') -} + } + } + } + } +} Index: src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -61,7 +61,7 @@ for (target in targets) { val matchingLocals = localInfo.matchLocals( module, target.method.clazz, target.method.method, target.result.insn, - CollectVisitor.Mode.MATCH_ALL + CollectVisitor.Mode.RESOLUTION ) ?: continue if (matchingLocals.size != 1) { holder.registerProblem( Index: src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt =================================================================== --- src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -23,23 +23,27 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT_CODE -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.INJECTION_POINT -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.SELECTOR +import com.demonwav.mcdev.platform.mixin.util.InjectionPointSpecifier import com.demonwav.mcdev.util.cached import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.insideAnnotationAttribute import com.demonwav.mcdev.util.reference.ReferenceResolver import com.demonwav.mcdev.util.reference.completeToLiteral import com.demonwav.mcdev.util.toTypedArray +import com.intellij.codeInsight.completion.CompletionUtil import com.intellij.codeInsight.completion.PrioritizedLookupElement import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key +import com.intellij.patterns.ElementPattern +import com.intellij.patterns.PsiJavaPatterns +import com.intellij.patterns.StandardPatterns import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement -import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiLiteral import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.searches.AnnotatedElementsSearch import com.intellij.psi.search.searches.ClassInheritorsSearch @@ -51,6 +55,8 @@ import com.intellij.psi.util.parentOfType object InjectionPointReference : ReferenceResolver(), MixinReference { + val ELEMENT_PATTERN: ElementPattern = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) + .insideAnnotationAttribute(AT) override val description: String get() = "injection point type '%s'" @@ -60,11 +66,9 @@ override fun resolveReference(context: PsiElement): PsiElement? { // Remove slice selectors from the injection point type var name = context.constantStringValue ?: return null - val at = context.parentOfType() ?: return null - val isInsideSlice = at.parentOfType()?.hasQualifiedName(SLICE) == true - if (isInsideSlice) { - for (sliceSelector in getSliceSelectors(context.project)) { - if (name.endsWith(":$sliceSelector")) { + if (allowSpecifiers(context)) { + for (specifier in InjectionPointSpecifier.entries) { + if (name.endsWith(":$specifier")) { name = name.substringBeforeLast(':') break } @@ -85,8 +89,16 @@ } override fun collectVariants(context: PsiElement): Array { + val atCodes = getAllAtCodes(context.project).keys + val text = context.constantStringValue?.removeSuffix(CompletionUtil.DUMMY_IDENTIFIER) + val prefix = text?.substringBeforeLast(':', missingDelimiterValue = "") + if (prefix != null && prefix in atCodes && allowSpecifiers(context)) { + return InjectionPointSpecifier.entries.asSequence() + .map { LookupElementBuilder.create("$prefix:$it") } + .toTypedArray() + } return ( - getAllAtCodes(context.project).keys.asSequence() + atCodes.asSequence() .filter { InjectionPoint.byAtCode(it)?.discouragedMessage == null } @@ -106,6 +118,11 @@ ).toTypedArray() } + private fun allowSpecifiers(context: PsiElement): Boolean { + val at = context.parentOfType() ?: return false + return InjectionPointSpecifier.isAllowed(at) + } + private fun LookupElementBuilder.completeInjectionPoint(context: PsiElement): LookupElementBuilder { val injectionPoint = InjectionPoint.byAtCode(lookupString) ?: return completeToLiteral(context) @@ -114,26 +131,6 @@ } } - private val SLICE_SELECTORS_KEY = Key>>("mcdev.sliceSelectors") - - private fun getSliceSelectors(project: Project): List { - return CachedValuesManager.getManager(project).getCachedValue( - project, - SLICE_SELECTORS_KEY, - { - val selectorClass = JavaPsiFacade.getInstance(project) - .findClass(SELECTOR, GlobalSearchScope.allScope(project)) - ?: return@getCachedValue CachedValueProvider.Result( - emptyList(), - PsiModificationTracker.MODIFICATION_COUNT, - ) - val enumConstants = selectorClass.fields.mapNotNull { (it as? PsiEnumConstant)?.name } - CachedValueProvider.Result(enumConstants, PsiModificationTracker.MODIFICATION_COUNT) - }, - false, - ) - } - private val INJECTION_POINT_INHERITORS = Key>>("mcdev.injectionPointInheritors") private fun getCustomInjectionPointInheritors(project: Project): List { Index: src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt =================================================================== --- src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -47,8 +47,7 @@ // Injection point types registrar.registerReferenceProvider( - PsiJavaPatterns.psiLiteral(StandardPatterns.string()) - .insideAnnotationAttribute(AT), + InjectionPointReference.ELEMENT_PATTERN, InjectionPointReference, ) Index: src/main/kotlin/platform/mixin/reference/target/TargetReference.kt =================================================================== --- src/main/kotlin/platform/mixin/reference/target/TargetReference.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/reference/target/TargetReference.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -90,7 +90,7 @@ return targets.all { val failure = AtResolver(at, it.clazz, it.method).isUnresolved() // leave it if there is a filter to blame, the target reference was at least resolved - failure != null && failure.filterToBlame == null + failure != null && failure.filterStats.isEmpty() } } Index: src/main/kotlin/platform/mixin/util/InjectionPointSpecifier.kt =================================================================== --- src/main/kotlin/platform/mixin/util/InjectionPointSpecifier.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) +++ src/main/kotlin/platform/mixin/util/InjectionPointSpecifier.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -0,0 +1,48 @@ +/* + * 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.util + +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.SPECIFIER +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.parentOfType + +enum class InjectionPointSpecifier { + FIRST, LAST, ONE, ALL; + + companion object { + fun isAllowed(at: PsiAnnotation): Boolean { + if (isAllowedOutsideSlice(at.project)) { + return true + } + val isInsideSlice = at.parentOfType()?.hasQualifiedName(SLICE) == true + return isInsideSlice + } + + private fun isAllowedOutsideSlice(project: Project): Boolean = + (JavaPsiFacade.getInstance(project) + .findClass(SPECIFIER, GlobalSearchScope.allScope(project)) + != null) + } +} Index: src/main/kotlin/platform/mixin/util/MixinConstants.kt =================================================================== --- src/main/kotlin/platform/mixin/util/MixinConstants.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/platform/mixin/util/MixinConstants.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -37,7 +37,7 @@ const val COMPATIBILITY_LEVEL = "org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel" const val CONSTANT_CONDITION = "org.spongepowered.asm.mixin.injection.Constant.Condition" const val INJECTION_POINT = "org.spongepowered.asm.mixin.injection.InjectionPoint" - const val SELECTOR = "org.spongepowered.asm.mixin.injection.InjectionPoint.Selector" + const val SPECIFIER = "org.spongepowered.asm.mixin.injection.InjectionPoint.Specifier" const val TARGET_SELECTOR = "org.spongepowered.asm.mixin.injection.selectors.TargetSelector" const val MIXIN_AGENT = "org.spongepowered.tools.agent.MixinAgent" const val MIXIN_CONFIG = "org.spongepowered.asm.mixin.transformer.MixinConfig" Index: src/main/kotlin/util/reference/ReferenceResolver.kt =================================================================== --- src/main/kotlin/util/reference/ReferenceResolver.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/util/reference/ReferenceResolver.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -105,7 +105,7 @@ } } -private fun PsiElement.findContextElement(): PsiElement { +fun PsiElement.findContextElement(): PsiElement { var current: PsiElement var parent = this Index: src/main/kotlin/util/sequences.kt =================================================================== --- src/main/kotlin/util/sequences.kt (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/kotlin/util/sequences.kt (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -29,3 +29,24 @@ } fun Sequence.filterNotNull(transform: (T) -> Any?) = this.filter { transform(it) != null } + +fun Sequence.memoized(): Sequence { + val cache = mutableListOf() + val iterator = this.iterator() + + return sequence { + var index = 0 + while (true) { + if (index < cache.size) { + yield(cache[index]) + } else if (iterator.hasNext()) { + val next = iterator.next() + cache.add(next) + yield(next) + } else { + break + } + index++ + } + } +} Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 55ee6b293763157cfe5a060027a5685b2196fc61) +++ src/main/resources/META-INF/plugin.xml (revision a9c3fab25901dfabbcfc9b3aab88c31d3aa79cdf) @@ -713,6 +713,7 @@ +