diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbb6cfd46..17d333b89 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,12 @@ jobs: strategy: fail-fast: false matrix: - java: [17-jdk, 21-jdk] + java: [21-jdk] runs-on: ubuntu-24.04 container: image: eclipse-temurin:${{ matrix.java }} options: --user root steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/actions/wrapper-validation@v4 - run: ./gradlew build publishToMavenLocal --stacktrace --warning-mode fail \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de6e243fe..400278fc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ jobs: options: --user root steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/actions/wrapper-validation@v4 - run: ./gradlew build publish --stacktrace env: MAVEN_URL: ${{ secrets.MAVEN_URL }} diff --git a/HEADER.txt b/HEADER.txt index 3f213880a..08c1f7a11 100644 --- a/HEADER.txt +++ b/HEADER.txt @@ -1,22 +1,24 @@ -This file is part of ${name}, licensed under the MIT License (MIT). - -Copyright (c) ${organization} <${url}> -Copyright (c) contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ diff --git a/build.gradle b/build.gradle index 47d4c7df7..1da302e1d 100644 --- a/build.gradle +++ b/build.gradle @@ -5,21 +5,20 @@ buildscript { mavenCentral() } dependencies { - classpath 'gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin:0.16.1' classpath 'com.guardsquare:proguard-gradle:' + (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11) ? '7.5.0' : '7.1.0') } } plugins { - //id "me.modmuss50.remotesign" version "0.4.0" - id "io.github.goooler.shadow" version "8.1.7" + // id "me.modmuss50.remotesign" version "0.5.0" + id "com.gradleup.shadow" version "9.0.0-beta17" + id "dev.lukebemish.central-portal-publishing" version "0.1.7" } import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar // Apply plugin apply plugin: 'java' -apply plugin: 'com.github.hierynomus.license' apply plugin: 'checkstyle' apply plugin: 'maven-publish' apply plugin: 'eclipse' @@ -27,7 +26,7 @@ apply plugin: 'idea' apply plugin: 'potemkin-modules' // Default tasks -defaultTasks 'licenseFormat', 'check', 'build' +defaultTasks 'check', 'build' // Basic project information group = 'io.github.legacymoddingmc' @@ -298,22 +297,13 @@ processResources { } // License header formatting -license { - ext { - name = "Mixin" - organization = "SpongePowered" - url = "https://www.spongepowered.org" - } - include '**/*.java' - header file("HEADER.txt") - sourceSets = project.sourceSets - ignoreFailures false - strictCheck true - mapping { - java = 'SLASHSTAR_STYLE' - } +def validateHeaders = tasks.register("validateHeaders", org.spongepowered.asm.mixin.build.ValidateLicenseHeadersTasks) { + header = resources.text.fromFile("HEADER.txt") + source = sourceSets.collect { it.allJava } } +tasks.check.dependsOn validateHeaders + checkstyle { configProperties = [ "name" : project.name, @@ -353,6 +343,7 @@ project.sourceSets.each { set -> { compileModularityJava { inputs.files(modularityInputs) + options.javaModuleVersion = project.version as String doFirst { options.compilerArgs = [ '--module-path', classpath.asPath, @@ -457,15 +448,23 @@ ext.excludePomDeps = [ 'junit', 'mockito-core', 'mixin-asm-debug-all', - 'log4j-core' + 'log4j-core', + 'gson' ] +centralPortalPublishing.bundle('mixin') { + username = ENV.MAVEN_CENTRAL_USERNAME + password = ENV.MAVEN_CENTRAL_PASSWORD +} + +tasks.publish.dependsOn tasks.named("publishMixinCentralPortalBundle") + publishing { publications { developer(MavenPublication) { publication -> - groupId project.group - artifactId project.base.archivesName.get() - version project.version + groupId = project.group + artifactId = project.base.archivesName.get() + version = project.version artifact sourceJar artifact javadocJar @@ -547,24 +546,15 @@ publishing { repositories { if (ENV.MAVEN_URL) { maven { - url ENV.MAVEN_URL + url = ENV.MAVEN_URL credentials { - username ENV.MAVEN_USERNAME - password ENV.MAVEN_PASSWORD + username = ENV.MAVEN_USERNAME + password = ENV.MAVEN_PASSWORD } } } - if (ENV.MAVEN_CENTRAL_URL) { - repositories.maven { - name "central" - url ENV.MAVEN_CENTRAL_URL - credentials { - username ENV.MAVEN_CENTRAL_USERNAME - password ENV.MAVEN_CENTRAL_PASSWORD - } - } - } + centralPortalPublishing.portalBundle(':', 'mixin') } } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index e7001a077..6450ea907 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -4,8 +4,8 @@ plugins { repositories { maven { - name 'sponge' - url 'https://repo.spongepowered.org/repository/maven-public' + name = 'sponge' + url = 'https://repo.spongepowered.org/repository/maven-public' } } diff --git a/buildSrc/src/main/groovy/org/spongepowered/asm/mixin/build/ValidateLicenseHeadersTasks.groovy b/buildSrc/src/main/groovy/org/spongepowered/asm/mixin/build/ValidateLicenseHeadersTasks.groovy new file mode 100644 index 000000000..2b30598c5 --- /dev/null +++ b/buildSrc/src/main/groovy/org/spongepowered/asm/mixin/build/ValidateLicenseHeadersTasks.groovy @@ -0,0 +1,56 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.spongepowered.asm.mixin.build + +import org.gradle.api.resources.TextResource +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.SourceTask +import org.gradle.api.tasks.TaskAction + +abstract class ValidateLicenseHeadersTasks extends SourceTask { + @Input + TextResource header + + @TaskAction + void run() { + def headerStr = header.asString() + def javaSources = getSource().matching(pattern -> pattern.include("**/*.java")).files + + def failedFiles = [] + + for (def source : javaSources) { + if (!source.text.startsWith(headerStr)) { + failedFiles.add(source.name) + logger.warn("File {} does not have the correct license header", source.path) + } + } + + if (!failedFiles.isEmpty()) { + throw new RuntimeException("License header validation failed for the following files:\n" + + failedFiles.join("\n")) + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f..1b33c55ba 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4413138c..ff23a68d7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf133..23d15a936 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 25da30dbd..db3a6ac20 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -68,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java b/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java index 7821ded06..82e9d8727 100644 --- a/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java +++ b/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java @@ -78,7 +78,7 @@ public byte[] transform(ClassLoader loader, String className, Class classBein try { MixinAgent.log(Level.INFO, "Redefining class {}", className); - return MixinAgent.this.classTransformer.transformClassBytes(null, className, classfileBuffer); + return MixinAgent.this.classTransformer.transformClassBytes(null, className.replace('/', '.'), classfileBuffer); } catch (Throwable th) { MixinAgent.log(Level.ERROR, "Error while re-transforming class {}", className, th); return MixinAgent.ERROR_BYTECODE; diff --git a/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java b/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java index f68c3ed3f..5d800b6b2 100644 --- a/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java +++ b/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java @@ -49,10 +49,20 @@ public final class FabricUtil { */ public static final int COMPATIBILITY_0_14_0 = 14000; // 0.14.0+mixin.0.8.6 + /** + * Fabric compatibility version 0.16.5 + */ + public static final int COMPATIBILITY_0_16_5 = 16005; // 0.16.5+mixin.0.8.7 + + /** + * Fabric compatibility version 0.17.0 + */ + public static final int COMPATIBILITY_0_17_0 = 17000; // 0.17.0+mixin.0.8.7 + /** * Latest compatibility version */ - public static final int COMPATIBILITY_LATEST = COMPATIBILITY_0_14_0; + public static final int COMPATIBILITY_LATEST = COMPATIBILITY_0_17_0; public static String getModId(IMixinConfig config) { return getModId(config, "(unknown)"); diff --git a/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java b/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java index d9f2e913d..d2c282276 100644 --- a/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java +++ b/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java @@ -625,11 +625,10 @@ final String getStringValue() { ? System.getProperty(this.property, this.defaultValue) : this.defaultValue; } - @SuppressWarnings("unchecked") > E getEnumValue(E defaultValue) { String value = System.getProperty(this.property, defaultValue.name()); try { - return (E)Enum.valueOf(defaultValue.getClass(), value.toUpperCase(Locale.ROOT)); + return Enum.valueOf(defaultValue.getDeclaringClass(), value.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ex) { return defaultValue; } @@ -861,6 +860,48 @@ boolean isSupported() { return JavaVersion.current() >= JavaVersion.JAVA_22 && ASM.getMaxSupportedClassVersion() >= 66; } + }, + + /** + * Java 23 or above is required + */ + JAVA_23(23, Opcodes.V23, LanguageFeatures.METHODS_IN_INTERFACES | LanguageFeatures.PRIVATE_SYNTHETIC_METHODS_IN_INTERFACES + | LanguageFeatures.PRIVATE_METHODS_IN_INTERFACES | LanguageFeatures.NESTING | LanguageFeatures.DYNAMIC_CONSTANTS + | LanguageFeatures.RECORDS | LanguageFeatures.SEALED_CLASSES) { + + @Override + boolean isSupported() { + return JavaVersion.current() >= JavaVersion.JAVA_23 && ASM.isAtLeastVersion(9, 7); + } + + }, + + /** + * Java 23 or above is required + */ + JAVA_24(24, Opcodes.V24, LanguageFeatures.METHODS_IN_INTERFACES | LanguageFeatures.PRIVATE_SYNTHETIC_METHODS_IN_INTERFACES + | LanguageFeatures.PRIVATE_METHODS_IN_INTERFACES | LanguageFeatures.NESTING | LanguageFeatures.DYNAMIC_CONSTANTS + | LanguageFeatures.RECORDS | LanguageFeatures.SEALED_CLASSES) { + + @Override + boolean isSupported() { + return JavaVersion.current() >= JavaVersion.JAVA_24 && ASM.isAtLeastVersion(9, 7, 1); + } + + }, + + /** + * Java 25 or above is required + */ + JAVA_25(25, Opcodes.V25, LanguageFeatures.METHODS_IN_INTERFACES | LanguageFeatures.PRIVATE_SYNTHETIC_METHODS_IN_INTERFACES + | LanguageFeatures.PRIVATE_METHODS_IN_INTERFACES | LanguageFeatures.NESTING | LanguageFeatures.DYNAMIC_CONSTANTS + | LanguageFeatures.RECORDS | LanguageFeatures.SEALED_CLASSES) { + + @Override + boolean isSupported() { + return JavaVersion.current() >= JavaVersion.JAVA_25 && ASM.isAtLeastVersion(9, 8); + } + }, ; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/InjectionPoint.java b/src/main/java/org/spongepowered/asm/mixin/injection/InjectionPoint.java index e87f3e871..200794f97 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/InjectionPoint.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/InjectionPoint.java @@ -45,6 +45,7 @@ import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodNode; +import org.spongepowered.asm.mixin.FabricUtil; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.injection.callback.CallbackInjector; @@ -552,14 +553,20 @@ static final class Shift extends InjectionPoint { private final InjectionPoint input; private final int shift; + private final boolean respectSpecifier; public Shift(InjectionPoint input, int shift) { + this(input, shift, FabricUtil.COMPATIBILITY_LATEST); + } + + public Shift(InjectionPoint input, int shift, int fabricCompatibility) { if (input == null) { throw new IllegalArgumentException("Must supply an input injection point for SHIFT"); } this.input = input; this.shift = shift; + this.respectSpecifier = fabricCompatibility >= FabricUtil.COMPATIBILITY_0_16_5; } /* (non-Javadoc) @@ -606,6 +613,11 @@ public boolean find(String desc, InsnList insns, Collection no return nodes.size() > 0; } + + @Override + public Specifier getSpecifier(Specifier defaultSpecifier) { + return this.respectSpecifier ? this.input.getSpecifier(defaultSpecifier) : super.getSpecifier(defaultSpecifier); + } } /** @@ -871,15 +883,17 @@ private static InjectionPoint create(IMixinContext context, InjectionPointData d private static InjectionPoint shift(IInjectionPointContext context, InjectionPoint point, At.Shift shift, int by) { + + int fabricCompatibility = FabricUtil.getCompatibility(context); if (point != null) { if (shift == At.Shift.BEFORE) { - return InjectionPoint.before(point); + return new InjectionPoint.Shift(point, -1, fabricCompatibility); } else if (shift == At.Shift.AFTER) { - return InjectionPoint.after(point); + return new InjectionPoint.Shift(point, 1, fabricCompatibility); } else if (shift == At.Shift.BY) { InjectionPoint.validateByValue(context.getMixin(), context.getMethod(), context.getAnnotationNode(), point, by); - return InjectionPoint.shift(point, by); + return new InjectionPoint.Shift(point, by, fabricCompatibility); } } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/code/Injector.java b/src/main/java/org/spongepowered/asm/mixin/injection/code/Injector.java index 6e813add7..273dac285 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/code/Injector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/code/Injector.java @@ -446,9 +446,11 @@ protected AbstractInsnNode invokeHandler(InsnList insns) { * @return injected insn node */ protected AbstractInsnNode invokeHandler(InsnList insns, MethodNode handler) { - boolean isInterface = Bytecode.hasFlag(classNode, Opcodes.ACC_INTERFACE); - boolean isPrivate = (handler.access & Opcodes.ACC_PRIVATE) != 0; - int invokeOpcode = this.isStatic ? Opcodes.INVOKESTATIC : isInterface ? Opcodes.INVOKEINTERFACE : isPrivate ? Opcodes.INVOKESPECIAL : Opcodes.INVOKEVIRTUAL; + boolean isPrivate = Bytecode.hasFlag(handler, Opcodes.ACC_PRIVATE); + boolean isSynthetic = Bytecode.hasFlag(handler, Opcodes.ACC_SYNTHETIC); + int invokeOpcode = this.isStatic ? Opcodes.INVOKESTATIC : + this.isInterface ? (isSynthetic && isPrivate ? Opcodes.INVOKESPECIAL : Opcodes.INVOKEINTERFACE) : + isPrivate ? Opcodes.INVOKESPECIAL : Opcodes.INVOKEVIRTUAL; MethodInsnNode insn = new MethodInsnNode(invokeOpcode, this.classNode.name, handler.name, handler.desc, isInterface); insns.add(insn); this.info.addCallbackInvocation(handler); diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/modify/LocalVariableDiscriminator.java b/src/main/java/org/spongepowered/asm/mixin/injection/modify/LocalVariableDiscriminator.java index 676665b31..5438c8c6f 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/modify/LocalVariableDiscriminator.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/modify/LocalVariableDiscriminator.java @@ -153,30 +153,30 @@ private Local[] initLocals(Target target, boolean argsOnly, AbstractInsnNode nod if (!argsOnly) { LocalVariableNode[] locals = Locals.getLocalsAt(target.classNode, target.method, node, org.spongepowered.asm.mixin.FabricUtil.getCompatibility(info)); if (locals != null) { - Local[] lvt = new Local[locals.length]; - for (int l = 0; l < locals.length; l++) { - if (locals[l] != null) { - lvt[l] = new Local(locals[l].name, Type.getType(locals[l].desc)); - } - } - return lvt; + return getLocals(locals); } } - - Local[] lvt = new Local[this.baseArgIndex + Bytecode.getArgsSize(target.arguments)]; - if (!this.isStatic) { - lvt[0] = new Local("this", Type.getObjectType(target.classNode.name)); - } - for (int local = this.baseArgIndex, arg = 0; local < lvt.length; local++) { - Type argType = target.arguments[arg++]; - lvt[local] = new Local("arg" + local, argType); - if (argType.getSize() == 2) { - lvt[++local] = null; + + int fabricCompatibility = org.spongepowered.asm.mixin.FabricUtil.getCompatibility(info); + // Before 0.17.0, the names of arg variables were inconsistent. With argsOnly = true, they were "arg" + lvIndex + // whereas for argsOnly = false, they were "arg" + paramIndex. We pick the latter always now which is the slightly + // saner option, but we need to pick the former here when backwards compat is needed. + boolean fallbackToLvIndex = fabricCompatibility < org.spongepowered.asm.mixin.FabricUtil.COMPATIBILITY_0_17_0; + LocalVariableNode[] initialLocals = Locals.getInitialMethodLocals(target.method, target.classNode, fabricCompatibility, fallbackToLvIndex); + + return getLocals(initialLocals); + } + + private Local[] getLocals(LocalVariableNode[] initialLocals) { + Local[] lvt = new Local[initialLocals.length]; + for (int l = 0; l < initialLocals.length; l++) { + if (initialLocals[l] != null) { + lvt[l] = new Local(initialLocals[l].name, Type.getType(initialLocals[l].desc)); } } return lvt; } - + private void initOrdinals() { Map ordinalMap = new HashMap(); for (int l = 0; l < this.locals.length; l++) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DynamicSelectorDesc.java b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DynamicSelectorDesc.java index f3df61903..f0b53d444 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DynamicSelectorDesc.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DynamicSelectorDesc.java @@ -503,7 +503,7 @@ public MatchResult match(ElementNode node) { if (node == null || this.disabled) { return MatchResult.NONE; } else if (node.isField()) { - return this.matches(node.getOwner(), node.getName(), node.getDesc(), this.returnType.getInternalName()); + return this.matches(node.getOwner(), node.getName(), node.getDesc(), this.returnType.getDescriptor()); } else { return this.matches(node.getOwner(), node.getName(), node.getDesc(), this.methodDesc); } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/MemberInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/MemberInfo.java index dcbdecf00..70545d4c0 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/MemberInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/MemberInfo.java @@ -883,16 +883,6 @@ public static MemberInfo parse(final String input, final ISelectorContext contex if (context != null) { name = context.remap(name); } - - int lastDotPos = name.lastIndexOf('.'); - int semiColonPos = name.indexOf(';'); - if (lastDotPos > -1) { - owner = name.substring(0, lastDotPos).replace('.', '/'); - name = name.substring(lastDotPos + 1); - } else if (semiColonPos > -1 && name.startsWith("L")) { - owner = name.substring(1, semiColonPos).replace('.', '/'); - name = name.substring(semiColonPos + 1); - } int parenPos = name.indexOf('('); int colonPos = name.indexOf(':'); @@ -904,6 +894,16 @@ public static MemberInfo parse(final String input, final ISelectorContext contex name = name.substring(0, colonPos); } + int lastDotPos = name.lastIndexOf('.'); + int semiColonPos = name.indexOf(';'); + if (lastDotPos > -1) { + owner = name.substring(0, lastDotPos).replace('.', '/'); + name = name.substring(lastDotPos + 1); + } else if (semiColonPos > -1 && name.startsWith("L")) { + owner = name.substring(1, semiColonPos).replace('.', '/'); + name = name.substring(semiColonPos + 1); + } + if ((name.indexOf('/') > -1 || name.indexOf('.') > -1) && owner == null) { owner = name; name = ""; diff --git a/src/main/java/org/spongepowered/asm/mixin/struct/MemberRef.java b/src/main/java/org/spongepowered/asm/mixin/struct/MemberRef.java index 985cec013..1420da952 100644 --- a/src/main/java/org/spongepowered/asm/mixin/struct/MemberRef.java +++ b/src/main/java/org/spongepowered/asm/mixin/struct/MemberRef.java @@ -331,6 +331,15 @@ public void setHandle(int tag, String owner, String name, String desc, boolean i */ public abstract void setDesc(String desc); + /** + * Whether the owner of this member is a mixin. + * @return Whether the owner of this member is a mixin. + */ + public boolean ownerIsMixin() { + String owner = getOwner(); + return !owner.startsWith("[") && ClassInfo.isMixin(owner); + } + @Override public String toString() { return String.format("%s for %s.%s%s%s", Bytecode.getOpcodeName(this.getOpcode()), this.getOwner(), this.getName(), this.isField() ? ":" : "", diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/ClassInfo.java b/src/main/java/org/spongepowered/asm/mixin/transformer/ClassInfo.java index 66fa2a278..504b611e8 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/ClassInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/ClassInfo.java @@ -24,6 +24,8 @@ */ package org.spongepowered.asm.mixin.transformer; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -907,6 +909,59 @@ private ClassInfo(ClassNode classNode) { } } + private ClassInfo(Class cls) { + this.name = getName(cls); + this.superName = cls.getSuperclass() != null ? getName(cls.getSuperclass()) : ClassInfo.JAVA_LANG_OBJECT; + this.initialisers = new HashSet(); + this.methods = new HashSet(); + this.fields = new HashSet(); + this.isInterface = cls.isInterface(); + Class[] interfaces = cls.getInterfaces(); + this.interfaces = new HashSet(interfaces.length); + this.isMixin = false; + this.mixin = null; + this.mixins = Collections.emptySet(); + + for (Class iface : interfaces) { + this.interfaces.add(getName(iface)); + } + + for (Constructor ctor : cls.getDeclaredConstructors()) { + if ((ctor.getModifiers() & (Modifier.PROTECTED | Modifier.PUBLIC)) == 0) { + continue; + } + + this.initialisers.add(new Method(ctor.getName(), org.objectweb.asm.Type.getConstructorDescriptor(ctor), ctor.getModifiers())); + } + + for (java.lang.reflect.Method method : cls.getDeclaredMethods()) { + if ((method.getModifiers() & (Modifier.PROTECTED | Modifier.PUBLIC)) == 0) { + continue; + } + + this.methods.add(new Method(method.getName(), org.objectweb.asm.Type.getMethodDescriptor(method), method.getModifiers())); + } + + for (java.lang.reflect.Field field : cls.getDeclaredFields()) { + if ((field.getModifiers() & (Modifier.PROTECTED | Modifier.PUBLIC)) == 0) { + continue; + } + + this.fields.add(new Field(field.getName(), org.objectweb.asm.Type.getDescriptor(field.getType()), field.getModifiers())); + } + + this.isProbablyStatic = cls.getEnclosingClass() == null || Modifier.isStatic(cls.getModifiers()); + this.methodMapper = null; + + this.access = cls.getModifiers(); + this.isInner = cls.getEnclosingClass() != null; + this.outerName = cls.getDeclaringClass() != null ? getName(cls.getDeclaringClass()) : null; + } + + private static String getName(Class cls) { + return cls.getName().replace('.', '/'); + } + void addInterface(String iface) { this.interfaces.add(iface); this.getSignature().addInterface(iface); @@ -2023,11 +2078,15 @@ public static ClassInfo forName(String className) { className = className.replace('.', '/'); ClassInfo info = ClassInfo.cache.get(className); - if (info == null) { + if (!ClassInfo.cache.containsKey(className)) { try { - int flags = MixinEnvironment.getCurrentEnvironment().getOption(Option.CLASSREADER_EXPAND_FRAMES) ? ClassReader.EXPAND_FRAMES : 0; - ClassNode classNode = MixinService.getService().getBytecodeProvider().getClassNode(className, true, flags); - info = new ClassInfo(classNode); + if (className.startsWith("java/")) { // this would ideally check the platform class loader for other jdk classes, but needs extra api to do so + info = new ClassInfo(Class.forName(className.replace('/', '.'), false, ClassInfo.class.getClassLoader())); + } else { + int flags = MixinEnvironment.getCurrentEnvironment().getOption(Option.CLASSREADER_EXPAND_FRAMES) ? ClassReader.EXPAND_FRAMES : 0; + ClassNode classNode = MixinService.getService().getBytecodeProvider().getClassNode(className, true, flags); + info = new ClassInfo(classNode); + } } catch (Exception ex) { ClassInfo.logger.catching(Level.TRACE, ex); ClassInfo.logger.warn("Error loading class: {} ({}: {})", className, ex.getClass().getName(), ex.getMessage()); @@ -2231,4 +2290,13 @@ private static ClassInfo getCommonSuperClass(ClassInfo type1, ClassInfo type2, b return type1; } + public static boolean isMixin(String className) { + ClassInfo cachedInfo = ClassInfo.fromCache(className); + if (cachedInfo == null) { + // Mixin infos are forcefully cached, so this isn't one + return false; + } + return cachedInfo.isMixin(); + } + } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/IMixinTransformer.java b/src/main/java/org/spongepowered/asm/mixin/transformer/IMixinTransformer.java index 769ab93fc..3098177e9 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/IMixinTransformer.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/IMixinTransformer.java @@ -101,6 +101,15 @@ public interface IMixinTransformer { * @return true if the class was transformed */ public abstract boolean transformClass(MixinEnvironment environment, String name, ClassNode classNode); + + /** + * Determines whether mixin could transform the provided class, without actually running the transformation + * + * @param environment Current environment + * @param name Class transformed name + * @return true if the class could be transformed + */ + public abstract boolean couldTransformClass(MixinEnvironment environment, String name); /** * Generate the specified mixin-synthetic class diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/InnerClassGenerator.java b/src/main/java/org/spongepowered/asm/mixin/transformer/InnerClassGenerator.java index 4a5a56b9d..d70c0537a 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/InnerClassGenerator.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/InnerClassGenerator.java @@ -25,10 +25,12 @@ package org.spongepowered.asm.mixin.transformer; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import org.objectweb.asm.tree.InnerClassNode; import org.spongepowered.asm.logging.ILogger; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; @@ -57,7 +59,7 @@ final class InnerClassGenerator implements IClassGenerator { * Information about an inner class instance. Implements {@link Remapper} so * that it can participate in the remapping process. */ - static class InnerClassInfo extends Remapper implements ISyntheticClassInfo { + class InnerClassInfo extends Remapper implements ISyntheticClassInfo { /** * Mixin which provides this class @@ -155,10 +157,21 @@ String getNestHostName() { void accept(final ClassVisitor classVisitor) throws ClassNotFoundException, IOException { ClassNode classNode = MixinService.getService().getBytecodeProvider().getClassNode(this.originalName); + this.readInnerClasses(classNode); classNode.accept(classVisitor); this.loadCounter++; } + private void readInnerClasses(ClassNode classNode) { + // Read possibly nested inner classes + for (InnerClassNode inner : classNode.innerClasses) { + if ((inner.outerName != null && this.findRemappedName(inner.outerName) != null) + || inner.name.startsWith(this.mixin.getClassRef() + "$")) { + InnerClassGenerator.this.registerInnerClass(this.owner, this.targetClassInfo, inner.name); + } + } + } + /** * Used to remap fields which have been renamed while being applied to * the target class or by an implementation of IRemapper. @@ -198,10 +211,9 @@ public String mapMethodName(String owner, String name, String desc) { */ @Override public String map(String key) { - if (this.originalName.equals(key)) { - return this.name; - } else if (this.ownerName.equals(key)) { - return this.targetClassInfo.getName(); + String remappedName = this.findRemappedName(key); + if (remappedName != null) { + return remappedName; } return key; } @@ -213,6 +225,15 @@ public String map(String key) { public String toString() { return this.name; } + + private String findRemappedName(String originalName) { + if (this.ownerName.equals(originalName)) { + return this.targetClassInfo.getName(); + } + return InnerClassGenerator.this.innerClassNames.get( + InnerClassGenerator.innerClassCoordinate(this.owner, this.targetClassInfo, originalName) + ); + } } @@ -252,21 +273,6 @@ public void visitSource(String source, String debug) { av.visitEnd(); } - /* (non-Javadoc) - * @see org.objectweb.asm.commons.RemappingClassAdapter - * #visitInnerClass(java.lang.String, java.lang.String, - * java.lang.String, int) - */ - @Override - public void visitInnerClass(String name, String outerName, String innerName, int access) { - if (name.startsWith(this.info.getOriginalName() + "$")) { - throw new InvalidMixinException(this.info.getOwner(), "Found unsupported nested inner class " + name + " in " - + this.info.getOriginalName()); - } - - super.visitInnerClass(name, outerName, innerName, access); - } - } /** @@ -338,7 +344,7 @@ public String getName() { * @param innerClassName Original inner class name */ void registerInnerClass(MixinInfo owner, ClassInfo targetClass, String innerClassName) { - String coordinate = String.format("%s:%s:%s", owner, innerClassName, targetClass.getName()); + String coordinate = InnerClassGenerator.innerClassCoordinate(owner, targetClass, innerClassName); String uniqueName = this.innerClassNames.get(coordinate); if (uniqueName != null) { return; @@ -421,7 +427,8 @@ private static String getUniqueReference(String originalName, ClassInfo targetCl if (name.matches("^[0-9]+$")) { name = "Anonymous"; } - return String.format("%s$%s$%s", targetClass, name, UUID.randomUUID().toString().replace("-", "")); + UUID uuid = UUID.nameUUIDFromBytes(originalName.getBytes(StandardCharsets.UTF_8)); + return String.format("%s$%s$%s", targetClass, name, uuid.toString().replace("-", "")); } /** @@ -457,4 +464,8 @@ private static ClassVisitor createRemappingAdapter(ClassVisitor cv, Remapper rem return InnerClassGenerator.clRemapper.getConstructor(ClassVisitor.class, Remapper.class).newInstance(cv, remapper); } + private static String innerClassCoordinate(MixinInfo owner, ClassInfo targetClass, String innerClassName) { + return String.format("%s:%s:%s", owner.getClassRef(), innerClassName, targetClass.getName()); + } + } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java index d3a550f5e..c6aac93bf 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java @@ -1394,9 +1394,11 @@ static Config create(String configFile, MixinEnvironment outer, IMixinConfigSour if (resource == null) { throw new IllegalArgumentException(String.format("The specified resource '%s' was invalid or could not be read", configFile)); } - MixinConfig config = new Gson().fromJson(new InputStreamReader(resource), MixinConfig.class); - if (config.onLoad(service, configFile, outer, source)) { - return config.getHandle(); + try (InputStreamReader reader = new InputStreamReader(resource)) { + MixinConfig config = new Gson().fromJson(reader, MixinConfig.class); + if (config.onLoad(service, configFile, outer, source)) { + return config.getHandle(); + } } return null; } catch (IllegalArgumentException ex) { diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessor.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessor.java index 6f3116190..e044d2e05 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessor.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessor.java @@ -25,7 +25,9 @@ package org.spongepowered.asm.mixin.transformer; import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.logging.ILogger; import org.spongepowered.asm.mixin.transformer.MixinConfig.IListener; +import org.spongepowered.asm.service.MixinService; /** * Coprocessors are parts of the mixin pipeline which aren't involved in @@ -33,11 +35,10 @@ * mixins. * *

Such tasks as {@link MixinCoprocessorAccessor making accessor mixins - * loadable (and transforming the accessors therein)}, {@link - * MixinCoprocessorSyntheticInner exposing synthetic inner classes to all - * consumers} and {@link MixinCoprocessorNestHost applying nest member - * attributes to nest hosts which may themselves not be mixin targets} are - * handled by different coprocessors.

+ * loadable (and transforming the accessors therein)}, and {@link + * MixinCoprocessorNestHost applying nest member attributes to nest hosts + * which may themselves not be mixin targets} are handled by different + * coprocessors.

