User: joe Date: 10 Oct 26 13:52 Revision: f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05 Summary: Centralize all reflection through McdevReflection, and add a junit test to make sure reflected fields exist TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10650&personal=false Index: src/main/kotlin/platform/mixin/action/GenerateAccessorHandler.kt =================================================================== --- src/main/kotlin/platform/mixin/action/GenerateAccessorHandler.kt (revision cdb61bdc0a6f6b0da67cf29d81359ad2d86b4643) +++ src/main/kotlin/platform/mixin/action/GenerateAccessorHandler.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) @@ -27,11 +27,12 @@ import com.demonwav.mcdev.platform.mixin.util.isAccessorMixin import com.demonwav.mcdev.platform.mixin.util.isMixin import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.util.McdevReflection +import com.demonwav.mcdev.util.McdevReflection.invokeUnwrap import com.demonwav.mcdev.util.capitalize import com.demonwav.mcdev.util.findContainingClass import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.fullQualifiedName -import com.demonwav.mcdev.util.invokeDeclaredMethod import com.demonwav.mcdev.util.invokeLater import com.intellij.codeInsight.daemon.impl.quickfix.CreateClassKind import com.intellij.codeInsight.generation.ClassMember @@ -115,21 +116,12 @@ { val offset = mixinEditor.caretModel.offset try { - this.invokeDeclaredMethod( - "doGenerate", - params = arrayOf( - Project::class.java, - Editor::class.java, - PsiClass::class.java, - Array::class.java, - ), - args = arrayOf( + McdevReflection.METHOD_GENERATE_MEMBERS_HANDLER_BASE_DO_GENERATE.invokeUnwrap( + this, - project, - mixinEditor, - mixinClass, - members, + project, + mixinEditor, + mixinClass, + members, - ), - owner = GenerateMembersHandlerBase::class.java, ) } catch (e: GenerateCodeException) { val message = e.message ?: "Unknown error" Index: src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt (revision cdb61bdc0a6f6b0da67cf29d81359ad2d86b4643) +++ src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) @@ -21,6 +21,8 @@ package com.demonwav.mcdev.platform.mixin.expression import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.McdevReflection +import com.demonwav.mcdev.util.McdevReflection.invokeUnwrap import com.demonwav.mcdev.util.findContainingModifierList import com.demonwav.mcdev.util.findContainingNameValuePair import com.demonwav.mcdev.util.parseArray @@ -44,23 +46,11 @@ import com.intellij.psi.util.PsiUtil import com.intellij.psi.util.parentOfType import com.intellij.util.SmartList -import java.lang.reflect.Method class MEExpressionInjector : MultiHostInjector { private object Const { val ELEMENTS = listOf(PsiLiteralExpression::class.java) val ME_EXPRESSION_INJECTION = Key.create("mcdev.meExpressionInjection") - - val CLASS_INJECTION_RESULT: Class<*> = - Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionResult") - val CLASS_INJECTION_REGISTRAR_IMPL: Class<*> = - Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionRegistrarImpl") - val METHOD_ADD_TO_RESULTS: Method = - CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("addToResults", CLASS_INJECTION_RESULT) - .also { it.isAccessible = true } - val METHOD_GET_INJECTED_RESULT: Method = - CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("getInjectedResult") - .also { it.isAccessible = true } } private data class MEExpressionInjection(val modCount: Long, val injectionResult: Any) @@ -89,7 +79,8 @@ val modCount = PsiModificationTracker.getInstance(project).modificationCount val primaryElement = modifierList.getUserData(Const.ME_EXPRESSION_INJECTION) if (primaryElement != null && primaryElement.modCount == modCount) { - Const.METHOD_ADD_TO_RESULTS.invoke(registrar, primaryElement.injectionResult) + McdevReflection.METHOD_INJECTION_REGISTRAR_IMPL_ADD_TO_RESULTS + .invokeUnwrap(registrar, primaryElement.injectionResult) return } @@ -144,8 +135,11 @@ modifierList.putUserData( Const.ME_EXPRESSION_INJECTION, - MEExpressionInjection(modCount, Const.METHOD_GET_INJECTED_RESULT.invoke(registrar)) + MEExpressionInjection( + modCount, + McdevReflection.METHOD_INJECTION_REGISTRAR_IMPL_GET_INJECTED_RESULT.invokeUnwrap(registrar)!!, - ) + ) + ) } private fun iterateConcatenation(element: PsiElement, consumer: (PsiElement) -> Unit) { Index: src/main/kotlin/update/PluginUpdater.kt =================================================================== --- src/main/kotlin/update/PluginUpdater.kt (revision cdb61bdc0a6f6b0da67cf29d81359ad2d86b4643) +++ src/main/kotlin/update/PluginUpdater.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) @@ -20,7 +20,7 @@ package com.demonwav.mcdev.update -import com.demonwav.mcdev.util.findDeclaredField +import com.demonwav.mcdev.util.McdevReflection import com.demonwav.mcdev.util.forEachNotNull import com.demonwav.mcdev.util.invokeLater import com.intellij.ide.plugins.IdeaPluginDescriptor @@ -136,7 +136,7 @@ val status = downloader.prepareToInstall(indicator) // If the download failed, quit // But otherwise force the install - if (!status && downloader.findDeclaredField("myFile") == null) { + if (!status && McdevReflection.FIELD_PLUGIN_DOWNLOADER_MY_FILE.get(downloader) == null) { return } Index: src/main/kotlin/util/McdevReflection.kt =================================================================== --- src/main/kotlin/util/McdevReflection.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) +++ src/main/kotlin/util/McdevReflection.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) @@ -0,0 +1,109 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 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.util + +import com.intellij.codeInsight.generation.ClassMember +import com.intellij.codeInsight.generation.GenerateMembersHandlerBase +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.updateSettings.impl.PluginDownloader +import com.intellij.psi.PsiClass +import java.lang.reflect.Field +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.nio.file.Path + +/** + * All reflective access to IntelliJ internals goes through this object, so that the existence of every member can be + * verified in one place by `McdevReflectionTest`. + */ +object McdevReflection { + @JvmStatic + val CLASS_INJECTION_RESULT: Class<*> = + Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionResult") + + @JvmStatic + val CLASS_INJECTION_REGISTRAR_IMPL: Class<*> = + Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionRegistrarImpl") + + @JvmStatic + val METHOD_INJECTION_REGISTRAR_IMPL_ADD_TO_RESULTS: Method = + CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethodEx("addToResults", Void.TYPE, CLASS_INJECTION_RESULT) + + @JvmStatic + val METHOD_INJECTION_REGISTRAR_IMPL_GET_INJECTED_RESULT: Method = + CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethodEx("getInjectedResult", CLASS_INJECTION_RESULT) + + @JvmStatic + val METHOD_GENERATE_MEMBERS_HANDLER_BASE_DO_GENERATE: Method = + GenerateMembersHandlerBase::class.java.getDeclaredMethodEx( + "doGenerate", + Void.TYPE, + Project::class.java, + Editor::class.java, + PsiClass::class.java, + Array::class.java, + ) + + @JvmStatic + val FIELD_PLUGIN_DOWNLOADER_MY_FILE: Field = + PluginDownloader::class.java.getDeclaredFieldEx("myFile", Path::class.java) + + /** + * Like [Class.getDeclaredField], but makes the field accessible and checks that it has the expected [type]. + */ + private fun Class<*>.getDeclaredFieldEx(name: String, type: Class<*>): Field { + val field = getDeclaredField(name) + check(field.type == type) { + "Field $field has type ${field.type.name}, expected ${type.name}" + } + field.isAccessible = true + return field + } + + /** + * Like [Class.getDeclaredMethod], but makes the method accessible and checks that it has the expected [returnType]. + */ + private fun Class<*>.getDeclaredMethodEx(name: String, returnType: Class<*>, vararg params: Class<*>): Method { + val method = getDeclaredMethod(name, *params) + check(method.returnType == returnType) { + "Method $method has return type ${method.returnType.name}, expected ${returnType.name}" + } + method.isAccessible = true + return method + } + + /** + * Like [Method.invoke], but rethrows the exception thrown by the invoked method rather than wrapping it in an + * [InvocationTargetException]. + */ + fun Method.invokeUnwrap(obj: Any?, vararg args: Any?): Any? { + try { + return invoke(obj, *args) + } catch (e: InvocationTargetException) { + throw e.cause ?: e + } + } + + fun initialize() { + // load the class, used for testing + } +} Index: src/main/kotlin/util/reflection-utils.kt =================================================================== --- src/main/kotlin/util/reflection-utils.kt (revision cdb61bdc0a6f6b0da67cf29d81359ad2d86b4643) +++ src/main/kotlin/util/reflection-utils.kt (revision cdb61bdc0a6f6b0da67cf29d81359ad2d86b4643) @@ -1,47 +0,0 @@ -/* - * 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.util - -import kotlin.reflect.KClass - -val KClass.asPrimitiveType: Class - get() = this.javaPrimitiveType ?: error("javaPrimitiveType is not available for $this") - -fun Any.findDeclaredField(name: String, owner: Class<*> = javaClass): Any? { - return runCatching { - val field = owner.getDeclaredField(name) - field.isAccessible = true - field.get(this) - }.getOrNull() -} - -fun Any.invokeDeclaredMethod( - name: String, - params: Array>, - args: Array, - owner: Class<*> = javaClass, -): Any? { - return runCatching { - val method = owner.getDeclaredMethod(name, *params) - method.isAccessible = true - method(this, *args) - }.getOrNull() -} Index: src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt =================================================================== --- src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt (revision cdb61bdc0a6f6b0da67cf29d81359ad2d86b4643) +++ src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) @@ -3,7 +3,7 @@ * * https://mcdev.io/ * - * Copyright (C) 2025 minecraft-dev + * Copyright (C) 2026 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 @@ -25,7 +25,7 @@ import com.demonwav.mcdev.platform.mixin.BaseMixinTest import com.demonwav.mcdev.platform.mixin.util.MissingLVTChecker import com.demonwav.mcdev.util.BeforeOrAfter -import com.demonwav.mcdev.util.invokeDeclaredMethod +import com.demonwav.mcdev.util.McdevReflection.invokeUnwrap import com.intellij.codeInsight.lookup.Lookup import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiElement @@ -112,14 +112,9 @@ // TODO: this is for debugging, remove this once we figure out why tests are failing try { - val actual = StringUtil.convertLineSeparators( - (fixture.invokeDeclaredMethod( - "getHostFile", - emptyArray(), - emptyArray(), - CodeInsightTestFixtureImpl::class.java - ) as PsiFile).text - ) + val getHostFile = CodeInsightTestFixtureImpl::class.java.getDeclaredMethod("getHostFile") + .also { it.isAccessible = true } + val actual = StringUtil.convertLineSeparators((getHostFile.invokeUnwrap(fixture) as PsiFile).text) assertEquals( expectedAfter.replace("", ""), actual Index: src/test/kotlin/util/McdevReflectionTest.kt =================================================================== --- src/test/kotlin/util/McdevReflectionTest.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) +++ src/test/kotlin/util/McdevReflectionTest.kt (revision f7c9804eb342f2bbf5d9b7678c5faea7fdb98d05) @@ -0,0 +1,36 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 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.util + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("McdevReflection Tests") +class McdevReflectionTest { + @Test + @DisplayName("Reflected members exist") + fun reflectedMembersExist() { + // All classes, fields and methods are resolved when McdevReflection is initialized, which throws if any of + // them can't be found. + assertDoesNotThrow { McdevReflection.initialize() } + } +}