* *

These classes were previously encapsulated in a single companion class * called MixinPostProcessor, but the mixture of responsibilities of @@ -153,6 +154,25 @@ ProcessResult process(String className, ClassNode classNode) { return ProcessResult.NONE; } + private static final ILogger logger = MixinService.getService().getLogger("mixin"); + private boolean willLogUnimplementedCouldTransform = true; + + /** + * Determine ahead-of-time whether a given class could be transformed ({@link ProcessResult#TRANSFORMED}, + * {@link ProcessResult#PASSTHROUGH_TRANSFORMED}, or modification in + * {@link MixinCoprocessor#postProcess(String, ClassNode)}) by processing by this coprocessor. + * + * @param className Name of the target class + * @return true if the coprocessor might transform the class when processed + */ + public boolean couldTransform(String className) { + if (willLogUnimplementedCouldTransform) { + willLogUnimplementedCouldTransform = false; + logger.error("MixinCoprocessor {} ({}) does not implement couldTransform, which may lead to unnecessary transformation", getName(), getClass().getName()); + } + return true; + } + /** * Perform postprocessing actions on the supplied class. This is called for * all classes. For passthrough classes and classes which are not mixin diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorAccessor.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorAccessor.java index 423c16394..970f0a7c2 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorAccessor.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorAccessor.java @@ -126,6 +126,11 @@ ProcessResult process(String className, ClassNode classNode) { return ProcessResult.PASSTHROUGH_TRANSFORMED; } + @Override + public boolean couldTransform(String className) { + return MixinEnvironment.getCompatibilityLevel().supports(LanguageFeatures.METHODS_IN_INTERFACES) && this.accessorMixins.containsKey(className); + } + private Method getAccessorMethod(MixinInfo mixin, MethodNode methodNode, ClassInfo targetClass) throws MixinTransformerError { Method method = mixin.getClassInfo().findMethod(methodNode, ClassInfo.INCLUDE_ALL); diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorNestHost.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorNestHost.java index 592446392..1cfba2779 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorNestHost.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorNestHost.java @@ -105,4 +105,8 @@ boolean postProcess(String className, ClassNode classNode) { return true; } + @Override + public boolean couldTransform(String className) { + return this.nestHosts.containsKey(className); + } } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorPassthrough.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorPassthrough.java index a970320a6..0c648e4d7 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorPassthrough.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorPassthrough.java @@ -64,4 +64,8 @@ ProcessResult process(String className, ClassNode classNode) { return this.loadable.contains(className) ? ProcessResult.PASSTHROUGH_NONE : ProcessResult.NONE; } + @Override + public boolean couldTransform(String className) { + return false; + } } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorSyntheticInner.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorSyntheticInner.java deleted file mode 100644 index 8291dc129..000000000 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorSyntheticInner.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * This file is part of Mixin, licensed under the MIT License (MIT). - * - * Copyright (c) SpongePowered - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package org.spongepowered.asm.mixin.transformer; - -import java.util.HashSet; -import java.util.Set; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldNode; -import org.objectweb.asm.tree.MethodNode; - -/** - * Coprocessor which handles synthetic inner classes in mixins. Since synthetic - * inner classes are usually just switch tables, it is reasonably safe to share - * them amongst all mixin targets. Therefore the class is simply transformed so - * that all members are public. - * - *

Nest based-access control could potentially be used where a mixin has a - * single target, and Java 11 or higher is in use, but at the moment it's more - * straightforward to just make everything public.

- */ -class MixinCoprocessorSyntheticInner extends MixinCoprocessor { - - /** - * Synthetic inner classes in mixins - */ - private final Set syntheticInnerClasses = new HashSet(); - - MixinCoprocessorSyntheticInner() { - } - - @Override - String getName() { - return "syntheticinner"; - } - - @Override - public void onInit(MixinInfo mixin) { - for (String innerClass : mixin.getSyntheticInnerClasses()) { - this.registerSyntheticInner(innerClass.replace('/', '.')); - } - } - - void registerSyntheticInner(String className) { - this.syntheticInnerClasses.add(className); - } - - /** - * "Pass through" a synthetic inner class. Transforms package-private - * members in the class into public so that they are accessible from their - * new home in the target class - */ - @Override - ProcessResult process(String className, ClassNode classNode) { - if (!this.syntheticInnerClasses.contains(className)) { - return ProcessResult.NONE; - } - - classNode.access |= Opcodes.ACC_PUBLIC; - - for (FieldNode field : classNode.fields) { - if ((field.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) == 0) { - field.access |= Opcodes.ACC_PUBLIC; - } - } - - for (MethodNode method : classNode.methods) { - if ((method.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) == 0) { - method.access |= Opcodes.ACC_PUBLIC; - } - } - - return ProcessResult.PASSTHROUGH_TRANSFORMED; - } - -} diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessors.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessors.java index 1656338c1..a5afb34f3 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessors.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessors.java @@ -67,6 +67,21 @@ ProcessResult process(String className, ClassNode classNode) { return result; } + /** + * Determine ahead-of-time whether a given class could be transformed by any registered coprocessor. + * + * @param className Name of the target class + * @return true if the coprocessors might transform the class when processed + */ + public boolean processingCouldTransform(String className) { + for (MixinCoprocessor coprocessor : this) { + if (coprocessor.couldTransform(className)) { + return true; + } + } + return false; + } + /** * Perform postprocessing actions on the supplied class using all registered * coprocessors. diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java index 41701876b..a26ac474f 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java @@ -224,14 +224,9 @@ class State { * Interfaces soft-implemented using {@link Implements} */ protected final List softImplements = new ArrayList(); - - /** - * Synthetic inner classes - */ - protected final Set syntheticInnerClasses = new HashSet(); /** - * Non-synthetic inner classes + * Inner classes */ protected final Set innerClasses = new HashSet(); @@ -286,10 +281,6 @@ List getSoftImplements() { return this.softImplements; } - Set getSyntheticInnerClasses() { - return this.syntheticInnerClasses; - } - Set getInnerClasses() { return this.innerClasses; } @@ -420,18 +411,13 @@ void readImplementations(SubType type) { } /** - * Read inner class definitions for the class and locate any synthetic - * inner classes so that we can add them to the passthrough set in our - * parent config. + * Read inner class definitions for the class and locate any of our inner classes. */ void readInnerClasses() { for (InnerClassNode inner : this.validationClassNode.innerClasses) { - ClassInfo innerClass = ClassInfo.forName(inner.name); if ((inner.outerName != null && inner.outerName.equals(this.classInfo.getName())) || inner.name.startsWith(this.validationClassNode.name + "$")) { - if (innerClass.isProbablyStatic() && innerClass.isSynthetic()) { - this.syntheticInnerClasses.add(inner.name); - } else if (!innerClass.isMixin()) { + if (!ClassInfo.isMixin(inner.name)) { this.innerClasses.add(inner.name); } } @@ -464,7 +450,7 @@ class Reloaded extends State { */ @Override protected void validateChanges(SubType type, List targetClasses) { - if (!this.syntheticInnerClasses.equals(this.previous.syntheticInnerClasses)) { + if (!this.innerClasses.equals(this.previous.innerClasses)) { throw new MixinReloadException(MixinInfo.this, "Cannot change inner classes"); } if (!this.interfaces.equals(this.previous.interfaces)) { @@ -1024,9 +1010,6 @@ private ClassInfo getTargetClass(DeclaredTarget target) throws InvalidMixinExcep return null; } this.type.validateTarget(target.name, targetInfo); - if (target.isPrivate && targetInfo.isReallyPublic() && !this.isVirtual()) { - this.handleTargetError(String.format("@Mixin target %s is public in %s and should be specified in value", target.name, this), true); - } return targetInfo; } @@ -1238,16 +1221,9 @@ public List getTargetClasses() { List getSoftImplements() { return Collections.unmodifiableList(this.getState().getSoftImplements()); } - - /** - * Get the synthetic inner classes for this mixin - */ - Set getSyntheticInnerClasses() { - return Collections.unmodifiableSet(this.getState().getSyntheticInnerClasses()); - } /** - * Get the user-defined inner classes for this mixin + * Get the inner classes for this mixin */ Set getInnerClasses() { return Collections.unmodifiableSet(this.getState().getInnerClasses()); diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorStandard.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorStandard.java index 5be4f8f22..85cf936ba 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorStandard.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorStandard.java @@ -783,6 +783,10 @@ protected void transformField(FieldInsnNode fieldNode) { } protected void transformMemberReference(MemberRef ref) { + if (!ClassInfo.isMixin(ref.getOwner())) { + // No need to transform anything + return; + } IActivity activity = this.activities.begin("%s::%s:%s", ref.getOwner(), ref.getName(), ref.getDesc()); Section metaTimer = this.profiler.begin("meta"); diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinProcessor.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinProcessor.java index 2cfa27c33..1a2985a51 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinProcessor.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinProcessor.java @@ -226,7 +226,6 @@ public String getErrorMessage(IMixinInfo mixin, IMixinConfig config, Phase phase this.hotSwapper = hotSwapper; this.coprocessors.add(new MixinCoprocessorPassthrough()); - this.coprocessors.add(new MixinCoprocessorSyntheticInner()); this.coprocessors.add(new MixinCoprocessorAccessor(this.sessionId)); this.coprocessors.add(nestHostCoprocessor); @@ -251,7 +250,7 @@ public void audit(MixinEnvironment environment) { for (String target : unhandled) { try { auditLogger.info("Force-loading class {}", target); - this.service.getClassProvider().findClass(target, true); + this.service.getClassProvider().findClass(target, false); } catch (ClassNotFoundException ex) { auditLogger.error("Could not force-load " + target, ex); } @@ -274,25 +273,14 @@ synchronized boolean applyMixins(MixinEnvironment environment, String name, Clas return false; } - boolean locked = this.lock.push().check(); Section mixinTimer = this.profiler.begin("mixin"); - - if (locked) { - for (MixinConfig config : this.pendingConfigs) { - if (config.hasPendingMixinsFor(name)) { - ReEntrantTransformerError error = new ReEntrantTransformerError("Re-entrance error."); - MixinProcessor.logger.warn("Re-entrance detected during prepare phase, this will cause serious problems.", error); - throw error; - } - } - } else { - try { - this.checkSelect(environment); - } catch (Exception ex) { - this.lock.pop(); - mixinTimer.end(); - throw new MixinException(ex); - } + boolean locked; + + try { + locked = lockAndSelect(environment, name); + } catch (Throwable th) { + mixinTimer.end(); + throw th; } boolean transformed = false; @@ -397,6 +385,65 @@ synchronized boolean applyMixins(MixinEnvironment environment, String name, Clas return transformed; } + synchronized boolean couldTransformClass(MixinEnvironment environment, String name) { + if (environment != MixinEnvironment.getCurrentEnvironment()) { + throw new MixinException("Current environment must match the supplied environment"); + } + + if (name == null || this.errorState) { + return false; + } + + lockAndSelect(environment, name); + + try { + if (this.coprocessors.processingCouldTransform(name)) { + return true; + } + + for (MixinConfig config : this.configs) { + if (config.packageMatch(name)) { + // If the class is in a mixin package, it may be transformed + return true; + } + } + + for (MixinConfig config : this.configs) { + if (config.hasMixinsFor(name)) { + // If any config has mixins for the class, it may be transformed + return true; + } + } + + return false; + } finally { + this.lock.pop(); + } + } + + private boolean lockAndSelect(MixinEnvironment environment, String name) { + boolean locked = this.lock.push().check(); + + if (locked) { + for (MixinConfig config : this.pendingConfigs) { + if (config.hasPendingMixinsFor(name)) { + ReEntrantTransformerError error = new ReEntrantTransformerError("Re-entrance error."); + MixinProcessor.logger.warn("Re-entrance detected during prepare phase, this will cause serious problems.", error); + throw error; + } + } + } else { + try { + this.checkSelect(environment); + } catch (Exception ex) { + this.lock.pop(); + throw new MixinException(ex); + } + } + + return locked; + } + private String getInvalidClassError(String name, ClassNode targetClassNode, MixinConfig ownedByConfig) { if (ownedByConfig.getClasses().contains(name)) { return String.format("Illegal classload request for %s. Mixin is defined in %s and cannot be referenced directly", name, ownedByConfig); diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTargetContext.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTargetContext.java index a97fc0b78..e66c19798 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTargetContext.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTargetContext.java @@ -544,7 +544,7 @@ private void validateMethod(MethodNode method) { } } - if (Bytecode.isVirtual(method)) { + if (this.getEnvironment().getOption(Option.DEBUG_VERIFY) && Bytecode.isVirtual(method)) { Method superMethod = this.targetClassInfo.findMethodInHierarchy(method, SearchType.SUPER_CLASSES_ONLY, Traversal.ALL, 0); if (superMethod != null && superMethod.isFinal()) { throw new InvalidMixinException(this.mixin, String.format("%s%s in %s overrides a final method from %s", @@ -600,7 +600,7 @@ private void transformMethodRef(MethodNode method, Iterator it } else if (this.detachedSuper || this.inheritsFromMixin) { if (methodRef.getOpcode() == Opcodes.INVOKESPECIAL) { this.updateStaticBinding(method, methodRef); - } else if (methodRef.getOpcode() == Opcodes.INVOKEVIRTUAL && ClassInfo.forName(methodRef.getOwner()).isMixin()) { + } else if (methodRef.getOpcode() == Opcodes.INVOKEVIRTUAL && methodRef.ownerIsMixin()) { this.updateDynamicBinding(method, methodRef); } } @@ -637,9 +637,11 @@ private void transformFieldRef(MethodNode method, Iterator ite if (field != null && field.isRenamed() && field.getOriginalName().equals(fieldRef.getName()) && field.isStatic()) { fieldRef.setName(field.getName()); } + } else if (this.innerClasses.containsKey(fieldRef.getOwner())) { + fieldRef.setOwner(this.innerClasses.get(fieldRef.getOwner())); } else { - ClassInfo fieldOwner = ClassInfo.forName(fieldRef.getOwner()); - if (fieldOwner.isMixin()) { + if (ClassInfo.isMixin(fieldRef.getOwner())) { + ClassInfo fieldOwner = ClassInfo.forName(fieldRef.getOwner()); ClassInfo actualOwner = this.targetClassInfo.findCorrespondingType(fieldOwner); fieldRef.setOwner(actualOwner != null ? actualOwner.getName() : this.getTarget().getClassRef()); } @@ -903,7 +905,7 @@ private void updateBinding(MethodNode method, MemberRef methodRef, Traversal tra + " but is mixin."); } methodRef.setOwner(superMethod.getImplementor().getName()); - } else if (ClassInfo.forName(methodRef.getOwner()).isMixin()) { + } else if (methodRef.ownerIsMixin()) { throw new MixinTransformerError("Error resolving " + methodRef + " in " + this); } } @@ -1011,11 +1013,8 @@ private String transformSingleDescriptor(String desc, boolean isObject) { descriptorActivity.end(); return desc; } - ClassInfo typeInfo = ClassInfo.forName(type); - if (typeInfo == null) { - throw new ClassMetadataNotFoundException(type.replace('/', '.')); - } - if (!typeInfo.isMixin() || typeInfo.isLoadable()) { + ClassInfo typeInfo; + if (!ClassInfo.isMixin(type) || (typeInfo = ClassInfo.forName(type)).isLoadable()) { descriptorActivity.end(); return desc; } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java index c6bde16de..2c2df6ed1 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTransformer.java @@ -229,7 +229,10 @@ public boolean computeFramesForClass(MixinEnvironment environment, String name, * @return Transformed bytecode */ @Override - public byte[] transformClass(MixinEnvironment environment, String name, byte[] classBytes) { + public synchronized byte[] transformClass(MixinEnvironment environment, String name, byte[] classBytes) { + if (!couldTransformClass(environment, name)) { + return classBytes; + } ClassNode classNode = this.readClass(name, classBytes); if (this.processor.applyMixins(environment, name, classNode)) { return this.writeClass(classNode); @@ -246,10 +249,22 @@ public byte[] transformClass(MixinEnvironment environment, String name, byte[] c * @return true if the class was transformed */ @Override - public boolean transformClass(MixinEnvironment environment, String name, ClassNode classNode) { + public synchronized boolean transformClass(MixinEnvironment environment, String name, ClassNode classNode) { return this.processor.applyMixins(environment, name, classNode); } - + + /** + * Determines whether mixin could transform the provided class, without actually running the transformation + * + * @param environment Current environment + * @param name Class transformed name + * @return true if the class could be transformed + */ + @Override + public synchronized boolean couldTransformClass(MixinEnvironment environment, String name) { + return this.processor.couldTransformClass(environment, name); + } + /** * Generate the specified mixin-synthetic class * @@ -258,7 +273,7 @@ public boolean transformClass(MixinEnvironment environment, String name, ClassNo * @return Generated bytecode or null if no class was generated */ @Override - public byte[] generateClass(MixinEnvironment environment, String name) { + public synchronized byte[] generateClass(MixinEnvironment environment, String name) { ClassNode classNode = MixinTransformer.createEmptyClass(name); if (this.generator.generateClass(environment, name, classNode)) { return this.writeClass(classNode); @@ -273,7 +288,7 @@ public byte[] generateClass(MixinEnvironment environment, String name) { * @return True if the class was generated successfully */ @Override - public boolean generateClass(MixinEnvironment environment, String name, ClassNode classNode) { + public synchronized boolean generateClass(MixinEnvironment environment, String name, ClassNode classNode) { return this.generator.generateClass(environment, name, classNode); } diff --git a/src/main/java/org/spongepowered/asm/util/Annotations.java b/src/main/java/org/spongepowered/asm/util/Annotations.java index 259986d8b..5bff4b6e7 100644 --- a/src/main/java/org/spongepowered/asm/util/Annotations.java +++ b/src/main/java/org/spongepowered/asm/util/Annotations.java @@ -552,19 +552,14 @@ public static T getValue(AnnotationNode annotation, String key, Class ann */ @SuppressWarnings("unchecked") public static T getValue(AnnotationNode annotation, String key) { - boolean getNextValue = false; - if (annotation == null || annotation.values == null) { return null; } // Keys and value are stored in successive pairs, search for the key and if found return the following entry - for (Object value : annotation.values) { - if (getNextValue) { - return (T) value; - } - if (value.equals(key)) { - getNextValue = true; + for (int i = 0; i < annotation.values.size() - 1; i += 2) { + if (annotation.values.get(i).equals(key)) { + return (T) annotation.values.get(i + 1); } } diff --git a/src/main/java/org/spongepowered/asm/util/Bytecode.java b/src/main/java/org/spongepowered/asm/util/Bytecode.java index eae400113..8a451ee26 100644 --- a/src/main/java/org/spongepowered/asm/util/Bytecode.java +++ b/src/main/java/org/spongepowered/asm/util/Bytecode.java @@ -30,9 +30,9 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.ListIterator; import java.util.Map; +import com.google.common.collect.Iterators; import org.objectweb.asm.Attribute; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; @@ -41,6 +41,7 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; import org.objectweb.asm.util.CheckClassAdapter; +import org.objectweb.asm.util.Printer; import org.objectweb.asm.util.TraceClassVisitor; import org.spongepowered.asm.util.asm.ASM; import org.spongepowered.asm.util.asm.MarkerNode; @@ -464,7 +465,7 @@ public static String describeNode(AbstractInsnNode node, boolean listFormat) { } else if (node instanceof InvokeDynamicInsnNode) { InvokeDynamicInsnNode idc = (InvokeDynamicInsnNode)node; out += String.format("[%s] %s%s { %s %s::%s%s }", Bytecode.getOpcodeName(node), idc.name, idc.desc, - Bytecode.getOpcodeName(idc.bsm.getTag(), "H_GETFIELD", 1), idc.bsm.getOwner(), idc.bsm.getName(), idc.bsm.getDesc()); + Bytecode.getOpcodeName(idc.bsm.getTag(), Printer.HANDLE_TAG), idc.bsm.getOwner(), idc.bsm.getName(), idc.bsm.getDesc()); } else if (node instanceof LineNumberNode) { LineNumberNode ln = (LineNumberNode)node; out += String.format("LINE=[%d] LABEL=[%s]", ln.line, ln.start.getLabel()); @@ -473,7 +474,7 @@ public static String describeNode(AbstractInsnNode node, boolean listFormat) { } else if (node instanceof IntInsnNode) { out += (((IntInsnNode)node).operand); } else if (node instanceof FrameNode) { - out += String.format("[%s] ", Bytecode.getOpcodeName(((FrameNode)node).type, "H_INVOKEINTERFACE", -1)); + out += String.format("[%s] ", getFrameTypeName((FrameNode) node)); } else if (node instanceof TypeInsnNode) { out += String.format("[%s] %s", Bytecode.getOpcodeName(node), ((TypeInsnNode)node).desc); } else { @@ -483,53 +484,56 @@ public static String describeNode(AbstractInsnNode node, boolean listFormat) { } /** - * Uses reflection to find an approximate constant name match for the - * supplied node's opcode + * Finds a constant name match for the supplied node's opcode * * @param node Node to query for opcode - * @return Approximate opcode name (approximate because some constants in - * the {@link Opcodes} class have the same value as opcodes + * @return opcode name */ public static String getOpcodeName(AbstractInsnNode node) { return node != null ? Bytecode.getOpcodeName(node.getOpcode()) : ""; } /** - * Uses reflection to find an approximate constant name match for the - * supplied opcode + * Finds a constant name match for the supplied node's opcode * * @param opcode Opcode to look up - * @return Approximate opcode name (approximate because some constants in - * the {@link Opcodes} class have the same value as opcodes + * @return opcode name */ public static String getOpcodeName(int opcode) { - return Bytecode.getOpcodeName(opcode, "UNINITIALIZED_THIS", 1); + return Bytecode.getOpcodeName(opcode, Printer.OPCODES); } - private static String getOpcodeName(int opcode, String start, int min) { - if (opcode >= min) { - boolean found = false; - - try { - for (java.lang.reflect.Field f : Opcodes.class.getDeclaredFields()) { - if (!found && !f.getName().equals(start)) { - continue; - } - found = true; - if (f.getType() == Integer.TYPE && f.getInt(null) == opcode) { - return f.getName(); - } - } - } catch (Exception ex) { - // derp - } - } - - return opcode >= 0 ? String.valueOf(opcode) : "UNKNOWN"; + private static String getOpcodeName(int opcode, String[] names) { + if (opcode < 0) { + return "UNKNOWN"; + } + if (opcode < names.length) { + return names[opcode]; + } + return String.valueOf(opcode); + } + + private static String getFrameTypeName(FrameNode node) { + switch (node.type) { + case Opcodes.F_NEW: + return "F_NEW"; + case Opcodes.F_FULL: + return "F_FULL"; + case Opcodes.F_APPEND: + return "F_APPEND"; + case Opcodes.F_CHOP: + return "F_CHOP"; + case Opcodes.F_SAME: + return "F_SAME"; + case Opcodes.F_SAME1: + return "F_SAME1"; + default: + return "UNKNOWN"; + } } /** - * Uses reflection to find a matching constant in the {@link Opcodes} + * Finds a matching constant in the {@link Opcodes} * interface for the specified opcode name. Supported formats are raw * numeric values, bare constant names (eg. ACONST_NULL) or * qualified names (eg. Opcodes.ACONST_NULL). Returns the value if @@ -556,24 +560,14 @@ public static int parseOpcodeName(String opcodeName) { return -1; } - return Bytecode.parseOpcodeName(opcodeName, "UNINITIALIZED_THIS", 1); + return Bytecode.parseOpcodeName(opcodeName, Printer.OPCODES); } - private static int parseOpcodeName(String opcodeName, String start, int min) { - boolean found = false; - - try { - for (java.lang.reflect.Field f : Opcodes.class.getDeclaredFields()) { - if (!found && !f.getName().equals(start)) { - continue; - } - found = true; - if (f.getType() == Integer.TYPE && f.getName().equalsIgnoreCase(opcodeName)) { - return f.getInt(null); - } + private static int parseOpcodeName(String name, String[] names) { + for (int i = 0; i < names.length; i++) { + if (name.equalsIgnoreCase(names[i])) { + return i; } - } catch (Exception ex) { - // well this is embarrassing } return -1; @@ -1256,16 +1250,13 @@ public static String getUnboxingMethod(Type type) { * @param b Incoming method */ public static void compareBridgeMethods(MethodNode a, MethodNode b) { - ListIterator ia = a.instructions.iterator(); - ListIterator ib = b.instructions.iterator(); + Iterator ia = Iterators.filter(a.instructions.iterator(), Bytecode::isRealInsn); + Iterator ib = Iterators.filter(b.instructions.iterator(), Bytecode::isRealInsn); int index = 0; for (; ia.hasNext() && ib.hasNext(); index++) { AbstractInsnNode na = ia.next(); AbstractInsnNode nb = ib.next(); - if (na instanceof LabelNode) { - continue; - } if (na instanceof MethodInsnNode) { MethodInsnNode ma = (MethodInsnNode)na; @@ -1292,11 +1283,18 @@ public static void compareBridgeMethods(MethodNode a, MethodNode b) { } } - if (ia.hasNext() || ib.hasNext()) { - throw new SyntheticBridgeException(Problem.BAD_LENGTH, a.name, a.desc, index, null, null); + if (ia.hasNext()) { + throw new SyntheticBridgeException(Problem.BAD_LENGTH, a.name, a.desc, index, ia.next(), null); + } + if (ib.hasNext()) { + throw new SyntheticBridgeException(Problem.BAD_LENGTH, a.name, a.desc, index, null, ib.next()); } } + private static boolean isRealInsn(AbstractInsnNode insn) { + return insn.getOpcode() != -1; + } + /** * Perform a naïve merge of ClassNode members onto a target ClassNode * diff --git a/src/main/java/org/spongepowered/asm/util/JavaVersion.java b/src/main/java/org/spongepowered/asm/util/JavaVersion.java index 9382f27b3..b900669de 100644 --- a/src/main/java/org/spongepowered/asm/util/JavaVersion.java +++ b/src/main/java/org/spongepowered/asm/util/JavaVersion.java @@ -116,6 +116,21 @@ public abstract class JavaVersion { * Version number for Java 22 */ public static final double JAVA_22 = 22.0; + + /** + * Version number for Java 23 + */ + public static final double JAVA_23 = 23.0; + + /** + * Version number for Java 24 + */ + public static final double JAVA_24 = 24.0; + + /** + * Version number for Java 25 + */ + public static final double JAVA_25 = 25.0; private static double current = 0.0; diff --git a/src/main/java/org/spongepowered/asm/util/Locals.java b/src/main/java/org/spongepowered/asm/util/Locals.java index 0fcc960c1..441c2ed6c 100644 --- a/src/main/java/org/spongepowered/asm/util/Locals.java +++ b/src/main/java/org/spongepowered/asm/util/Locals.java @@ -270,10 +270,117 @@ private Locals() { // utility class } + /** + * Constructs the initial local variable frame for a method, containing only "this" (if non-static) + * and the method parameters with their actual names extracted from the LVT when available. + * + * @param method the method to construct locals for + * @param classNode the class containing the method + * @param fabricCompatibility the compatibility level to use + * @return an array of LocalVariableNodes representing the initial method frame + */ + public static LocalVariableNode[] getInitialMethodLocals(MethodNode method, ClassNode classNode, int fabricCompatibility) { + return getInitialMethodLocals(method, classNode, fabricCompatibility, false); + } + + /** + * Constructs the initial local variable frame for a method, containing only "this" (if non-static) + * and the method parameters with their actual names extracted from the LVT when available. + * + * @param method the method to construct locals for + * @param classNode the class containing the method + * @param fabricCompatibility the compatibility level to use + * @param fallbackToLvIndex whether the fallback names should use the LVT index of the variable (for backwards compatibility) + * @return an array of LocalVariableNodes representing the initial method frame + */ + public static LocalVariableNode[] getInitialMethodLocals(MethodNode method, ClassNode classNode, int fabricCompatibility, boolean fallbackToLvIndex) { + boolean isStatic = Bytecode.isStatic(method); + Type[] argTypes = Type.getArgumentTypes(method.desc); + int initialFrameSize = Bytecode.getFirstNonArgLocalIndex(method); + LocalVariableNode[] frame = new LocalVariableNode[initialFrameSize]; + + int local = 0; + + // Try to extract parameter names from LVT if compatibility level is high enough + String[] paramNames = fabricCompatibility >= org.spongepowered.asm.mixin.FabricUtil.COMPATIBILITY_0_17_0 + ? getParameterNames(method, isStatic) + : new String[argTypes.length]; + + // Initialise implicit "this" reference in non-static methods + if (!isStatic) { + frame[local++] = new LocalVariableNode("this", Type.getObjectType(classNode.name).getDescriptor(), null, null, null, 0); + } + + // Initialise method arguments + for (int index = 0; index < argTypes.length; index++) { + Type argType = argTypes[index]; + String paramName = paramNames[index]; + if (paramName == null) { + if (fallbackToLvIndex) { + paramName = "arg" + local; + } else { + paramName = "arg" + index; + } + } + frame[local] = new LocalVariableNode(paramName, argType.getDescriptor(), null, null, null, local); + local += argType.getSize(); + } + + return frame; + } + + /** + * Attempts to extract parameter names from the method's local variable table. + * This is used as a fallback when detailed LVT information is not available at a specific + * instruction, but we still want to use actual parameter names instead of generic "arg" names. + * + * @param method the method to extract parameter names from + * @param isStatic whether the method is static (affects the starting local index) + * @return an array of parameter names, with null entries where names are unavailable + */ + private static String[] getParameterNames(MethodNode method, boolean isStatic) { + Type[] argTypes = Type.getArgumentTypes(method.desc); + if (argTypes.length == 0) { + return new String[0]; + } + + // Always allocate array for all parameters + String[] paramNames = new String[argTypes.length]; + + // Build map of local variable index to parameter index + Map indexToParam = new HashMap<>(); + int localIndex = isStatic ? 0 : 1; // Skip "this" for non-static methods + for (int arg = 0; arg < argTypes.length; arg++) { + indexToParam.put(localIndex, arg); + localIndex += argTypes[arg].getSize(); + } + + // Single pass through local variable table to extract names + if (method.localVariables != null) { + for (LocalVariableNode lvNode : method.localVariables) { + Integer paramIndex = indexToParam.get(lvNode.index); + if (paramIndex != null) { + paramNames[paramIndex] = lvNode.name; + } + } + } + + // Merge in names from parameters field where LVT didn't provide them + if (method.parameters != null) { + for (int i = 0; i < Math.min(argTypes.length, method.parameters.size()); i++) { + if (paramNames[i] == null) { + paramNames[i] = method.parameters.get(i).name; + } + } + } + + return paramNames; + } + /** * Injects appropriate LOAD opcodes into the supplied InsnList for each * entry in the supplied locals array starting at pos - * + * * @param locals Local types (can contain nulls for uninitialised, TOP, or * RETURN values in locals) * @param insns Instruction List to inject into @@ -334,7 +441,7 @@ public static void loadLocals(Type[] locals, InsnList insns, int pos, int limit) */ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode method, AbstractInsnNode node, int fabricCompatibility) { if (fabricCompatibility >= org.spongepowered.asm.mixin.FabricUtil.COMPATIBILITY_0_10_0) { - return Locals.getLocalsAt(classNode, method, node, Settings.DEFAULT); + return Locals.getLocalsAt(classNode, method, node, Settings.DEFAULT, fabricCompatibility); } else { return getLocalsAt092(classNode, method, node); } @@ -343,16 +450,16 @@ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode me /** *

Attempts to identify available locals at an arbitrary point in the * bytecode specified by node.

- * + * *

This method builds an approximate view of the locals available at an * arbitrary point in the bytecode by examining the following features in - * the bytecode:

+ * the bytecode:

*
    *
  • Any available stack map frames
  • *
  • STORE opcodes
  • *
  • The local variable table
  • *
- * + * *

Inference proceeds by walking the bytecode from the start of the * method looking for stack frames and STORE opcodes. When either of these * is encountered, an attempt is made to cross-reference the values in the @@ -363,14 +470,14 @@ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode me * simulated locals array are spaced according to their size (unlike the * representation in FrameNode) and this TOP, NULL and UNINTITIALIZED_THIS * opcodes will be represented as null values in the simulated frame.

- * + * *

This code does not currently simulate the prescribed JVM behaviour * where overwriting the second slot of a DOUBLE or LONG actually * invalidates the DOUBLE or LONG stored in the previous location, so we * have to hope (for now) that this behaviour isn't emitted by the compiler * or any upstream transformers. I may have to re-think this strategy if * this situation is encountered in the wild.

- * + * * @param classNode ClassNode containing the method, used to initialise the * implicit "this" reference in simple methods with no stack frames * @param method MethodNode to explore @@ -384,6 +491,10 @@ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode me * specified location */ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode method, AbstractInsnNode node, Settings settings) { + return getLocalsAt(classNode, method, node, settings, org.spongepowered.asm.mixin.FabricUtil.COMPATIBILITY_LATEST); + } + + private static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode method, AbstractInsnNode node, Settings settings, int fabricCompatibility) { for (int i = 0; i < 3 && (node instanceof LabelNode || node instanceof LineNumberNode); i++) { AbstractInsnNode nextNode = Locals.nextNode(method.instructions, node); if (nextNode instanceof FrameNode) { // Do not ffwd over frames @@ -402,25 +513,16 @@ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode me } List frames = methodInfo.getFrames(); + // Initialize the frame with "this" and method parameters + LocalVariableNode[] initialLocals = getInitialMethodLocals(method, classNode, fabricCompatibility); LocalVariableNode[] frame = new LocalVariableNode[method.maxLocals]; - int local = 0, index = 0; + System.arraycopy(initialLocals, 0, frame, 0, initialLocals.length); - // Initialise implicit "this" reference in non-static methods - if ((method.access & Opcodes.ACC_STATIC) == 0) { - frame[local++] = new LocalVariableNode("this", Type.getObjectType(classNode.name).toString(), null, null, null, 0); - } - - // Initialise method arguments - for (Type argType : Type.getArgumentTypes(method.desc)) { - frame[local] = new LocalVariableNode("arg" + index++, argType.toString(), null, null, null, local); - local += argType.getSize(); - } - - final int initialFrameSize = local; - int frameSize = local; + final int initialFrameSize = initialLocals.length; + int frameSize = initialFrameSize; int frameIndex = -1; - int lastFrameSize = local; - int knownFrameSize = local; + int lastFrameSize = initialFrameSize; + int knownFrameSize = initialFrameSize; VarInsnNode storeInsn = null; for (Iterator iter = method.instructions.iterator(); iter.hasNext();) { diff --git a/src/main/java/org/spongepowered/asm/util/throwables/SyntheticBridgeException.java b/src/main/java/org/spongepowered/asm/util/throwables/SyntheticBridgeException.java index 0138c6bdd..17042a836 100644 --- a/src/main/java/org/spongepowered/asm/util/throwables/SyntheticBridgeException.java +++ b/src/main/java/org/spongepowered/asm/util/throwables/SyntheticBridgeException.java @@ -125,15 +125,15 @@ public void printAnalysis(IMixinContext context, MethodNode mda, MethodNode mdb) printer.add().kv("Method", this.name + this.desc).kv("Problem Type", this.problem).add().hr(); String merged = Annotations.getValue(Annotations.getVisible(mda, MixinMerged.class), "mixin"); String owner = merged != null ? merged : context.getTargetClassRef().replace('/', '.'); - this.printMethod(printer.add("Existing method").add().kv("Owner", owner).add(), mda).hr(); - this.printMethod(printer.add("Incoming method").add().kv("Owner", context.getClassRef().replace('/', '.')).add(), mdb).hr(); + this.printMethod(printer.add("Existing method").add().kv("Owner", owner).add(), mda, a).hr(); + this.printMethod(printer.add("Incoming method").add().kv("Owner", context.getClassRef().replace('/', '.')).add(), mdb, b).hr(); this.printProblem(printer, context, mda, mdb).print(System.err); } - private PrettyPrinter printMethod(PrettyPrinter printer, MethodNode method) { - int index = 0; - for (Iterator iter = method.instructions.iterator(); iter.hasNext(); index++) { - printer.kv(index == this.index ? ">>>>" : "", Bytecode.describeNode(iter.next())); + private PrettyPrinter printMethod(PrettyPrinter printer, MethodNode method, AbstractInsnNode marker) { + for (Iterator iter = method.instructions.iterator(); iter.hasNext();) { + AbstractInsnNode insn = iter.next(); + printer.kv(insn == marker ? ">>>>" : "", Bytecode.describeNode(insn)); } return printer.add(); }