diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d36769d9..cbb6cfd46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,12 @@ jobs: strategy: fail-fast: false matrix: - java: [8-jdk, 11-jdk, 16-jdk, 17-jdk] - runs-on: ubuntu-20.04 + java: [17-jdk, 21-jdk] + runs-on: ubuntu-24.04 container: - image: openjdk:${{ matrix.java }} + image: eclipse-temurin:${{ matrix.java }} options: --user root steps: - - uses: actions/checkout@v1 - - uses: gradle/wrapper-validation-action@v1 - - run: ./gradlew build publishToMavenLocal --stacktrace \ No newline at end of file + - uses: actions/checkout@v4 + - uses: gradle/wrapper-validation-action@v2 + - 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 319e54748..2dcf2bd66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,13 +2,13 @@ name: Release on: [workflow_dispatch] # Manual trigger jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 container: - image: openjdk:17-jdk + image: eclipse-temurin:21-jdk options: --user root steps: - - uses: actions/checkout@v1 - - uses: gradle/wrapper-validation-action@v1 + - uses: actions/checkout@v4 + - uses: gradle/wrapper-validation-action@v2 - run: ./gradlew checkVersion build publish --stacktrace env: MAVEN_URL: ${{ secrets.MAVEN_URL }} diff --git a/build.gradle b/build.gradle index ac9f85be6..3f6771fb1 100644 --- a/build.gradle +++ b/build.gradle @@ -6,13 +6,13 @@ buildscript { } dependencies { classpath 'gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin:0.16.1' - classpath 'gradle.plugin.com.github.jengelman.gradle.plugins:shadow:7.0.0' - classpath 'com.guardsquare:proguard-gradle:' + (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11) ? '7.2.0' : '7.1.0') + 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 "me.modmuss50.remotesign" version "0.4.0" + id "io.github.goooler.shadow" version "8.1.7" } import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar @@ -24,7 +24,6 @@ apply plugin: 'checkstyle' apply plugin: 'maven-publish' apply plugin: 'eclipse' apply plugin: 'idea' -apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'potemkin-modules' // Default tasks @@ -32,9 +31,11 @@ defaultTasks 'licenseFormat', 'check', 'build' // Basic project information group = 'io.github.legacymoddingmc' -archivesBaseName = 'sponge-mixin' version = buildVersion + "+mixin." + upstreamMixinVersion +base { + archivesName = 'sponge-mixin' +} def ENV = System.getenv() if (!ENV.CI) { @@ -50,12 +51,13 @@ ext.packaging = 'jar' ext.asmVersion = project.hasProperty("asmVersion") ? asmVersion : '9.0' ext.legacyForgeAsmVersion = project.hasProperty("legacyForgeAsmVersion") ? asmVersion : '5.0.3' -// Minimum version of Java required -sourceCompatibility = '1.8' -targetCompatibility = '1.8' - java { modularity.inferModulePath = false + disableAutoTargetJvm() + + // Minimum version of Java required + sourceCompatibility = '1.8' + targetCompatibility = '1.8' } // Project repositories @@ -71,40 +73,20 @@ repositories { } maven { // For modlauncher - name = 'forge' - url = 'https://files.minecraftforge.net/maven' - } -} - -configurations { - stagingJar - - exampleImplementation .extendsFrom implementation - fernflowerImplementation .extendsFrom implementation - launchwrapperImplementation .extendsFrom implementation - agentImplementation .extendsFrom implementation - modlauncherImplementation .extendsFrom implementation - modlauncher9Implementation .extendsFrom modlauncherImplementation - modularityImplementation .extendsFrom modlauncher9Implementation - modularityCompileOnly .extendsFrom compileOnly - - proguard { - extendsFrom fernflowerImplementation - extendsFrom launchwrapperImplementation - extendsFrom modlauncherImplementation - extendsFrom compileClasspath + name = 'neoforged' + url = 'https://maven.neoforged.net/releases' } } sourceSets { legacy { ext.languageVersion = 8 - ext.compatibility = '1.6' + ext.compatibility = '1.8' } main { compileClasspath += legacy.output ext.languageVersion = 8 - ext.compatibility = '1.6' + ext.compatibility = '1.8' } ap { compileClasspath += main.output @@ -114,13 +96,13 @@ sourceSets { fernflower { compileClasspath += main.output ext.languageVersion = 8 - ext.compatibility = '1.6' + ext.compatibility = '1.8' ext.modularityExcluded = true } agent { compileClasspath += main.output ext.languageVersion = 8 - ext.compatibility = '1.6' + ext.compatibility = '1.8' } bridge { compileClasspath += main.output @@ -164,6 +146,26 @@ sourceSets { modularityDummy {} } +configurations { + stagingJar + + exampleImplementation .extendsFrom implementation + fernflowerImplementation .extendsFrom implementation + launchwrapperImplementation .extendsFrom implementation + agentImplementation .extendsFrom implementation + modlauncherImplementation .extendsFrom implementation + modlauncher9Implementation .extendsFrom modlauncherImplementation + modularityImplementation .extendsFrom modlauncher9Implementation + modularityCompileOnly .extendsFrom compileOnly + + proguard { + extendsFrom fernflowerImplementation + extendsFrom launchwrapperImplementation + extendsFrom modlauncherImplementation + extendsFrom compileClasspath + } +} + // Because Mixin aims to support a variety of environments, we have to be able to run with older versions of GSON and Guava that lack official module // names. This means the same library may appear with multiple module names. We want to be able to link our module with either of these two at // runtime, without having to have two versions of the library on our compile-time module path. To do this, we generate empty "potemkin" jars with @@ -226,7 +228,7 @@ dependencies { modlauncher9Implementation ("cpw.mods:modlauncher:$modlauncherVersion") { exclude module: 'jopt-simple' } - modlauncher9Implementation 'cpw.mods:securejarhandler:0.9.+' + modlauncher9Implementation 'cpw.mods:securejarhandler:2.1.24' // asm bridge bridgeImplementation 'org.apache.logging.log4j:log4j-core:2.0-beta9' @@ -243,11 +245,11 @@ javadoc { source sourceSets.ap.allJava options.encoding = 'UTF-8' exclude { - it.relativePath.file && it.relativePath.pathString =~ 'tools' && !(it.name =~ /SuppressedBy|package-info/) } + it.relativePath.file && it.relativePath.pathString =~ 'tools' && !(it.name =~ /SuppressedBy|package-info/) + } options { docTitle 'Welcome to the Mixin Javadoc' overview 'docs/javadoc/overview.html' - stylesheetFile file('docs/javadoc/mixin.css') addBooleanOption '-allow-script-in-comments', true } doLast { @@ -320,7 +322,7 @@ checkstyle { "year" : project.inceptionYear ] configFile = file("checkstyle.xml") - toolVersion = '8.44' + toolVersion = '10.17.0' } // Source compiler configuration @@ -333,10 +335,8 @@ tasks.withType(JavaCompile) { def modularityInputs = objects.fileCollection() project.sourceSets.each { set -> { - if (set.ext.has("languageVersion")) { - project.tasks[set.compileJavaTaskName].javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(set.ext.languageVersion) - } + if (set.ext.has("languageVersion") && JavaVersion.current().isJava9Compatible()) { + project.tasks[set.compileJavaTaskName].options.release = set.ext.languageVersion } if (set.ext.has("compatibility")) { project.tasks[set.compileJavaTaskName].sourceCompatibility = set.ext.compatibility @@ -439,7 +439,7 @@ task sourceJar(type: Jar) { } task javadocJar(type: Jar, dependsOn: javadoc) { - from javadoc.destinationDir + from javadoc.destinationDir archiveClassifier = "javadoc" } @@ -464,7 +464,7 @@ publishing { publications { developer(MavenPublication) { publication -> groupId project.group - artifactId project.archivesBaseName + artifactId project.base.archivesName.get() version project.version artifact sourceJar @@ -519,8 +519,8 @@ publishing { } } - developers { - // Et. al. that arent in the fabric org on maven central + developers { + // Et. al. that arent in the fabric org on maven central developer { id = "makamys" @@ -534,13 +534,13 @@ publishing { email = "modmuss50@fabricmc.net" } - developer { - id = "sfPlayer" - name = "Player" - email = "player@fabricmc.net" - } - } - } + developer { + id = "sfPlayer" + name = "Player" + email = "player@fabricmc.net" + } + } + } } } @@ -555,24 +555,24 @@ publishing { } } - 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 - } - } - } + 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 + } + } + } } } remoteSign { - requestUrl = ENV.SIGNING_SERVER - pgpAuthKey = ENV.SIGNING_PGP_KEY - useDummyForTesting = ENV.SIGNING_SERVER == null - sign publishing.publications.developer + requestUrl = ENV.SIGNING_SERVER + pgpAuthKey = ENV.SIGNING_PGP_KEY + useDummyForTesting = ENV.SIGNING_SERVER == null + sign publishing.publications.developer } // A task to ensure that the version being released has not already been released. diff --git a/checkstyle.xml b/checkstyle.xml index 1109c4ac6..48e5470cc 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -9,7 +9,7 @@ Description: none --> - + @@ -131,13 +131,6 @@ - - - - - - - diff --git a/gradle.properties b/gradle.properties index 7153f2b07..9f0736688 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,11 +4,11 @@ packaging=jar description=Mixin (LegacyModdingMC fork) url=https://github.com/LegacyModdingMC organization=LegacyModdingMC -buildVersion=0.13.0 -upstreamMixinVersion=0.8.5 +buildVersion=0.15.0 +upstreamMixinVersion=0.8.7 buildType=RELEASE asmVersion=9.6 legacyForgeAsmVersion=5.0.3 -modlauncherAsmVersion=9.1 -modlauncherVersion=9.0.7 +modlauncherAsmVersion=9.5 +modlauncherVersion=10.0.9 legacyModlauncherVersion=7.0.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2..e6441136f 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 2e6e5897b..a4413138c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882ed..b740cf133 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,99 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# 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 # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +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, +# 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. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..25da30dbd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java b/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java index 91d8c6218..7821ded06 100644 --- a/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java +++ b/src/agent/java/org/spongepowered/tools/agent/MixinAgent.java @@ -32,7 +32,7 @@ import java.util.ArrayList; import java.util.List; -import org.spongepowered.asm.logging.ILogger; +import org.spongepowered.asm.logging.Level; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.mixin.transformer.IMixinTransformer; @@ -40,6 +40,7 @@ import org.spongepowered.asm.mixin.transformer.throwables.MixinReloadException; import org.spongepowered.asm.service.IMixinService; import org.spongepowered.asm.service.MixinService; +import org.spongepowered.asm.service.ServiceNotAvailableError; import org.spongepowered.asm.transformers.MixinClassReader; import org.spongepowered.asm.util.asm.ASM; @@ -76,23 +77,23 @@ public byte[] transform(ClassLoader loader, String className, Class classBein } try { - MixinAgent.logger.info("Redefining class {}", className); + MixinAgent.log(Level.INFO, "Redefining class {}", className); return MixinAgent.this.classTransformer.transformClassBytes(null, className, classfileBuffer); } catch (Throwable th) { - MixinAgent.logger.error("Error while re-transforming class {}", className, th); + MixinAgent.log(Level.ERROR, "Error while re-transforming class {}", className, th); return MixinAgent.ERROR_BYTECODE; } } private List reloadMixin(String className, ClassNode classNode) { - MixinAgent.logger.info("Redefining mixin {}", className); + MixinAgent.log(Level.INFO, "Redefining mixin {}", className); try { return MixinAgent.this.classTransformer.reload(className.replace('/', '.'), classNode); } catch (MixinReloadException e) { - MixinAgent.logger.error("Mixin {} cannot be reloaded, needs a restart to be applied: {} ", e.getMixinInfo(), e.getMessage()); + MixinAgent.log(Level.ERROR, "Mixin {} cannot be reloaded, needs a restart to be applied: {} ", e.getMixinInfo(), e.getMessage()); } catch (Throwable th) { // catch everything as otherwise it is ignored - MixinAgent.logger.error("Error while finding targets for mixin {}", className, th); + MixinAgent.log(Level.ERROR, "Error while finding targets for mixin {}", className, th); } return null; } @@ -109,18 +110,18 @@ private boolean reApplyMixins(List targets) { for (String target : targets) { String targetName = target.replace('/', '.'); - MixinAgent.logger.debug("Re-transforming target class {}", target); + MixinAgent.log(Level.DEBUG, "Re-transforming target class {}", target); try { Class targetClass = service.getClassProvider().findClass(targetName); byte[] targetBytecode = MixinAgent.classLoader.getOriginalTargetBytecode(targetName); if (targetBytecode == null) { - MixinAgent.logger.error("Target class {} bytecode is not registered", targetName); + MixinAgent.log(Level.ERROR, "Target class {} bytecode is not registered", targetName); return false; } targetBytecode = MixinAgent.this.classTransformer.transformClassBytes(null, targetName, targetBytecode); MixinAgent.instrumentation.redefineClasses(new ClassDefinition(targetClass, targetBytecode)); } catch (Throwable th) { - MixinAgent.logger.error("Error while re-transforming target class {}", target, th); + MixinAgent.log(Level.ERROR, "Error while re-transforming target class {}", target, th); return false; } } @@ -140,8 +141,6 @@ private boolean reApplyMixins(List targets) { */ static final MixinAgentClassLoader classLoader = new MixinAgentClassLoader(); - static final ILogger logger = MixinService.getService().getLogger("mixin.agent"); - /** * Instance used to register the transformer */ @@ -195,7 +194,7 @@ public void registerTargetClass(String name, ClassNode classNode) { public static void init(Instrumentation instrumentation) { MixinAgent.instrumentation = instrumentation; if (!MixinAgent.instrumentation.isRedefineClassesSupported()) { - MixinAgent.logger.error("The instrumentation doesn't support re-definition of classes"); + MixinAgent.log(Level.ERROR, "The instrumentation doesn't support re-definition of classes"); } for (MixinAgent agent : MixinAgent.agents) { agent.initTransformer(); @@ -229,4 +228,19 @@ public static void agentmain(String arg, Instrumentation instrumentation) { MixinAgent.init(instrumentation); } + /** + * Wrapper for logger since we can't access the log abstraction in premain + * + * @param level the logging level + * @param message the message to log + * @param params parameters to the message + */ + public static void log(Level level, String message, Object... params) { + try { + MixinService.getService().getLogger("mixin.agent").log(level, message, params); + } catch (ServiceNotAvailableError err) { + System.err.printf("MixinAgent: %s: %s", level.name(), String.format(message, params)); + } + } + } diff --git a/src/agent/java/org/spongepowered/tools/agent/MixinAgentClassLoader.java b/src/agent/java/org/spongepowered/tools/agent/MixinAgentClassLoader.java index 2b4af2709..b3578f4f4 100644 --- a/src/agent/java/org/spongepowered/tools/agent/MixinAgentClassLoader.java +++ b/src/agent/java/org/spongepowered/tools/agent/MixinAgentClassLoader.java @@ -27,7 +27,7 @@ import java.util.HashMap; import java.util.Map; -import org.spongepowered.asm.logging.ILogger; +import org.spongepowered.asm.logging.Level; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -35,6 +35,7 @@ import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.service.MixinService; +import org.spongepowered.asm.service.ServiceNotAvailableError; import org.spongepowered.asm.util.Constants; /** @@ -43,8 +44,6 @@ */ class MixinAgentClassLoader extends ClassLoader { - private static final ILogger logger = MixinService.getService().getLogger("mixin.agent"); - /** * Mapping of mixin mixin classes to their fake classes */ @@ -62,7 +61,7 @@ class MixinAgentClassLoader extends ClassLoader { * @param name Name of the fake class */ void addMixinClass(String name) { - MixinAgentClassLoader.logger.debug("Mixin class {} added to class loader", name); + MixinAgentClassLoader.log(Level.DEBUG, "Mixin class {} added to class loader", name); try { byte[] bytes = this.materialise(name); Class clazz = this.defineClass(name, bytes, 0, bytes.length); @@ -71,7 +70,7 @@ void addMixinClass(String name) { clazz.getDeclaredConstructor().newInstance(); this.mixins.put(clazz, bytes); } catch (Throwable e) { - MixinAgentClassLoader.logger.catching(e); + MixinAgentClassLoader.log(Level.ERROR, "Catching {}", e); } } @@ -91,9 +90,9 @@ void addTargetClass(String name, ClassNode classNode) { classNode.accept(cw); this.targets.put(name, cw.toByteArray()); } catch (Exception ex) { - MixinAgentClassLoader.logger.error("Error storing original class bytecode for {} in mixin hotswap agent. {}: {}", + MixinAgentClassLoader.log(Level.ERROR, "Error storing original class bytecode for {} in mixin hotswap agent. {}: {}", name, ex.getClass().getName(), ex.getMessage()); - MixinAgentClassLoader.logger.debug(ex.toString()); + MixinAgentClassLoader.log(Level.DEBUG, ex.toString()); } } } @@ -144,4 +143,19 @@ private byte[] materialise(String name) { return cw.toByteArray(); } + /** + * Wrapper for logger since we can't access the log abstraction in premain + * + * @param level the logging level + * @param message the message to log + * @param params parameters to the message + */ + public static void log(Level level, String message, Object... params) { + try { + MixinService.getService().getLogger("mixin.agent").log(level, message, params); + } catch (ServiceNotAvailableError err) { + System.err.printf("MixinAgent: %s: %s", level.name(), String.format(message, params)); + } + } + } diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixin.java b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixin.java index 001bbdfae..f10832b79 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixin.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixin.java @@ -467,6 +467,11 @@ public ReferenceMapper getReferenceMapper() { public String getClassName() { return this.getClassRef().replace('/', '.'); } + + @Override + public String getTargetClassName() { + return this.primaryTarget.toString(); + } @Override public String getTargetClassRef() { diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandler.java b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandler.java index 2e7c7415f..375af9e00 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandler.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandler.java @@ -165,6 +165,11 @@ public String remap(String reference) { return reference; } + @Override + public String getElementDescription() { + return String.format("%s annotation on %s", this.getAnnotation(), this); + } + @Override public String toString() { return TypeUtils.getName(this.element); diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerAccessor.java b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerAccessor.java index d5bfd8025..f91add4af 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerAccessor.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerAccessor.java @@ -138,7 +138,7 @@ public String toString() { static class AnnotatedElementInvoker extends AnnotatedElementAccessor { private AccessorType type = AccessorType.METHOD_PROXY; - + public AnnotatedElementInvoker(ExecutableElement element, AnnotationHandle annotation, IMixinContext context, boolean shouldRemap) { super(element, annotation, context, shouldRemap); } @@ -165,16 +165,24 @@ public void attach(TypeHandle target) { for (String prefix : AccessorType.OBJECT_FACTORY.getExpectedPrefixes()) { if (prefix.equals(accessorName.prefix) - && (Constants.CTOR.equals(accessorName.name) || target.getSimpleName().equals(accessorName.name))) { + && (Constants.CTOR.equals(accessorName.name) || target.getSimpleName().equalsIgnoreCase(accessorName.name))) { this.type = AccessorType.OBJECT_FACTORY; return; } } } + @Override + public String getAnnotationValue() { + String value = super.getAnnotationValue(); + return (this.type == AccessorType.OBJECT_FACTORY && value == null) ? this.returnType.toString() : value; + } + @Override public boolean shouldRemap() { - return (this.type == AccessorType.METHOD_PROXY || this.getAnnotationValue() != null) && super.shouldRemap(); + return (this.type == AccessorType.OBJECT_FACTORY + || this.type == AccessorType.METHOD_PROXY + || this.getAnnotationValue() != null) && super.shouldRemap(); } @Override diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerInjector.java b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerInjector.java index 27d760710..1328cd9fd 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerInjector.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/AnnotatedMixinElementHandlerInjector.java @@ -253,13 +253,13 @@ private boolean registerInjector(AnnotatedElementInjector elem, String reference } } - IReferenceManager refMap = this.obf.getReferenceManager(); + IReferenceManager refMaps = this.obf.getReferenceManager(); try { // If the original owner is unspecified, and the mixin is multi-target, we strip the owner from the obf mappings if ((targetMember.getOwner() == null && this.mixin.isMultiTarget()) || target.isSimulated()) { obfData = AnnotatedMixinElementHandler.stripOwnerData(obfData); } - refMap.addMethodMapping(this.classRef, reference, obfData); + refMaps.addMethodMapping(this.classRef, reference, obfData); } catch (ReferenceConflictException ex) { String conflictType = this.mixin.isMultiTarget() ? "Multi-target" : "Target"; @@ -270,9 +270,9 @@ private boolean registerInjector(AnnotatedElementInjector elem, String reference String newName = newMember instanceof ITargetSelectorByName ? ((ITargetSelectorByName)newMember).getName() : newMember.toString(); if (oldName != null && oldName.equals(newName)) { obfData = AnnotatedMixinElementHandler.stripDescriptors(obfData); - refMap.setAllowConflicts(true); - refMap.addMethodMapping(this.classRef, reference, obfData); - refMap.setAllowConflicts(false); + refMaps.setAllowConflicts(true); + refMaps.addMethodMapping(this.classRef, reference, obfData); + refMaps.setAllowConflicts(false); // This is bad because in notch mappings, using the bare target name might cause everything to explode elem.printMessage(this.ap, MessageType.BARE_REFERENCE, "Coerced " + conflictType + " reference has conflicting descriptors for " diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/ObfuscationEnvironment.java b/src/ap/java/org/spongepowered/tools/obfuscation/ObfuscationEnvironment.java index c0c781a18..6ee81ef7f 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/ObfuscationEnvironment.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/ObfuscationEnvironment.java @@ -30,10 +30,6 @@ import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic.Kind; import org.spongepowered.asm.mixin.injection.selectors.ITargetSelectorRemappable; diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/ReferenceManager.java b/src/ap/java/org/spongepowered/tools/obfuscation/ReferenceManager.java index 850b368f3..ec1ad5544 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/ReferenceManager.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/ReferenceManager.java @@ -27,8 +27,10 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.net.URI; import java.util.List; +import javax.annotation.processing.Filer; import javax.tools.FileObject; import javax.tools.StandardLocation; @@ -129,7 +131,9 @@ public void write() { try { writer = this.newWriter(this.outRefMapFileName, "refmap"); - this.refMapper.write(writer); + if (writer != null) { + this.refMapper.write(writer); + } } catch (IOException ex) { ex.printStackTrace(); } finally { @@ -154,9 +158,26 @@ private PrintWriter newWriter(String fileName, String description) throws IOExce return new PrintWriter(outFile); } - FileObject outResource = this.ap.getProcessingEnvironment().getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", fileName); - this.ap.printMessage(MessageType.INFO, "Writing " + description + " to " + new File(outResource.toUri()).getAbsolutePath()); - return new PrintWriter(outResource.openWriter()); + try { + Filer filer = this.ap.getProcessingEnvironment().getFiler(); + FileObject outResource = null; + try { + outResource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", fileName); + } catch (Exception ex) { + // fileName is not a valid relative path? + outResource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", new File(fileName).getName()); + } + + URI resourceUri = outResource.toUri(); + String absolutePath = "file".equals(resourceUri.getScheme()) ? new File(resourceUri).getAbsolutePath() : resourceUri.toString(); + PrintWriter writer = new PrintWriter(outResource.openWriter()); + this.ap.printMessage(MessageType.INFO, "Writing " + description + " to (" + resourceUri.getScheme() + ") " + absolutePath); + return writer; + } catch (Exception ex) { + this.ap.printMessage(MessageType.ERROR, "Cannot write " + description + " to (" + fileName + "): " + ex.getClass().getName() + + ": " + ex.getMessage()); + return null; + } } /* (non-Javadoc) diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/SupportedOptions.java b/src/ap/java/org/spongepowered/tools/obfuscation/SupportedOptions.java index ac0a03a9e..1383eba36 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/SupportedOptions.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/SupportedOptions.java @@ -50,6 +50,7 @@ public final class SupportedOptions { public static final String PLUGIN_VERSION = "pluginVersion"; public static final String QUIET = "quiet"; public static final String SHOW_MESSAGE_TYPES = "showMessageTypes"; + public static final String DISABLE_INTERFACE_MIXINS = "disableInterfaceMixins"; private SupportedOptions() {} @@ -70,7 +71,8 @@ public static Set getAllOptions() { SupportedOptions.MAPPING_TYPES, SupportedOptions.PLUGIN_VERSION, SupportedOptions.QUIET, - SupportedOptions.SHOW_MESSAGE_TYPES + SupportedOptions.SHOW_MESSAGE_TYPES, + SupportedOptions.DISABLE_INTERFACE_MIXINS ); options.addAll( ObfuscationServices.getInstance().getSupportedOptions() diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/TargetMap.java b/src/ap/java/org/spongepowered/tools/obfuscation/TargetMap.java index 870005b47..590d64eb1 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/TargetMap.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/TargetMap.java @@ -39,8 +39,6 @@ import java.util.List; import java.util.Set; -import javax.lang.model.element.TypeElement; - import org.spongepowered.tools.obfuscation.mirror.TypeHandle; import org.spongepowered.tools.obfuscation.mirror.TypeReference; diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/interfaces/IMessagerEx.java b/src/ap/java/org/spongepowered/tools/obfuscation/interfaces/IMessagerEx.java index 85df92ec9..282a1f5b4 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/interfaces/IMessagerEx.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/interfaces/IMessagerEx.java @@ -425,6 +425,9 @@ public static void applyOptions(CompilerEnvironment env, IOptionProvider options // Apply the "quiet" option if specified, or if in dev env MessageType.INFO.setEnabled(!(env.isDevelopmentEnvironment() || "true".equalsIgnoreCase(options.getOption(SupportedOptions.QUIET)))); + // Selectively enable injector-in-interface as an error based on user option + MessageType.INJECTOR_IN_INTERFACE.setEnabled(options.getOption(SupportedOptions.DISABLE_INTERFACE_MIXINS, false)); + // Legacy option if ("error".equalsIgnoreCase(options.getOption(SupportedOptions.OVERWRITE_ERROR_LEVEL))) { MessageType.OVERWRITE_DOCS.setKind(Kind.ERROR); diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandle.java b/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandle.java index 5c2f1e6ac..965da9f51 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandle.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandle.java @@ -78,7 +78,7 @@ public class TypeHandle { * for related classes (eg. superclass) without using mirror */ protected final ITypeHandleProvider typeProvider; - + /** * Reference to this handle, for serialisation */ @@ -295,9 +295,11 @@ public boolean isNotInterface() { /** * Gets whether this handle is a supertype of the other handle + * + * @param other the TypeHandle to compare with */ public boolean isSuperTypeOf(TypeHandle other) { - List superTypes = new ArrayList<>(); + List superTypes = new ArrayList(); if (other.getSuperclass() != null) { superTypes.add(other.getSuperclass()); } @@ -309,7 +311,7 @@ public boolean isSuperTypeOf(TypeHandle other) { } return false; } - + /** * Get the TypeReference for this type, used for serialisation */ diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandleSimulated.java b/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandleSimulated.java index bdb44612e..f565f0bbf 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandleSimulated.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeHandleSimulated.java @@ -160,7 +160,8 @@ public MappingMethod getMappingMethod(String name, String desc) { } - private static MethodHandle findMethodRecursive(TypeHandle target, String name, String signature, String rawSignature, boolean matchCase, ITypeHandleProvider typeProvider) { + private static MethodHandle findMethodRecursive(TypeHandle target, String name, String signature, String rawSignature, boolean matchCase, + ITypeHandleProvider typeProvider) { TypeElement elem = target.getTargetElement(); if (elem == null) { return null; @@ -186,7 +187,8 @@ private static MethodHandle findMethodRecursive(TypeHandle target, String name, return TypeHandleSimulated.findMethodRecursive(superClass, name, signature, rawSignature, matchCase, typeProvider); } - private static MethodHandle findMethodRecursive(TypeMirror target, String name, String signature, String rawSignature, boolean matchCase, ITypeHandleProvider typeProvider) { + private static MethodHandle findMethodRecursive(TypeMirror target, String name, String signature, String rawSignature, boolean matchCase, + ITypeHandleProvider typeProvider) { if (!(target instanceof DeclaredType)) { return null; } diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeUtils.java b/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeUtils.java index c194ed923..709a6eaff 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeUtils.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/mirror/TypeUtils.java @@ -275,12 +275,9 @@ public static String getJavaSignature(Element element) { * @return java signature */ public static String getJavaSignature(String descriptor) { - /* Fabric: Work around https://github.com/SpongePowered/Mixin/issues/560 */ if (!descriptor.contains("(")) { return SignaturePrinter.getTypeName(org.objectweb.asm.Type.getType(descriptor), false, true); } - /* End fabric */ - return new SignaturePrinter("", descriptor).setFullyQualified(true).toDescriptor(); } diff --git a/src/ap/java/org/spongepowered/tools/obfuscation/validation/TargetValidator.java b/src/ap/java/org/spongepowered/tools/obfuscation/validation/TargetValidator.java index 00a0e41d7..168e92b5c 100644 --- a/src/ap/java/org/spongepowered/tools/obfuscation/validation/TargetValidator.java +++ b/src/ap/java/org/spongepowered/tools/obfuscation/validation/TargetValidator.java @@ -29,6 +29,9 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @@ -111,7 +114,8 @@ private void validateClassMixin(TypeElement mixin, Collection target } private boolean validateSuperClass(TypeHandle targetType, TypeHandle superClass) { - return targetType.isImaginary() || targetType.isSimulated() || superClass.isSuperTypeOf(targetType) || this.checkMixinsFor(targetType, superClass); + return targetType.isImaginary() || targetType.isSimulated() || superClass.isSuperTypeOf(targetType) + || this.checkMixinsFor(targetType, superClass); } private boolean checkMixinsFor(TypeHandle targetType, TypeHandle superMixin) { diff --git a/src/bridge/java/org/spongepowered/asm/bridge/RemapperAdapter.java b/src/bridge/java/org/spongepowered/asm/bridge/RemapperAdapter.java index c373ac278..b7ca115cb 100644 --- a/src/bridge/java/org/spongepowered/asm/bridge/RemapperAdapter.java +++ b/src/bridge/java/org/spongepowered/asm/bridge/RemapperAdapter.java @@ -53,7 +53,7 @@ public String toString() { @Override public String mapMethodName(String owner, String name, String desc) { this.logger.debug("{} is remapping method {}{} for {}", this, name, desc, owner); - if(!supportsNullArguments && (owner == null || name == null || desc == null)) { + if (!supportsNullArguments && (owner == null || name == null || desc == null)) { return name; } String newName = this.remapper.mapMethodName(owner, name, desc); @@ -69,7 +69,7 @@ public String mapMethodName(String owner, String name, String desc) { @Override public String mapFieldName(String owner, String name, String desc) { this.logger.debug("{} is remapping field {}{} for {}", this, name, desc, owner); - if(!supportsNullArguments && (owner == null || name == null || desc == null)) { + if (!supportsNullArguments && (owner == null || name == null || desc == null)) { return name; } String newName = this.remapper.mapFieldName(owner, name, desc); @@ -85,7 +85,7 @@ public String mapFieldName(String owner, String name, String desc) { @Override public String map(String typeName) { this.logger.debug("{} is remapping class {}", this, typeName); - if(typeName == null) { + if (typeName == null) { return typeName; } return this.remapper.map(typeName); @@ -93,7 +93,7 @@ public String map(String typeName) { @Override public String unmap(String typeName) { - if(typeName == null) { + if (typeName == null) { return typeName; } return typeName; @@ -101,7 +101,7 @@ public String unmap(String typeName) { @Override public String mapDesc(String desc) { - if(desc == null) { + if (desc == null) { return desc; } return this.remapper.mapDesc(desc); @@ -109,7 +109,7 @@ public String mapDesc(String desc) { @Override public String unmapDesc(String desc) { - if(desc == null) { + if (desc == null) { return desc; } String newDesc = ObfuscationUtil.unmapDescriptor(desc, this); diff --git a/src/fernflower/java/org/spongepowered/asm/mixin/transformer/debug/RuntimeDecompiler.java b/src/fernflower/java/org/spongepowered/asm/mixin/transformer/debug/RuntimeDecompiler.java index 18b8ca040..c703639e2 100644 --- a/src/fernflower/java/org/spongepowered/asm/mixin/transformer/debug/RuntimeDecompiler.java +++ b/src/fernflower/java/org/spongepowered/asm/mixin/transformer/debug/RuntimeDecompiler.java @@ -45,8 +45,6 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; -import com.google.common.io.MoreFiles; -import com.google.common.io.RecursiveDeleteOption; /** * Wrapper for FernFlower to support runtime-decompilation of post-mixin classes @@ -73,7 +71,7 @@ public RuntimeDecompiler(File outputPath) { this.outputPath = outputPath; if (this.outputPath.exists()) { try { - MoreFiles.deleteRecursively(this.outputPath.toPath(), RecursiveDeleteOption.ALLOW_INSECURE); + org.spongepowered.asm.util.Files.deleteRecursively(this.outputPath); } catch (IOException ex) { this.logger.debug("Error cleaning output directory: {}", ex.getMessage()); } diff --git a/src/launchwrapper/java/org/spongepowered/asm/service/mojang/MixinServiceLaunchWrapper.java b/src/launchwrapper/java/org/spongepowered/asm/service/mojang/MixinServiceLaunchWrapper.java index cc1610ae1..b7c94df16 100644 --- a/src/launchwrapper/java/org/spongepowered/asm/service/mojang/MixinServiceLaunchWrapper.java +++ b/src/launchwrapper/java/org/spongepowered/asm/service/mojang/MixinServiceLaunchWrapper.java @@ -612,7 +612,16 @@ public ClassNode getClassNode(String className) throws ClassNotFoundException, I */ @Override public ClassNode getClassNode(String className, boolean runTransformers) throws ClassNotFoundException, IOException { - return this.getClassNode(className, this.getClassBytes(className, true), ClassReader.EXPAND_FRAMES); + return this.getClassNode(className, this.getClassBytes(className, runTransformers), ClassReader.EXPAND_FRAMES); + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.service.IClassBytecodeProvider#getClassNode( + * java.lang.String, boolean, int) + */ + @Override + public ClassNode getClassNode(String className, boolean runTransformers, int flags) throws ClassNotFoundException, IOException { + return this.getClassNode(className, this.getClassBytes(className, runTransformers), flags); } /** @@ -648,7 +657,7 @@ private static int findInStackTrace(String className, String methodName) { /** Initialize lazily so we don't reference LaunchWrapper's class loader before it has initialized. */ private LaunchClassLoaderUtil getClassLoaderUtil() { - if(this.classLoaderUtil == null) { + if (this.classLoaderUtil == null) { this.classLoaderUtil = new LaunchClassLoaderUtil(Launch.classLoader); } return this.classLoaderUtil; diff --git a/src/main/java/org/spongepowered/asm/launch/GlobalProperties.java b/src/main/java/org/spongepowered/asm/launch/GlobalProperties.java index b1ba1cab5..8913edefc 100644 --- a/src/main/java/org/spongepowered/asm/launch/GlobalProperties.java +++ b/src/main/java/org/spongepowered/asm/launch/GlobalProperties.java @@ -72,6 +72,11 @@ IPropertyKey resolve(IGlobalPropertyService service) { return this.key = service.resolveKey(this.name); } + + @Override + public String toString() { + return this.name; + } /** * Get or create a new global property key @@ -92,13 +97,6 @@ public static Keys of(String name) { return key; } - /* (non-Javadoc) - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return this.name; - } } private static IGlobalPropertyService service; diff --git a/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java b/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java index 8dbc050af..a338a4510 100644 --- a/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java +++ b/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java @@ -65,7 +65,7 @@ public abstract class MixinBootstrap { /** * Subsystem version */ - public static final String VERSION = "0.8.5"; + public static final String VERSION = "0.8.7"; /** * Transformer factory diff --git a/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformAgentDefault.java b/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformAgentDefault.java index 926626cc7..e55143094 100644 --- a/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformAgentDefault.java +++ b/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformAgentDefault.java @@ -43,7 +43,7 @@ public void prepare() { String mixinConfigs = this.handle.getAttribute(ManifestAttributes.MIXINCONFIGS); if (mixinConfigs != null) { for (String config : mixinConfigs.split(",")) { - this.manager.addConfig(config.trim()); + this.manager.addConfig(config.trim(), this.handle); } } diff --git a/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformManager.java b/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformManager.java index 23fffa33e..4491a25df 100644 --- a/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformManager.java +++ b/src/main/java/org/spongepowered/asm/launch/platform/MixinPlatformManager.java @@ -34,15 +34,16 @@ import java.util.Map; import org.spongepowered.asm.launch.platform.container.ContainerHandleURI; -import org.spongepowered.asm.logging.ILogger; import org.spongepowered.asm.launch.platform.container.IContainerHandle; +import org.spongepowered.asm.logging.ILogger; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; import org.spongepowered.asm.mixin.MixinEnvironment.Phase; -import org.spongepowered.asm.service.MixinService; -import org.spongepowered.asm.service.ServiceVersionError; import org.spongepowered.asm.mixin.Mixins; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigSource; import org.spongepowered.asm.mixin.throwables.MixinError; +import org.spongepowered.asm.service.MixinService; +import org.spongepowered.asm.service.ServiceVersionError; //import com.google.common.collect.ImmutableList; @@ -173,7 +174,7 @@ public final void prepare(CommandLineOptions args) { } for (String config : args.getConfigs()) { - this.addConfig(config); + this.addConfig(config, null); } } @@ -263,10 +264,10 @@ final void setCompatibilityLevel(String level) { * * @param config config resource name, does not require a leading / */ - final void addConfig(String config) { + final void addConfig(String config, IMixinConfigSource source) { if (config.endsWith(".json")) { - MixinPlatformManager.logger.debug("Registering mixin config: {}", config); - Mixins.addConfiguration(config); + MixinPlatformManager.logger.debug("Registering mixin config: {} source={}", config, source); + Mixins.addConfiguration(config, source); } else if (config.contains(".json@")) { throw new MixinError("Setting config phase via manifest is no longer supported: " + config + ". Specify target in config instead"); } diff --git a/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleURI.java b/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleURI.java index 95108fd70..c65328de6 100644 --- a/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleURI.java +++ b/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleURI.java @@ -53,6 +53,17 @@ public ContainerHandleURI(URI uri) { this.attributes = MainAttributes.of(uri); } + @Override + public String getId() { + // Not enough information for this + return null; + } + + @Override + public String getDescription() { + return this.uri.toString(); + } + /** * Get the URI for this handle */ diff --git a/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleVirtual.java b/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleVirtual.java index 0e7129bcc..7db24cb02 100644 --- a/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleVirtual.java +++ b/src/main/java/org/spongepowered/asm/launch/platform/container/ContainerHandleVirtual.java @@ -62,6 +62,16 @@ public ContainerHandleVirtual(String name) { this.name = name; } + @Override + public String getId() { + return this.name; + } + + @Override + public String getDescription() { + return this.toString(); + } + /** * Get the name of this container */ diff --git a/src/main/java/org/spongepowered/asm/launch/platform/container/IContainerHandle.java b/src/main/java/org/spongepowered/asm/launch/platform/container/IContainerHandle.java index 0d05af9d8..cc18d7303 100644 --- a/src/main/java/org/spongepowered/asm/launch/platform/container/IContainerHandle.java +++ b/src/main/java/org/spongepowered/asm/launch/platform/container/IContainerHandle.java @@ -26,13 +26,15 @@ import java.util.Collection; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigSource; + /** * Interface for container handles. Previously resources considered by Mixin * were indexed by URI but in order to provide more flexibility the container * handle system now wraps URIs in a more expressive object, and provides * support for both virtual containers and nested container resolution. */ -public interface IContainerHandle { +public interface IContainerHandle extends IMixinConfigSource { /** * Retrieve the value of attribute with the specified name, or null if not diff --git a/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java b/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java index b11673bf5..f68c3ed3f 100644 --- a/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java +++ b/src/main/java/org/spongepowered/asm/mixin/FabricUtil.java @@ -33,9 +33,26 @@ public final class FabricUtil { public static final String KEY_COMPATIBILITY = "fabric-compat"; // fabric mixin version compatibility boundaries, (major * 1000 + minor) * 1000 + patch + + /** + * Fabric compatibility version 0.9.2 + */ public static final int COMPATIBILITY_0_9_2 = 9002; // 0.9.2+mixin.0.8.2 incompatible local variable handling + + /** + * Fabric compatibility version 0.10.0 + */ public static final int COMPATIBILITY_0_10_0 = 10000; // 0.10.0+mixin.0.8.4 - public static final int COMPATIBILITY_LATEST = COMPATIBILITY_0_10_0; + + /** + * Fabric compatibility version 0.14.0 + */ + public static final int COMPATIBILITY_0_14_0 = 14000; // 0.14.0+mixin.0.8.6 + + /** + * Latest compatibility version + */ + public static final int COMPATIBILITY_LATEST = COMPATIBILITY_0_14_0; public static String getModId(IMixinConfig config) { return getModId(config, "(unknown)"); @@ -64,4 +81,7 @@ private static T getDecoration(IMixinConfig config, String key, T defaultVal return defaultValue; } } + + private FabricUtil() { + } } diff --git a/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java b/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java index 915067f2e..3c9633971 100644 --- a/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java +++ b/src/main/java/org/spongepowered/asm/mixin/MixinEnvironment.java @@ -38,6 +38,7 @@ import org.spongepowered.asm.launch.GlobalProperties; import org.spongepowered.asm.launch.GlobalProperties.Keys; import org.spongepowered.asm.launch.MixinBootstrap; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.extensibility.IEnvironmentTokenProvider; import org.spongepowered.asm.mixin.injection.At; @@ -330,7 +331,7 @@ public static enum Option { /** * Parent for environment settings */ - ENVIRONMENT(Inherit.ALWAYS_FALSE, "env"), + ENVIRONMENT(Inherit.ALWAYS_FALSE, true, "env"), /** * Force refmap obf type when required @@ -412,7 +413,20 @@ public static enum Option { * Behaviour for initialiser injections, current supported options are * "default" and "safe" */ - INITIALISER_INJECTION_MODE("initialiserInjectionMode", "default"); + INITIALISER_INJECTION_MODE("initialiserInjectionMode", "default"), + + /** + * Parent for tunable settings + */ + TUNABLE(Inherit.ALWAYS_FALSE, true, "tunable"), + + /** + * Tunable for the Mixin ClassReader behaviour, setting this option to + * true will cause the Mixin ClassReader to read mixin bytecode + * with {@link ClassReader#EXPAND_FRAMES} flag which restores the + * behaviour from versions 0.8.6 and below, newer versions default to 0. + */ + CLASSREADER_EXPAND_FRAMES(Option.TUNABLE, Inherit.INDEPENDENT, "classReaderExpandFrames", true, "false"); /** * Type of inheritance for options @@ -460,6 +474,11 @@ private enum Inherit { */ final Inherit inheritance; + /** + * Do not print this option in the masthead output + */ + final boolean isHidden; + /** * Java property name */ @@ -488,6 +507,10 @@ private Option(Inherit inheritance, String property) { this(null, inheritance, property, true); } + private Option(Inherit inheritance, boolean hidden, String property) { + this(null, inheritance, hidden, property, true); + } + private Option(String property, boolean flag) { this(null, property, flag); } @@ -500,29 +523,58 @@ private Option(Option parent, String property) { this(parent, Inherit.INHERIT, property, true); } + private Option(Option parent, boolean hidden, String property) { + this(parent, Inherit.INHERIT, hidden, property, true); + } + private Option(Option parent, Inherit inheritance, String property) { this(parent, inheritance, property, true); } + private Option(Option parent, Inherit inheritance, boolean hidden, String property) { + this(parent, inheritance, hidden, property, true); + } + private Option(Option parent, String property, boolean isFlag) { this(parent, Inherit.INHERIT, property, isFlag, null); } + private Option(Option parent, boolean hidden, String property, boolean isFlag) { + this(parent, Inherit.INHERIT, hidden, property, isFlag, null); + } + private Option(Option parent, Inherit inheritance, String property, boolean isFlag) { this(parent, inheritance, property, isFlag, null); } + private Option(Option parent, Inherit inheritance, boolean hidden, String property, boolean isFlag) { + this(parent, inheritance, hidden, property, isFlag, null); + } + private Option(Option parent, String property, String defaultStringValue) { this(parent, Inherit.INHERIT, property, false, defaultStringValue); } + private Option(Option parent, boolean hidden, String property, String defaultStringValue) { + this(parent, Inherit.INHERIT, hidden, property, false, defaultStringValue); + } + private Option(Option parent, Inherit inheritance, String property, String defaultStringValue) { this(parent, inheritance, property, false, defaultStringValue); } + private Option(Option parent, Inherit inheritance, boolean hidden, String property, String defaultStringValue) { + this(parent, inheritance, hidden, property, false, defaultStringValue); + } + private Option(Option parent, Inherit inheritance, String property, boolean isFlag, String defaultStringValue) { + this(parent, inheritance, false, property, isFlag, defaultStringValue); + } + + private Option(Option parent, Inherit inheritance, boolean hidden, String property, boolean isFlag, String defaultStringValue) { this.parent = parent; this.inheritance = inheritance; + this.isHidden = hidden; this.property = (parent != null ? parent.property : Option.PREFIX) + "." + property; this.defaultValue = defaultStringValue; this.isFlag = isFlag; @@ -754,63 +806,63 @@ boolean isSupported() { } }, + + /** + * Java 19 or above is required + */ + JAVA_19(19, Opcodes.V19, 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_19 && ASM.isAtLeastVersion(9, 3); + } + + }, + + /** + * Java 20 or above is required + */ + JAVA_20(20, Opcodes.V20, 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_20 && ASM.isAtLeastVersion(9, 4); + } + + }, + + /** + * Java 21 or above is required + */ + JAVA_21(21, Opcodes.V21, 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_21 && ASM.isAtLeastVersion(9, 5); + } - /** - * Java 19 or above is required - */ - JAVA_19(19, Opcodes.V19, 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_19 && ASM.isAtLeastVersion(9, 3); - } - - }, - - /** - * Java 20 or above is required - */ - JAVA_20(20, Opcodes.V20, 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_20 && ASM.isAtLeastVersion(9, 4); - } - - }, - - /** - * Java 21 or above is required - */ - JAVA_21(21, Opcodes.V21, 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_21 && ASM.isAtLeastVersion(9, 5); - } - - }, + }, - /** - * Java 22 or above is required - */ - JAVA_22(22, Opcodes.V22, 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) { + /** + * Java 22 or above is required + */ + JAVA_22(22, Opcodes.V22, 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_22 && ASM.isAtLeastVersion(9, 6); - } + @Override + boolean isSupported() { + return JavaVersion.current() >= JavaVersion.JAVA_22 && ASM.isAtLeastVersion(9, 6); + } - }, - ; + }, + ; /** * Default compatibility level to use if not specified by the service @@ -980,6 +1032,24 @@ public static CompatibilityLevel requiredFor(int languageFeatures) { } return null; } + + /** + * Return the maximum compatibility level which is actually effective in + * the current runtime, taking into account the current JRE and ASM + * versions + */ + public static CompatibilityLevel getMaxEffective() { + CompatibilityLevel max = CompatibilityLevel.JAVA_6; + for (CompatibilityLevel level : CompatibilityLevel.values()) { + if (level.isSupported()) { + max = level; + } + if (level == CompatibilityLevel.MAX_SUPPORTED) { + break; + } + } + return max; + } static String getSupportedVersions() { StringBuilder sb = new StringBuilder(); @@ -1013,6 +1083,113 @@ static String getSupportedVersions() { } + /** + * Mixin features which can be specified in mixin configs as required for + * the config to be valid. No support for backward compatibility but should + * help in the future to allow mixin configs to specify the features they + * require rather than relying purely on minVersion. Only applies to + * features added in version 0.8.6 and higher. + */ + public static enum Feature { + + /** + * Supports the unsafe flag on @At annotations to + * facilitate hassle-free constructor injections. + */ + UNSAFE_INJECTION(true), + + /** + * Support for the use of injector annotations in interface mixins + */ + INJECTORS_IN_INTERFACE_MIXINS { + + @Override + public boolean isAvailable() { + return CompatibilityLevel.getMaxEffective().supports(LanguageFeatures.METHODS_IN_INTERFACES); + } + + @Override + public boolean isEnabled() { + return MixinEnvironment.getCompatibilityLevel().supports(LanguageFeatures.METHODS_IN_INTERFACES); + } + + }; + + /** + * Existence of the enum constant does not necessarily indicate that the + * feature is actually supported by this version, for example if + * features can not be supported by future or previous JVM versions and + * need to be selectively enabled. This also allows us to add flags in + * the future to disable certain features globally for testing reasons. + */ + private boolean enabled; + + private Feature() { + this(false); + } + + private Feature(boolean enabled) { + this.enabled = enabled; + } + + /** + * Get whether this feature is available in the current runtime + * environment + */ + public boolean isAvailable() { + return true; + } + + /** + * Get whether this feature is supported in the current environment and + * compatibility level + */ + public boolean isEnabled() { + return this.isAvailable() && this.enabled; + } + + /** + * Convenience function which returns a Feature constant based on the + * feature id, but returns null instead of throwing an exception. + * + * @param featureId Feature ID (enum constant name) to check for + * @return Feature or null + */ + public static Feature get(String featureId) { + if (featureId == null) { + return null; + } + try { + return Feature.valueOf(featureId); + } catch (IllegalArgumentException ex) { + return null; + } + } + + /** + * Check whether a particular feature exists in this mixin version, even + * if it's not currently available + * + * @param featureId Feature ID (enum constant name) to check for + * @return true if the feature exists + */ + public static boolean exists(String featureId) { + return Feature.get(featureId) != null; + } + + /** + * Check whether a particular feature is available and enabled + * + * @param featureId Feature ID (enum constant name) to check for + * @return true if the feature is currently available + */ + public static boolean isActive(String featureId) { + Feature feature = Feature.get(featureId); + return feature != null && feature.isEnabled(); + } + + } + /** * Wrapper for providing a natural sorting order for providers */ @@ -1186,6 +1363,7 @@ private void printHeader(Object version) { printer.kv("Internal Version", version); printer.kv("Java Version", "%s (supports compatibility %s)", JavaVersion.current(), CompatibilityLevel.getSupportedVersions()); printer.kv("Default Compatibility Level", MixinEnvironment.getCompatibilityLevel()); + printer.kv("Max Effective Compatibility Level", CompatibilityLevel.getMaxEffective()); printer.kv("Detected ASM Version", ASM.getVersionString()); printer.kv("Detected ASM Supports Java", ASM.getClassVersionString()).hr(); printer.kv("Service Name", serviceName); @@ -1193,12 +1371,19 @@ private void printHeader(Object version) { printer.kv("Global Property Service Class", MixinService.getGlobalPropertyService().getClass().getName()); printer.kv("Logger Adapter Type", MixinService.getService().getLogger("mixin").getType()).hr(); for (Option option : Option.values()) { + if (option.isHidden) { + continue; + } StringBuilder indent = new StringBuilder(); for (int i = 0; i < option.depth; i++) { indent.append("- "); } printer.kv(option.property, "%s<%s>", indent, option); } + printer.hr(); + for (Feature feature : Feature.values()) { + printer.kv(feature.name(), "available=<%s> enabled=<%s>", feature.isAvailable(), feature.isEnabled()); + } printer.hr().kv("Detected Side", side); printer.print(System.err); } diff --git a/src/main/java/org/spongepowered/asm/mixin/Mixins.java b/src/main/java/org/spongepowered/asm/mixin/Mixins.java index e3f8164b2..675f621f6 100644 --- a/src/main/java/org/spongepowered/asm/mixin/Mixins.java +++ b/src/main/java/org/spongepowered/asm/mixin/Mixins.java @@ -32,6 +32,7 @@ import org.spongepowered.asm.logging.ILogger; import org.spongepowered.asm.launch.GlobalProperties; import org.spongepowered.asm.launch.GlobalProperties.Keys; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigSource; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.mixin.transformer.ClassInfo; import org.spongepowered.asm.mixin.transformer.Config; @@ -68,13 +69,23 @@ private Mixins() {} /** * Add multiple configurations - * + * * @param configFiles config resources to add */ public static void addConfigurations(String... configFiles) { + Mixins.addConfigurations(configFiles, null); + } + + /** + * Add multiple configurations + * + * @param configFiles config resources to add + * @param source source of the configuration resource + */ + public static void addConfigurations(String[] configFiles, IMixinConfigSource source) { MixinEnvironment fallback = MixinEnvironment.getDefaultEnvironment(); for (String configFile : configFiles) { - Mixins.createConfiguration(configFile, fallback); + Mixins.createConfiguration(configFile, fallback, source); } } @@ -84,20 +95,30 @@ public static void addConfigurations(String... configFiles) { * @param configFile path to configuration resource */ public static void addConfiguration(String configFile) { - Mixins.createConfiguration(configFile, MixinEnvironment.getDefaultEnvironment()); + Mixins.addConfiguration(configFile, (IMixinConfigSource) null); + } + + /** + * Add a mixin configuration resource + * + * @param configFile path to configuration resource + * @param source source of the configuration resource + */ + public static void addConfiguration(String configFile, IMixinConfigSource source) { + Mixins.createConfiguration(configFile, MixinEnvironment.getDefaultEnvironment(), source); } @Deprecated static void addConfiguration(String configFile, MixinEnvironment fallback) { - Mixins.createConfiguration(configFile, fallback); + Mixins.createConfiguration(configFile, fallback, null); } @SuppressWarnings("deprecation") - private static void createConfiguration(String configFile, MixinEnvironment fallback) { + private static void createConfiguration(String configFile, MixinEnvironment fallback, IMixinConfigSource source) { Config config = null; try { - config = Config.create(configFile, fallback); + config = Config.create(configFile, fallback, source); } catch (Exception ex) { Mixins.logger.error("Error encountered reading mixin config " + configFile + ": " + ex.getClass().getName() + " " + ex.getMessage(), ex); } diff --git a/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfig.java b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfig.java index 611d6371a..c74a05bc3 100644 --- a/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfig.java +++ b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfig.java @@ -52,7 +52,22 @@ public interface IMixinConfig { * @return the config filename (resource name) */ public abstract String getName(); - + + /** + * Get the source which initially provide this configuration object, for + * example the jar which specified it. + * + * @return the config source, or null if not provided by the service + */ + public abstract IMixinConfigSource getSource(); + + /** + * Get the id of the source id with all non-alpha characters removed. If the + * source is null or the source's id is null then this method returns null + * also. + */ + public abstract String getCleanSourceId(); + /** * Get the package containing all mixin classes * diff --git a/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfigSource.java b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfigSource.java new file mode 100644 index 000000000..96d5f6a3e --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/extensibility/IMixinConfigSource.java @@ -0,0 +1,44 @@ +/* + * 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.extensibility; + +/** + * Interface for loaded mixin configurations + */ +public interface IMixinConfigSource { + + /** + * Get the identifier for this source + */ + public abstract String getId(); + + /** + * Plain text description of the config source, can be as descriptive as + * necessary to help the user identify the source, for example could be the + * full path to the source item, or something more descriptive. + */ + public abstract String getDescription(); + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGenerator.java b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGenerator.java index bd93a7bda..d604ca594 100644 --- a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGenerator.java +++ b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGenerator.java @@ -44,13 +44,19 @@ public abstract class AccessorGenerator { protected final AccessorInfo info; /** - * True for static field, false for instance field + * True for static target, false for instance target */ protected final boolean targetIsStatic; + + /** + * True if the target is in an interface + */ + protected final boolean targetIsInterface; public AccessorGenerator(AccessorInfo info, boolean isStatic) { this.info = info; this.targetIsStatic = isStatic; + this.targetIsInterface = info.getTargetClassInfo().isInterface(); } protected void checkModifiers() { diff --git a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldGetter.java b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldGetter.java index b34a592e3..1ca0886fb 100644 --- a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldGetter.java +++ b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldGetter.java @@ -49,7 +49,7 @@ public MethodNode generate() { method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); } int opcode = this.targetIsStatic ? Opcodes.GETSTATIC : Opcodes.GETFIELD; - method.instructions.add(new FieldInsnNode(opcode, this.info.getClassNode().name, this.targetField.name, this.targetField.desc)); + method.instructions.add(new FieldInsnNode(opcode, this.info.getTargetClassNode().name, this.targetField.name, this.targetField.desc)); method.instructions.add(new InsnNode(this.targetType.getOpcode(Opcodes.IRETURN))); return method; } diff --git a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldSetter.java b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldSetter.java index 9219fe7e6..654a6f166 100644 --- a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldSetter.java +++ b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorFieldSetter.java @@ -91,7 +91,7 @@ public MethodNode generate() { } method.instructions.add(new VarInsnNode(this.targetType.getOpcode(Opcodes.ILOAD), stackSpace)); int opcode = this.targetIsStatic ? Opcodes.PUTSTATIC : Opcodes.PUTFIELD; - method.instructions.add(new FieldInsnNode(opcode, this.info.getClassNode().name, this.targetField.name, this.targetField.desc)); + method.instructions.add(new FieldInsnNode(opcode, this.info.getTargetClassNode().name, this.targetField.name, this.targetField.desc)); method.instructions.add(new InsnNode(Opcodes.RETURN)); return method; } diff --git a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorObjectFactory.java b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorObjectFactory.java index bcbc16ff6..fbcfcc2f9 100644 --- a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorObjectFactory.java +++ b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorGeneratorObjectFactory.java @@ -52,7 +52,7 @@ public MethodNode generate() { int size = Bytecode.getArgsSize(this.argTypes) + (returnSize * 2); MethodNode method = this.createMethod(size, size); - String className = this.info.getClassNode().name; + String className = this.info.getTargetClassNode().name; method.instructions.add(new TypeInsnNode(Opcodes.NEW, className)); method.instructions.add(new InsnNode(returnSize == 1 ? Opcodes.DUP : Opcodes.DUP2)); Bytecode.loadArgs(this.argTypes, method.instructions, 0); diff --git a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorInfo.java b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorInfo.java index 7493f67d4..0a02b504c 100644 --- a/src/main/java/org/spongepowered/asm/mixin/gen/AccessorInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/gen/AccessorInfo.java @@ -516,7 +516,8 @@ protected TNode findTarget(List> nodes) { try { return result.getSingleResult(true); } catch (IllegalStateException ex) { - throw new InvalidAccessorException(this, ex.getMessage() + " matching " + this.target + " in " + this.classNode.name + " for " + this); + throw new InvalidAccessorException(this, String.format("%s matching %s in %s for %s", + ex.getMessage(), this.target, this.classNode.name, this)); } } diff --git a/src/main/java/org/spongepowered/asm/mixin/gen/InvokerInfo.java b/src/main/java/org/spongepowered/asm/mixin/gen/InvokerInfo.java index e288ac0a9..64b6def92 100644 --- a/src/main/java/org/spongepowered/asm/mixin/gen/InvokerInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/gen/InvokerInfo.java @@ -110,7 +110,7 @@ private MethodNode findTargetMethod() { try { return result.getSingleResult(true); } catch (IllegalStateException ex) { - String message = ex.getMessage() + " matching " + this.target + " in " + this.classNode.name + " for " + this; + String message = String.format("%s matching %s in %s for %s", ex.getMessage(), this.target, this.classNode.name, this); if (this.type == AccessorType.METHOD_PROXY && this.specifiedName != null && this.target instanceof ITargetSelectorByName) { String name = ((ITargetSelectorByName)this.target).getName(); if (name != null && (name.contains(".") || name.contains("/"))) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/At.java b/src/main/java/org/spongepowered/asm/mixin/injection/At.java index 6136d5800..64ce23cd1 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/At.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/At.java @@ -209,4 +209,39 @@ public enum Shift { */ public boolean remap() default true; + /** + * In general, injecting into constructors should be treated with care, + * since compiled constructors - unlike regular methods - contain other + * structural elements of the class, including implied (when not explicit) + * superconstructor calls, explicit superconstructor or other delegated + * constructor calls, field initialisers and code from initialiser blocks, + * and of course the code from the original "constructor". + * + *

This means that unlike targetting a regular method, where it's often + * possible to derive a reasonable injection point from the Java source, in + * a constructor such assumptions can be dangerous. For example the HEAD + * of a regular method will always mean "before the first + * instruction", however in a constructor it's not possible to reasonably + * ascertain what the first instruction will actually be without inspecting + * the bytecode. This will certainly be prior to the delegate constructor + * call, which might come as a surprise if the delegate constructor is not + * explicit. Ultimately this means that for constructor code which appears + * before the regular constructor body, the class can be in a + * partially-initialised state (during initialisers) or in a + * fully-uninitialised state (prior to the delegate constructor call).

+ * + *

Because of this, by default certain injectors restrict usage to only + * RETURN opcodes when targetting a constructor, in order to ensure + * that the consumers are properly aware of the potential pitfalls. Whilst + * it was previously necessary to create a custom injection point in order + * to bypass this restriction, setting this option to true will + * also allow other injectors to act upon constructors, though care should + * be taken to ensure that the target is properly specified and attention is + * paid to the structure of the target bytecode.

+ * + * FABRIC CHANGE: true by default. + * + */ + public boolean unsafe() default true; + } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/Desc.java b/src/main/java/org/spongepowered/asm/mixin/injection/Desc.java index 97ec046b9..b30f45bd7 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/Desc.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/Desc.java @@ -115,7 +115,7 @@ * The next elements of this descriptor path, evaluated in order for each * recurse point. */ - // public Next[] next() default { }; + public Next[] next() default { }; /** * The minimum number of times this selector should match. By default the diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/Inject.java b/src/main/java/org/spongepowered/asm/mixin/injection/Inject.java index d3122829b..930160ca3 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/Inject.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/Inject.java @@ -293,4 +293,27 @@ */ public String constraints() default ""; + /** + * By default almost all injectors for a target class apply their injections + * at the same time. In other words, if multiple mixins target the same + * class then injectors are applied in priority order (since the mixins + * themselves are merged in priority order, and injectors run in the order + * they were merged). The exception being redirect injectors, which apply in + * a later pass. + * + *

The default order for injectors is 1000, and redirect + * injectors use 10000.

+ * + *

Specifying a value for order alters this default behaviour + * and causes the injector to inject either earlier or later than it + * normally would. For example specifying 900 will cause the + * injector to apply before others, while 1100 will apply later. + * Injectors with the same order will still apply in order of their + * mixin's priority. + * + * @return the application order for this injector, uses DEFAULT (1000) if + * not specified + */ + public int order() default 1000; + } 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 fb3bd5439..e87f3e871 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/InjectionPoint.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/InjectionPoint.java @@ -247,6 +247,35 @@ enum ShiftByViolationBehaviour { } + /** + * Boolean extensions for the At parser, mainly to avoid having to add many + * booleans in the future + */ + public static final class Flags { + + /** + * Change the default target restriction from METHODS_ONLY to ALLOW_ALL + */ + public static final int UNSAFE = 1; + + public static int parse(At at) { + int flags = 0; + if (at.unsafe()) { + flags |= Flags.UNSAFE; + } + return flags; + } + + public static int parse(AnnotationNode at) { + int flags = 0; + if (Annotations.getValue(at, "unsafe", Boolean.TRUE)) { + flags |= InjectionPoint.Flags.UNSAFE; + } + return flags; + } + + } + /** * Initial limit on the value of {@link At#by} which triggers warning/error * (based on environment) @@ -277,6 +306,7 @@ enum ShiftByViolationBehaviour { InjectionPoint.registerBuiltIn(AfterStoreLocal.class); InjectionPoint.registerBuiltIn(BeforeFinalReturn.class); InjectionPoint.registerBuiltIn(BeforeConstant.class); + InjectionPoint.registerBuiltIn(ConstructorHead.class); } private final String slice; @@ -284,13 +314,14 @@ enum ShiftByViolationBehaviour { private final String id; private final IMessageSink messageSink; + private RestrictTargetLevel targetRestriction; protected InjectionPoint() { this("", Specifier.DEFAULT, null); } protected InjectionPoint(InjectionPointData data) { - this(data.getSlice(), data.getSpecifier(), data.getId(), data.getMessageSink()); + this(data.getSlice(), data.getSpecifier(), data.getId(), data.getMessageSink(), data.getTargetRestriction()); } public InjectionPoint(String slice, Specifier specifier, String id) { @@ -298,10 +329,15 @@ public InjectionPoint(String slice, Specifier specifier, String id) { } public InjectionPoint(String slice, Specifier specifier, String id, IMessageSink messageSink) { + this(slice, specifier, id, messageSink, RestrictTargetLevel.METHODS_ONLY); + } + + public InjectionPoint(String slice, Specifier specifier, String id, IMessageSink messageSink, RestrictTargetLevel targetRestriction) { this.slice = slice; this.specifier = specifier; this.id = id; this.messageSink = messageSink; + this.targetRestriction = targetRestriction; } public String getSlice() { @@ -346,6 +382,13 @@ public boolean checkPriority(int targetPriority, int mixinPriority) { return targetPriority < mixinPriority; } + /** + * Set a new target restriction level for this injection point + */ + protected void setTargetRestriction(RestrictTargetLevel targetRestriction) { + this.targetRestriction = targetRestriction; + } + /** * Returns the target restriction level for this injection point. This level * defines whether an injection point is valid in its current state when @@ -355,20 +398,7 @@ public boolean checkPriority(int targetPriority, int mixinPriority) { * @return restriction level */ public RestrictTargetLevel getTargetRestriction(IInjectionPointContext context) { - return RestrictTargetLevel.CONSTRUCTORS_AFTER_DELEGATE; // Fabric change: allow inject in constructors - } - - /** - * Returns the target restriction level for this injection point's cancellation for - * {@literal @Inject} annotations. This level defines whether an injection point - * can declare {@code cancellable = true}. - * - * @param context injection-specific context - * @return restriction level - */ - // Fabric addition: prevent cancellation of inject in constructors - public RestrictTargetLevel getCancellationRestriction(IInjectionPointContext context) { - return RestrictTargetLevel.METHODS_ONLY; + return this.targetRestriction; } /** @@ -423,6 +453,18 @@ protected CompositeInjectionPoint(InjectionPoint... components) { this.components = components; } + + @Override + public RestrictTargetLevel getTargetRestriction(IInjectionPointContext context) { + RestrictTargetLevel level = RestrictTargetLevel.METHODS_ONLY; + for (InjectionPoint component : this.components) { + RestrictTargetLevel componentLevel = component.getTargetRestriction(context); + if (componentLevel.ordinal() > level.ordinal()) { + level = componentLevel; + } + } + return level; + } /* (non-Javadoc) * @see org.spongepowered.asm.mixin.injection.InjectionPoint#toString() @@ -527,6 +569,11 @@ public Shift(InjectionPoint input, int shift) { public String toString() { return "InjectionPoint(" + this.getClass().getSimpleName() + ")[" + this.input + "]"; } + + @Override + public RestrictTargetLevel getTargetRestriction(IInjectionPointContext context) { + return this.input.getTargetRestriction(context); + } @Override public boolean find(String desc, InsnList insns, Collection nodes) { @@ -665,7 +712,7 @@ public static List parse(IInjectionPointContext context, ListgetValue(at, "ordinal", Integer.valueOf(-1)); int opcode = Annotations.getValue(at, "opcode", Integer.valueOf(0)); String id = Annotations.getValue(at, "id"); - + int flags = InjectionPoint.Flags.parse(at); + if (args == null) { args = ImmutableList.of(); } - return InjectionPoint.parse(context, value, shift, by, args, target, slice, ordinal, opcode, id); + return InjectionPoint.parse(context, value, shift, by, args, target, slice, ordinal, opcode, id, flags); } /** @@ -745,12 +793,13 @@ public static InjectionPoint parse(IInjectionPointContext context, AnnotationNod * @param ordinal Ordinal offset for supported injection points * @param opcode Bytecode opcode for supported injection points * @param id Injection point id from annotation + * @param flags Additional flags * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static InjectionPoint parse(IMixinContext context, MethodNode method, AnnotationNode parent, String at, At.Shift shift, int by, - List args, String target, String slice, int ordinal, int opcode, String id) { - return InjectionPoint.parse(new AnnotatedMethodInfo(context, method, parent), at, shift, by, args, target, slice, ordinal, opcode, id); + List args, String target, String slice, int ordinal, int opcode, String id, int flags) { + return InjectionPoint.parse(new AnnotatedMethodInfo(context, method, parent), at, shift, by, args, target, slice, ordinal, opcode, id, flags); } /** @@ -768,17 +817,18 @@ public static InjectionPoint parse(IMixinContext context, MethodNode method, Ann * @param ordinal Ordinal offset for supported injection points * @param opcode Bytecode opcode for supported injection points * @param id Injection point id from annotation + * @param flags Additional flags * @return InjectionPoint parsed from the supplied data or null if parsing * failed */ public static InjectionPoint parse(IInjectionPointContext context, String at, At.Shift shift, int by, - List args, String target, String slice, int ordinal, int opcode, String id) { - InjectionPointData data = new InjectionPointData(context, at, args, target, slice, ordinal, opcode, id); + List args, String target, String slice, int ordinal, int opcode, String id, int flags) { + InjectionPointData data = new InjectionPointData(context, at, args, target, slice, ordinal, opcode, id, flags); Class ipClass = InjectionPoint.findClass(context.getMixin(), data); InjectionPoint point = InjectionPoint.create(context.getMixin(), data, ipClass); return InjectionPoint.shift(context, point, shift, by); } - + @SuppressWarnings("unchecked") private static Class findClass(IMixinContext context, InjectionPointData data) { String type = data.getType(); diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArg.java b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArg.java index 8d5fcc76b..a45a15c01 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArg.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArg.java @@ -221,5 +221,28 @@ * @return Constraints for this annotation */ public String constraints() default ""; + + /** + * By default almost all injectors for a target class apply their injections + * at the same time. In other words, if multiple mixins target the same + * class then injectors are applied in priority order (since the mixins + * themselves are merged in priority order, and injectors run in the order + * they were merged). The exception being redirect injectors, which apply in + * a later pass. + * + *

The default order for injectors is 1000, and redirect + * injectors use 10000.

+ * + *

Specifying a value for order alters this default behaviour + * and causes the injector to inject either earlier or later than it + * normally would. For example specifying 900 will cause the + * injector to apply before others, while 1100 will apply later. + * Injectors with the same order will still apply in order of their + * mixin's priority. + * + * @return the application order for this injector, uses DEFAULT (1000) if + * not specified + */ + public int order() default 1000; } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArgs.java b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArgs.java index 2430d8359..d50a625bd 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArgs.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyArgs.java @@ -198,5 +198,28 @@ * @return Constraints for this annotation */ public String constraints() default ""; + + /** + * By default almost all injectors for a target class apply their injections + * at the same time. In other words, if multiple mixins target the same + * class then injectors are applied in priority order (since the mixins + * themselves are merged in priority order, and injectors run in the order + * they were merged). The exception being redirect injectors, which apply in + * a later pass. + * + *

The default order for injectors is 1000, and redirect + * injectors use 10000.

+ * + *

Specifying a value for order alters this default behaviour + * and causes the injector to inject either earlier or later than it + * normally would. For example specifying 900 will cause the + * injector to apply before others, while 1100 will apply later. + * Injectors with the same order will still apply in order of their + * mixin's priority. + * + * @return the application order for this injector, uses DEFAULT (1000) if + * not specified + */ + public int order() default 1000; } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyConstant.java b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyConstant.java index 9870a40e7..e7a42f5fa 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyConstant.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyConstant.java @@ -178,5 +178,27 @@ * @return Constraints for this annotation */ public String constraints() default ""; + + /** + * By default almost all injectors for a target class apply their injections + * at the same time. In other words, if multiple mixins target the same + * class then injectors are applied in priority order (since the mixins + * themselves are merged in priority order, and injectors run in the order + * they were merged). Redirect injectors apply in a later pass. + * + *

The default order for redirect injectors is 10000, and all + * other injectors use 1000.

+ * + *

Specifying a value for order alters this default behaviour + * and causes the injector to inject either earlier or later than it + * normally would. For example specifying 9900 will cause the + * injector to apply before others, while 11000 will apply later. + * Injectors with the same order will still apply in order of their + * mixin's priority. + * + * @return the application order for this injector, uses default REDIRECT + * order (10000) if not specified + */ + public int order() default 10000; } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyVariable.java b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyVariable.java index a525f02a9..7bd2168e6 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/ModifyVariable.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/ModifyVariable.java @@ -271,5 +271,28 @@ * @return Constraints for this annotation */ public String constraints() default ""; + + /** + * By default almost all injectors for a target class apply their injections + * at the same time. In other words, if multiple mixins target the same + * class then injectors are applied in priority order (since the mixins + * themselves are merged in priority order, and injectors run in the order + * they were merged). The exception being redirect injectors, which apply in + * a later pass. + * + *

The default order for injectors is 1000, and redirect + * injectors use 10000.

+ * + *

Specifying a value for order alters this default behaviour + * and causes the injector to inject either earlier or later than it + * normally would. For example specifying 900 will cause the + * injector to apply before others, while 1100 will apply later. + * Injectors with the same order will still apply in order of their + * mixin's priority. + * + * @return the application order for this injector, uses DEFAULT (1000) if + * not specified + */ + public int order() default 1000; } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/Next.java b/src/main/java/org/spongepowered/asm/mixin/injection/Next.java new file mode 100644 index 000000000..c16b96af0 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/Next.java @@ -0,0 +1,86 @@ +/* + * 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.injection; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * See {@link Desc#next @Desc.next} + */ +@Target({ }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Next { + + /** + * The name of the member to match. Optional. If present is matched + * case-sensitively against targets. + */ + public String name() default ""; + + /** + * The return type of a method to match, or the declared type of a field. + * Defaults to void. + */ + public Class ret() default void.class; + + /** + * The argument types of a method to match, ignored for fields. Note that + * failing to specify this value matches a target with no arguments. + */ + public Class[] args() default { }; + + /** + * The minimum number of times this selector should match. By default the + * selector is allowed to match no targets. When selecting fields or methods + * setting this value to anything other than 0 or 1 is pointless since the + * member can either be present or absent. However when matching method + * calls or field accesses (eg. when using this value inside + * {@link At#desc @At.desc}) this allows a minimum number of matches to + * be specified in order to provide early validation that the selector + * matched the correct number of candidates. To specify an exact number of + * matches, set {@link #max max} to the same value as min. Values + * less than zero are ignored since selectors cannot match a negative number + * of times. + */ + public int min() default 0; + + /** + * The maximum number of times this selector can match. By default the + * selector is allowed to match an unlimited number of targets. When + * selecting fields or methods, setting this value to anything other than 1 + * is pointless since the member can either be present or absent. However + * when matching method calls or field accesses (eg. when + * using this value inside {@link At#desc @At.desc}) this allows a + * maximum number of matches to be specified in order to limit the number of + * times that the selector can match candidates. To specify an exact number + * of matches, set max to the same value as {@link #min min}. + * Values less than 1 are treated as Integer.MAX_VALUE (the + * default) since setting a value of 0 or less has no meaning. + */ + public int max() default Integer.MAX_VALUE; + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/Redirect.java b/src/main/java/org/spongepowered/asm/mixin/injection/Redirect.java index b72f5fdc3..c53ff6bea 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/Redirect.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/Redirect.java @@ -438,5 +438,27 @@ * @return Constraints for this annotation */ public String constraints() default ""; + + /** + * By default almost all injectors for a target class apply their injections + * at the same time. In other words, if multiple mixins target the same + * class then injectors are applied in priority order (since the mixins + * themselves are merged in priority order, and injectors run in the order + * they were merged). Redirect injectors apply in a later pass. + * + *

The default order for redirect injectors is 10000, and all + * other injectors use 1000.

+ * + *

Specifying a value for order alters this default behaviour + * and causes the injector to inject either earlier or later than it + * normally would. For example specifying 9900 will cause the + * injector to apply before others, while 11000 will apply later. + * Injectors with the same order will still apply in order of their + * mixin's priority. + * + * @return the application order for this injector, uses default REDIRECT + * order (10000) if not specified + */ + public int order() default 10000; } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java index 6542992ef..1a0089432 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java @@ -38,6 +38,8 @@ import org.spongepowered.asm.mixin.injection.InjectionPoint; import org.spongepowered.asm.mixin.injection.Surrogate; import org.spongepowered.asm.mixin.injection.code.Injector; +import org.spongepowered.asm.mixin.injection.code.InjectorTarget; +import org.spongepowered.asm.mixin.injection.struct.Constructor; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.struct.Target; @@ -184,7 +186,7 @@ private class Callback extends InsnList { List argNames = null; if (locals != null) { - int baseArgIndex = CallbackInjector.this.isStatic() ? 0 : 1; + int baseArgIndex = target.isStatic ? 0 : 1; argNames = new ArrayList(); for (int l = 0; l <= locals.length; l++) { if (l == this.frameSize) { @@ -419,7 +421,7 @@ public CallbackInjector(InjectionInfo info, boolean cancellable, LocalCapture lo @Override protected void sanityCheck(Target target, List injectionPoints) { super.sanityCheck(target, injectionPoints); - this.checkTargetModifiers(target, true); + this.checkTargetModifiers(target, false); } /* (non-Javadoc) @@ -428,27 +430,21 @@ protected void sanityCheck(Target target, List injectionPoints) * org.objectweb.asm.tree.AbstractInsnNode, java.util.Set) */ @Override - protected void addTargetNode(Target target, List myNodes, AbstractInsnNode node, Set nominators) { - InjectionNode injectionNode = target.addInjectionNode(node); + protected void addTargetNode(InjectorTarget injectorTarget, List myNodes, AbstractInsnNode node, Set nominators) { + InjectionNode injectionNode = injectorTarget.addInjectionNode(node); + if (this.cancellable && injectorTarget.getTarget() instanceof Constructor) { + throw new InvalidInjectionException(this.info, String.format("Found cancellable @Inject targetting a constructor in injector %s", this)); + } + for (InjectionPoint ip : nominators) { try { - this.checkTargetForNode(target, injectionNode, ip.getTargetRestriction(this.info)); + this.checkTargetForNode(injectorTarget.getTarget(), injectionNode, ip.getTargetRestriction(this.info)); } catch (InvalidInjectionException ex) { throw new InvalidInjectionException(this.info, String.format("%s selector %s", ip, ex.getMessage())); } - // Fabric start: prevent cancellation in constructor injections - if (this.cancellable) { - try { - this.checkTargetForNode(target, injectionNode, ip.getCancellationRestriction(this.info)); - } catch (InvalidInjectionException ex) { - throw new InvalidInjectionException(this.info, String.format("%s selector (cancellable = true) %s", ip, ex.getMessage())); - } - } - // Fabric end - String id = ip.getId(); if (Strings.isNullOrEmpty(id)) { continue; @@ -457,7 +453,7 @@ protected void addTargetNode(Target target, List myNodes, Abstrac String existingId = this.ids.get(Integer.valueOf(injectionNode.getId())); if (existingId != null && !existingId.equals(id)) { Injector.logger.warn("Conflicting id for {} insn in {}, found id {} on {}, previously defined as {}", Bytecode.getOpcodeName(node), - target.toString(), id, this.info, existingId); + injectorTarget.toString(), id, this.info, existingId); break; } @@ -574,7 +570,9 @@ private void inject(final Callback callback) { } } this.invokeCallback(callback, callbackMethod); - if (callback.usesCallbackInfo) this.injectCancellationCode(callback); + if (callback.usesCallbackInfo) { + this.injectCancellationCode(callback); + } callback.inject(); this.info.notifyInjected(callback.target); @@ -637,8 +635,8 @@ private void printLocals(final Callback callback) { printer.kv("Target Max LOCALS", callback.target.getMaxLocals()); printer.kv("Initial Frame Size", callback.frameSize); printer.kv("Callback Name", this.info.getMethodName()); - printer.kv("Instruction", "%s %s", callback.node.getClass().getSimpleName(), - Bytecode.getOpcodeName(callback.node.getCurrentTarget().getOpcode())); + printer.kv("Instruction", "%s %s", callback.node.getCurrentTarget().getClass().getSimpleName(), + Bytecode.describeNode(callback.node.getCurrentTarget())); printer.hr(); if (callback.locals.length > callback.frameSize) { printer.add(" %s %20s %s", "LOCAL", "TYPE", "NAME"); @@ -763,7 +761,7 @@ private void invokeCallback(final Callback callback, final MethodNode callbackMe // Push the target method's parameters onto the stack if (callback.captureArgs()) { - Bytecode.loadArgs(callback.target.arguments, callback, this.isStatic ? 0 : 1, -1); //, callback.typeCasts); + Bytecode.loadArgs(callback.target.arguments, callback, callback.target.isStatic ? 0 : 1, -1); //, callback.typeCasts); } // Push the callback info onto the stack diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/code/IInsnListEx.java b/src/main/java/org/spongepowered/asm/mixin/injection/code/IInsnListEx.java new file mode 100644 index 000000000..928090ff1 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/code/IInsnListEx.java @@ -0,0 +1,134 @@ +/* + * 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.injection.code; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.spongepowered.asm.mixin.injection.InjectionPoint; + +/** + * Interface for extensions for InsnList which provide additional context for + * the InsnList. This is mainly to allow passing additional information to + * InjectionPoint::{@link InjectionPoint#find} without breaking + * backward compatibility. + */ +public interface IInsnListEx { + + /** + * Type of special nodes supported by {@link #getSpecialNode} + */ + public enum SpecialNodeType { + + /** + * The delegate constructor call in a constructor + */ + DELEGATE_CTOR, + + /** + * The location for injected initialisers in a constructor + */ + INITIALISER_INJECTION_POINT, + + /** + * The location after field initialisers but before the first + * constructor body instruction, requires line numbers to be present in + * the target class + */ + CTOR_BODY + + } + + /** + * Get the name of the target method + */ + public abstract String getTargetName(); + + /** + * Get the descriptor of the target method + */ + public abstract String getTargetDesc(); + + /** + * Get the signature of the target method + */ + public abstract String getTargetSignature(); + + /** + * Get the access flags from the target method + */ + public abstract int getTargetAccess(); + + /** + * Get whether the target method is static + */ + public abstract boolean isTargetStatic(); + + /** + * Get whether the target method is a constructor + */ + public abstract boolean isTargetConstructor(); + + /** + * Get whether the target method is a static initialiser + */ + public abstract boolean isTargetStaticInitialiser(); + + /** + * Get - if available - the specified special node from the target. The + * returned node is not guaranteed to be non-null. + * + * @param type type of special node to fetch + * @return the special node or null if not available + */ + public abstract AbstractInsnNode getSpecialNode(SpecialNodeType type); + + + /** + * Get whether this list is decorated with the specified key + * + * @param key meta key + * @return true if the specified decoration exists + */ + public abstract boolean hasDecoration(String key); + + /** + * Get the specified decoration + * + * @param key meta key + * @param value type + * @return decoration value or null if absent + */ + public abstract V getDecoration(String key); + + /** + * Get the specified decoration or default value + * + * @param key meta key + * @param defaultValue default value to return + * @param value type + * @return decoration value or default value if absent + */ + public abstract V getDecoration(String key, V defaultValue); + +} 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 78ea6f37c..6e813add7 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 @@ -44,6 +44,7 @@ import org.spongepowered.asm.mixin.injection.InjectionPoint.RestrictTargetLevel; import org.spongepowered.asm.mixin.injection.InjectionPoint.Specifier; import org.spongepowered.asm.mixin.injection.invoke.RedirectInjector; +import org.spongepowered.asm.mixin.injection.struct.Constructor; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.struct.Target.Extension; @@ -204,6 +205,11 @@ public String toString() { */ protected final boolean isStatic; + /** + * True if the callback method is in an interface + */ + protected final boolean isInterface; + /** * Make a new CallbackInjector for the supplied InjectionInfo * @@ -213,12 +219,13 @@ public Injector(InjectionInfo info, String annotationType) { this.info = info; this.annotationType = annotationType; - this.classNode = info.getClassNode(); + this.classNode = info.getTargetClassNode(); this.methodNode = info.getMethod(); this.methodArgs = Type.getArgumentTypes(this.methodNode.desc); this.returnType = Type.getReturnType(this.methodNode.desc); this.isStatic = Bytecode.isStatic(this.methodNode); + this.isInterface = Bytecode.hasFlag(this.classNode, Opcodes.ACC_INTERFACE); } @Override @@ -239,13 +246,13 @@ public final List find(InjectorTarget injectorTarget, List myNodes = new ArrayList(); for (TargetNode node : this.findTargetNodes(injectorTarget, injectionPoints)) { - this.addTargetNode(injectorTarget.getTarget(), myNodes, node.insn, node.nominators); + this.addTargetNode(injectorTarget, myNodes, node.insn, node.nominators); } return myNodes; } - protected void addTargetNode(Target target, List myNodes, AbstractInsnNode node, Set nominators) { - myNodes.add(target.addInjectionNode(node)); + protected void addTargetNode(InjectorTarget injectorTarget, List myNodes, AbstractInsnNode node, Set nominators) { + myNodes.add(injectorTarget.addInjectionNode(node)); } /** @@ -293,7 +300,6 @@ public final void inject(Target target, List nodes) { */ private Collection findTargetNodes(InjectorTarget injectorTarget, List injectionPoints) { IMixinContext mixin = this.info.getMixin(); - MethodNode method = injectorTarget.getMethod(); Map targetNodes = new TreeMap(); List nodes = new ArrayList(32); @@ -308,29 +314,29 @@ private Collection findTargetNodes(InjectorTarget injectorTarget, Li injectorTarget, injectorTarget.getMergedBy(), injectorTarget.getMergedPriority())); } - if (!this.findTargetNodes(method, injectionPoint, injectorTarget, nodes)) { + if (!this.findTargetNodes(injectorTarget, injectionPoint, nodes)) { continue; } - + Specifier specifier = injectionPoint.getSpecifier(Specifier.ALL); if (specifier == Specifier.ONE && nodes.size() != 1) { throw new InvalidInjectionException(this.info, String.format("%s on %s has specifier :ONE but matched %d instructions", injectionPoint, this, nodes.size())); } else if (specifier != Specifier.ALL && nodes.size() > 1) { AbstractInsnNode specified = nodes.get(specifier == Specifier.FIRST ? 0 : nodes.size() - 1); - this.addTargetNode(method, targetNodes, injectionPoint, specified); + this.addTargetNode(injectorTarget, targetNodes, injectionPoint, specified); } else { for (AbstractInsnNode insn : nodes) { - this.addTargetNode(method, targetNodes, injectionPoint, insn); + this.addTargetNode(injectorTarget, targetNodes, injectionPoint, insn); } } } - + return targetNodes.values(); } - protected void addTargetNode(MethodNode method, Map targetNodes, InjectionPoint injectionPoint, AbstractInsnNode insn) { - Integer key = method.instructions.indexOf(insn); + protected void addTargetNode(InjectorTarget target, Map targetNodes, InjectionPoint injectionPoint, AbstractInsnNode insn) { + Integer key = target.getTarget().indexOf(insn); TargetNode targetNode = targetNodes.get(key); if (targetNode == null) { targetNode = new TargetNode(insn); @@ -339,9 +345,8 @@ protected void addTargetNode(MethodNode method, Map targetN targetNode.nominators.add(injectionPoint); } - protected boolean findTargetNodes(MethodNode into, InjectionPoint injectionPoint, InjectorTarget injectorTarget, - Collection nodes) { - return injectionPoint.find(into.desc, injectorTarget.getSlice(injectionPoint), nodes); + protected boolean findTargetNodes(InjectorTarget target, InjectionPoint injectionPoint, Collection nodes) { + return injectionPoint.find(target.getDesc(), target.getSlice(injectionPoint), nodes); } protected void sanityCheck(Target target, List injectionPoints) { @@ -379,19 +384,21 @@ protected final void checkTargetModifiers(Target target, boolean exactMatch) { * @param node Injection location */ protected void checkTargetForNode(Target target, InjectionNode node, RestrictTargetLevel targetLevel) { - if (target.isCtor) { + if (target instanceof Constructor) { + Constructor ctor = (Constructor)target; + if (targetLevel == RestrictTargetLevel.METHODS_ONLY) { throw new InvalidInjectionException(this.info, String.format("Found %s targetting a constructor in injector %s", this.annotationType, this)); } - DelegateInitialiser superCall = target.findDelegateInitNode(); + DelegateInitialiser superCall = ctor.findDelegateInitNode(); if (!superCall.isPresent) { - Injector.logger.warn("Suppressed mixin error for backwards compatibility: Delegate constructor lookup failed for {} target on {}", this.annotationType, this.info); + Injector.logger.warn("Suppressed mixin error for backwards compatibility: Delegate constructor lookup failed for %s target on %s", this.annotationType, this.info); } else { - int superCallIndex = target.indexOf(superCall.insn); - int targetIndex = target.indexOf(node.getCurrentTarget()); + int superCallIndex = ctor.indexOf(superCall.insn); + int targetIndex = ctor.indexOf(node.getCurrentTarget()); if (targetIndex <= superCallIndex) { if (targetLevel == RestrictTargetLevel.CONSTRUCTORS_AFTER_DELEGATE) { throw new InvalidInjectionException(this.info, String.format("Found %s targetting a constructor before %s() in injector %s", @@ -407,7 +414,7 @@ protected void checkTargetForNode(Target target, InjectionNode node, RestrictTar } } - this.checkTargetModifiers(target, true); + this.checkTargetModifiers(target, false); } protected void preInject(Target target, InjectionNode node) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/code/InjectorTarget.java b/src/main/java/org/spongepowered/asm/mixin/injection/code/InjectorTarget.java index 9347f3a7d..8fa8f5911 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/code/InjectorTarget.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/code/InjectorTarget.java @@ -27,13 +27,15 @@ import java.util.HashMap; import java.util.Map; +import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodNode; import org.spongepowered.asm.mixin.extensibility.IMixinConfig; import org.spongepowered.asm.mixin.injection.InjectionPoint; -import org.spongepowered.asm.mixin.injection.selectors.ITargetSelector; +import org.spongepowered.asm.mixin.injection.selectors.TargetSelectors.SelectedMethod; import org.spongepowered.asm.mixin.injection.struct.Target; +import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; import org.spongepowered.asm.util.Annotations; @@ -59,9 +61,9 @@ public class InjectorTarget { private final Target target; /** - * Selector which selected this method + * Selected method identified by the target selector */ - private final ITargetSelector selector; + private final SelectedMethod selectedMethod; /** * Name of the mixin which merged the target, if any @@ -79,10 +81,10 @@ public class InjectorTarget { * @param context owner * @param target target */ - public InjectorTarget(ISliceContext context, Target target, ITargetSelector selector) { + public InjectorTarget(ISliceContext context, Target target, SelectedMethod selectedMethod) { this.context = context; this.target = target; - this.selector = selector; + this.selectedMethod = selectedMethod; AnnotationNode merged = Annotations.getVisible(target.method, MixinMerged.class); this.mergedBy = Annotations.getValue(merged, "mixin"); @@ -94,6 +96,49 @@ public String toString() { return this.target.toString(); } + /** + * Add an injection node to this target if it does not already exist, + * returns the existing node if it exists + * + * @param node Instruction node to add + * @return wrapper for the specified node + */ + public InjectionNode addInjectionNode(AbstractInsnNode node) { + return this.target.addInjectionNode(node); + } + + /** + * Get an injection node from this collection if it already exists, returns + * null if the node is not tracked + * + * @param node instruction node + * @return wrapper node or null if not tracked + */ + public InjectionNode getInjectionNode(AbstractInsnNode node) { + return this.target.getInjectionNode(node); + } + + /** + * Get the target method name + */ + public String getName() { + return this.target.getName(); + } + + /** + * Get the target method descriptor + */ + public String getDesc() { + return this.target.getDesc(); + } + + /** + * Get the target method signature + */ + public String getSignature() { + return this.target.getSignature(); + } + /** * Get the target reference */ @@ -111,8 +156,8 @@ public MethodNode getMethod() { /** * Get the selector which selected this target */ - public ITargetSelector getSelector() { - return this.selector; + public SelectedMethod getSelectedMethod() { + return this.selectedMethod; } /** @@ -149,10 +194,10 @@ public InsnList getSlice(String id) { if (slice == null) { MethodSlice sliceInfo = this.context.getSlice(id); if (sliceInfo != null) { - slice = sliceInfo.getSlice(this.target.method); + slice = sliceInfo.getSlice(this.target); } else { // No slice exists so just wrap the method insns - slice = new InsnListReadOnly(this.target.method.instructions); + slice = new InsnListEx(this.target); } this.cache.put(id, slice); } @@ -180,5 +225,6 @@ public void dispose() { this.cache.clear(); } + } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/code/InsnListEx.java b/src/main/java/org/spongepowered/asm/mixin/injection/code/InsnListEx.java new file mode 100644 index 000000000..77055eeba --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/code/InsnListEx.java @@ -0,0 +1,250 @@ +/* + * 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.injection.code; + +import java.util.HashMap; +import java.util.Map; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.spongepowered.asm.mixin.injection.struct.Constructor; +import org.spongepowered.asm.mixin.injection.struct.IChainedDecoration; +import org.spongepowered.asm.mixin.injection.struct.Target; +import org.spongepowered.asm.mixin.transformer.struct.Initialiser; +import org.spongepowered.asm.mixin.transformer.struct.Initialiser.InjectionMode; +import org.spongepowered.asm.util.Bytecode.DelegateInitialiser; +import org.spongepowered.asm.util.Constants; + +/** + * InsnList with extensions, see {@link IInsnListEx} + */ +public class InsnListEx extends InsnListReadOnly implements IInsnListEx { + + /** + * The target method + */ + protected final Target target; + + /** + * Decorations on this insn list + */ + private Map decorations; + + public InsnListEx(Target target) { + super(target.insns); + this.target = target; + } + + @Override + public String toString() { + return this.target.toString(); + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #getTargetName() + */ + @Override + public String getTargetName() { + return this.target.getName(); + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #getTargetDesc() + */ + @Override + public String getTargetDesc() { + return this.target.getDesc(); + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #getTargetSignature() + */ + @Override + public String getTargetSignature() { + return this.target.getSignature(); + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #getTargetAccess() + */ + @Override + public int getTargetAccess() { + return this.target.method.access; + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #isTargetStatic() + */ + @Override + public boolean isTargetStatic() { + return this.target.isStatic; + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #isTargetConstructor() + */ + @Override + public boolean isTargetConstructor() { + return this.target instanceof Constructor; + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #isTargetStaticInitialiser() + */ + @Override + public boolean isTargetStaticInitialiser() { + return Constants.CLINIT.equals(this.target.getName()); + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.code.IInsnListEx + * #getSpecialNode + */ + @Override + public AbstractInsnNode getSpecialNode(SpecialNodeType type) { + switch (type) { + case DELEGATE_CTOR: + if (this.target instanceof Constructor) { + DelegateInitialiser superCall = ((Constructor)this.target).findDelegateInitNode(); + if (superCall.isPresent && this.contains(superCall.insn)) { + return superCall.insn; + } + } + return null; + + case INITIALISER_INJECTION_POINT: + if (this.target instanceof Constructor) { + // mode is always DEFAULT because we want to locate initialisers if possible + InjectionMode mode = Initialiser.InjectionMode.DEFAULT; + AbstractInsnNode initialiserInjectionPoint = ((Constructor)this.target).findInitialiserInjectionPoint(mode); + if (this.contains(initialiserInjectionPoint)) { + return initialiserInjectionPoint; + } + } + return null; + + case CTOR_BODY: + if (this.target instanceof Constructor) { + AbstractInsnNode beforeBody = ((Constructor)this.target).findFirstBodyInsn(); + if (this.contains(beforeBody)) { + return beforeBody; + } + } + return null; + default: + return null; + } + } + + /** + * Decorate this insn list with arbitrary metadata for use by + * context-specific injection points + * + * @param key meta key + * @param value meta value + * @param value type + * @return fluent + */ + @SuppressWarnings("unchecked") + public InsnListEx decorate(String key, V value) { + if (this.decorations == null) { + this.decorations = new HashMap(); + } + if (value instanceof IChainedDecoration && this.decorations.containsKey(key)) { + Object previous = this.decorations.get(key); + if (previous.getClass().equals(value.getClass())) { + ((IChainedDecoration)value).replace(previous); + } + } + this.decorations.put(key, value); + return this; + } + + /** + * Strip a specific decoration from this list if the decoration exists + * + * @param key meta key + * @return fluent + */ + public InsnListEx undecorate(String key) { + if (this.decorations != null) { + this.decorations.remove(key); + } + return this; + } + + /** + * Strip all decorations from this list + * + * @return fluent + */ + public InsnListEx undecorate() { + this.decorations = null; + return this; + } + + /** + * Get whether this list is decorated with the specified key + * + * @param key meta key + * @return true if the specified decoration exists + */ + public boolean hasDecoration(String key) { + return this.decorations != null && this.decorations.get(key) != null; + } + + /** + * Get the specified decoration + * + * @param key meta key + * @param value type + * @return decoration value or null if absent + */ + @SuppressWarnings("unchecked") + public V getDecoration(String key) { + return (V) (this.decorations == null ? null : this.decorations.get(key)); + } + + /** + * Get the specified decoration or default value + * + * @param key meta key + * @param defaultValue default value to return + * @param value type + * @return decoration value or null if absent + */ + @SuppressWarnings("unchecked") + public V getDecoration(String key, V defaultValue) { + V existing = (V) (this.decorations == null ? null : this.decorations.get(key)); + return existing != null ? existing : defaultValue; + } + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/code/InsnListReadOnly.java b/src/main/java/org/spongepowered/asm/mixin/injection/code/InsnListReadOnly.java index f1e647ef7..2ea71ea0e 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/code/InsnListReadOnly.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/code/InsnListReadOnly.java @@ -34,7 +34,7 @@ * instances so that custom InjectionPoint implementations cannot modify the * insn list whilst inspecting it. */ -public class InsnListReadOnly extends InsnList { +public abstract class InsnListReadOnly extends InsnList { private InsnList insnList; @@ -218,6 +218,9 @@ public AbstractInsnNode get(int index) { */ @Override public boolean contains(AbstractInsnNode insn) { + if (insn == null) { + return false; + } return this.insnList.contains(insn); } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/code/MethodSlice.java b/src/main/java/org/spongepowered/asm/mixin/injection/code/MethodSlice.java index 58201c05b..aa2d3d4c8 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/code/MethodSlice.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/code/MethodSlice.java @@ -40,6 +40,7 @@ import org.spongepowered.asm.mixin.injection.InjectionPoint.Specifier; import org.spongepowered.asm.mixin.injection.Slice; import org.spongepowered.asm.mixin.injection.struct.InjectionPointAnnotationContext; +import org.spongepowered.asm.mixin.injection.struct.Target; import org.spongepowered.asm.mixin.injection.throwables.InjectionError; import org.spongepowered.asm.mixin.injection.throwables.InvalidSliceException; import org.spongepowered.asm.service.MixinService; @@ -57,7 +58,7 @@ public final class MethodSlice { * identified by start and end to be accessed. In essence * this class provides a view of the underlying InsnList. */ - static final class InsnListSlice extends InsnListReadOnly { + static final class InsnListSlice extends InsnListEx { /** * ListIterator for the slice view, wraps an iterator returned by the @@ -178,8 +179,8 @@ public void add(AbstractInsnNode e) { */ private final int start, end; - protected InsnListSlice(InsnList inner, int start, int end) { - super(inner); + protected InsnListSlice(Target target, int start, int end) { + super(target); // Start and end are validated prior to construction this.start = start; @@ -259,6 +260,9 @@ public AbstractInsnNode get(int index) { */ @Override public boolean contains(AbstractInsnNode insn) { + if (insn == null) { + return false; + } for (AbstractInsnNode node : this.toArray()) { if (node == insn) { return true; @@ -361,13 +365,13 @@ public String getId() { /** * Get a sliced insn list based on the parameters specified in this slice * - * @param method method to slice + * @param target method to slice * @return read only slice */ - public InsnListReadOnly getSlice(MethodNode method) { - int max = method.instructions.size() - 1; - int start = this.find(method, this.from, 0, 0, "from"); - int end = this.find(method, this.to, max, start, "to"); + public InsnListReadOnly getSlice(Target target) { + int max = target.insns.size() - 1; + int start = this.find(target, this.from, 0, 0, "from"); + int end = this.find(target, this.to, max, start, "to"); if (start > end) { throw new InvalidSliceException(this.owner, String.format("%s is negative size. Range(%d -> %d)", this.describe(), start, end)); @@ -378,10 +382,10 @@ public InsnListReadOnly getSlice(MethodNode method) { } if (start == 0 && end == max) { - return new InsnListReadOnly(method.instructions); + return new InsnListEx(target); } - return new InsnListSlice(method.instructions, start, end); + return new InsnListSlice(target, start, end); } /** @@ -389,7 +393,7 @@ public InsnListReadOnly getSlice(MethodNode method) { * the index of the instruction matching the query. Returns the default * value if the query returns zero results. * - * @param method Method to query + * @param target Method to query * @param injectionPoint Query to run * @param defaultValue Value to return if injection point is null (open * ended) @@ -397,15 +401,15 @@ public InsnListReadOnly getSlice(MethodNode method) { * @param argument The name of the argument ("from" or "to") for debug msgs * @return matching insn index */ - private int find(MethodNode method, InjectionPoint injectionPoint, int defaultValue, int failValue, String argument) { + private int find(Target target, InjectionPoint injectionPoint, int defaultValue, int failValue, String argument) { if (injectionPoint == null) { return defaultValue; } String description = String.format("%s(%s)", this.name, argument); Deque nodes = new LinkedList(); - InsnListReadOnly insns = new InsnListReadOnly(method.instructions); - boolean result = injectionPoint.find(method.desc, insns, nodes); + InsnList insns = new InsnListEx(target); + boolean result = injectionPoint.find(target.getDesc(), insns, nodes); Specifier specifier = injectionPoint.getSpecifier(Specifier.FIRST); if (specifier == Specifier.ALL) { throw new InvalidSliceException(this.owner, String.format("ALL is not a valid specifier for slice %s", this.describe(description))); @@ -424,7 +428,7 @@ private int find(MethodNode method, InjectionPoint injectionPoint, int defaultVa this.successCountTo++; } - return method.instructions.indexOf(specifier == Specifier.FIRST ? nodes.getFirst() : nodes.getLast()); + return target.indexOf(specifier == Specifier.FIRST ? nodes.getFirst() : nodes.getLast()); } /** diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgInjector.java index dd1639e1f..c718f1608 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgInjector.java @@ -33,7 +33,7 @@ import org.spongepowered.asm.mixin.injection.InjectionPoint; import org.spongepowered.asm.mixin.injection.InjectionPoint.RestrictTargetLevel; import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.invoke.util.InvokeUtil; +import org.spongepowered.asm.mixin.injection.struct.ArgOffsets; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.struct.Target; @@ -110,28 +110,49 @@ protected void inject(Target target, InjectionNode node) { @Override protected void injectAtInvoke(Target target, InjectionNode node) { MethodInsnNode methodNode = (MethodInsnNode)node.getCurrentTarget(); - Type[] originalArgs = InvokeUtil.getOriginalArgs(node); - Type[] currentArgs = InvokeUtil.getCurrentArgs(node); - int argIndex = this.findArgIndex(target, originalArgs); + Type[] args = Type.getArgumentTypes(methodNode.desc); + ArgOffsets offsets = node.getDecoration(ArgOffsets.KEY, ArgOffsets.DEFAULT); + boolean nested = node.hasDecoration(ArgOffsets.KEY); + Type[] originalArgs = offsets.apply(args); + + if (originalArgs.length == 0) { + throw new InvalidInjectionException(this.info, "@ModifyArg injector " + this + " targets a method invocation " + + ((MethodInsnNode)node.getOriginalTarget()).name + "()" + Type.getReturnType(methodNode.desc) + " with no arguments!"); + } + + int argIndex = offsets.getArgIndex(this.findArgIndex(target, originalArgs)); + int baseIndex = offsets.getStartIndex(); + InsnList insns = new InsnList(); Extension extraLocals = target.extendLocals(); if (this.singleArgMode) { - this.injectSingleArgHandler(target, extraLocals, currentArgs, argIndex, insns); + this.injectSingleArgHandler(target, extraLocals, args, argIndex, insns, nested); } else { - this.injectMultiArgHandler(target, extraLocals, originalArgs, currentArgs, argIndex, insns); + if (!Arrays.equals(originalArgs, this.methodArgs)) { + throw new InvalidInjectionException(this.info, "@ModifyArg injector " + this + " targets a method with an invalid signature " + + Bytecode.getDescriptor(originalArgs) + ", expected " + Bytecode.getDescriptor(this.methodArgs)); + } + + this.injectMultiArgHandler(target, extraLocals, args, baseIndex, argIndex, insns, nested); } target.insns.insertBefore(methodNode, insns); - target.extendStack().set(2 - (extraLocals.get() - 1)).apply(); + Extension extraStack = target.extendStack(); + if (!isStatic) { + extraStack.add(); + } + extraStack.add(methodArgs); + extraStack.apply(); extraLocals.apply(); } /** * Inject handler opcodes for a single arg handler */ - private void injectSingleArgHandler(Target target, Extension extraLocals, Type[] args, int argIndex, InsnList insns) { - int[] argMap = this.storeArgs(target, args, insns, argIndex); + private void injectSingleArgHandler(Target target, Extension extraLocals, Type[] args, int argIndex, InsnList insns, boolean nested) { + int[] argMap = target.generateArgMap(args, argIndex, nested); + this.storeArgs(target, args, insns, argMap, argIndex, args.length, null, null); this.invokeHandlerWithArgs(args, insns, argMap, argIndex, argIndex + 1); this.pushArgs(args, insns, argMap, argIndex + 1, args.length); extraLocals.add((argMap[argMap.length - 1] - target.getMaxLocals()) + args[args.length - 1].getSize()); @@ -140,17 +161,15 @@ private void injectSingleArgHandler(Target target, Extension extraLocals, Type[] /** * Inject handler opcodes for a multi arg handler */ - private void injectMultiArgHandler(Target target, Extension extraLocals, Type[] originalArgs, Type[] currentArgs, int argIndex, InsnList insns) { - if (!Arrays.equals(originalArgs, this.methodArgs)) { - throw new InvalidInjectionException(this.info, "@ModifyArg method " + this + " targets a method with an invalid signature " - + Bytecode.getDescriptor(originalArgs) + ", expected " + Bytecode.getDescriptor(this.methodArgs)); - } - - int[] argMap = this.storeArgs(target, currentArgs, insns, 0); - this.pushArgs(currentArgs, insns, argMap, 0, argIndex); - this.invokeHandlerWithArgs(originalArgs, insns, argMap, 0, originalArgs.length); - this.pushArgs(currentArgs, insns, argMap, argIndex + 1, currentArgs.length); - extraLocals.add((argMap[argMap.length - 1] - target.getMaxLocals()) + currentArgs[currentArgs.length - 1].getSize()); + private void injectMultiArgHandler(Target target, Extension extraLocals, Type[] args, int baseIndex, int argIndex, InsnList insns, + boolean nested) { + int[] argMap = target.generateArgMap(args, baseIndex, nested); + int[] handlerArgMap = baseIndex == 0 ? argMap : Arrays.copyOfRange(argMap, baseIndex, baseIndex + this.methodArgs.length); + this.storeArgs(target, args, insns, argMap, baseIndex, args.length, null, null); + this.pushArgs(args, insns, argMap, baseIndex, argIndex); + this.invokeHandlerWithArgs(this.methodArgs, insns, handlerArgMap, 0, this.methodArgs.length); + this.pushArgs(args, insns, argMap, argIndex + 1, args.length); + extraLocals.add((argMap[argMap.length - 1] - target.getMaxLocals()) + args[args.length - 1].getSize()); } protected int findArgIndex(Target target, Type[] args) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgsInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgsInjector.java index f67aad0f1..53157f1ea 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgsInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/ModifyArgsInjector.java @@ -33,7 +33,7 @@ import org.spongepowered.asm.mixin.injection.InjectionPoint.RestrictTargetLevel; import org.spongepowered.asm.mixin.injection.ModifyArgs; import org.spongepowered.asm.mixin.injection.invoke.arg.ArgsClassGenerator; -import org.spongepowered.asm.mixin.injection.invoke.util.InvokeUtil; +import org.spongepowered.asm.mixin.injection.struct.ArgOffsets; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.struct.Target; @@ -41,8 +41,6 @@ import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; import org.spongepowered.asm.util.Bytecode; -import java.util.Arrays; - /** * A bytecode injector which allows a single argument of a chosen method call to * be altered. For details see javadoc for {@link ModifyArgs @ModifyArgs}. @@ -80,26 +78,28 @@ protected void inject(Target target, InjectionNode node) { */ @Override protected void injectAtInvoke(Target target, InjectionNode node) { - MethodInsnNode targetMethod = (MethodInsnNode)node.getCurrentTarget(); - - Type[] originalArgs = InvokeUtil.getOriginalArgs(node); - Type[] currentArgs = InvokeUtil.getCurrentArgs(node); + MethodInsnNode methodNode = (MethodInsnNode)node.getCurrentTarget(); + Type[] args = Type.getArgumentTypes(methodNode.desc); + ArgOffsets offsets = node.getDecoration(ArgOffsets.KEY, ArgOffsets.DEFAULT); + Type[] originalArgs = offsets.apply(args); + int endIndex = offsets.getArgIndex(originalArgs.length); + + String targetMethodDesc = Type.getMethodDescriptor(Type.getReturnType(methodNode.desc), originalArgs); + if (originalArgs.length == 0) { throw new InvalidInjectionException(this.info, "@ModifyArgs injector " + this + " targets a method invocation " - + targetMethod.name + targetMethod.desc + " with no arguments!"); + + ((MethodInsnNode)node.getOriginalTarget()).name + targetMethodDesc + " with no arguments!"); } - - String originalDesc = Type.getMethodDescriptor(Type.VOID_TYPE, originalArgs); - String clArgs = this.argsClassGenerator.getArgsClass(originalDesc, this.info.getMixin().getMixin()).getName(); + + String clArgs = this.argsClassGenerator.getArgsClass(targetMethodDesc, this.info.getMixin().getMixin()).getName(); boolean withArgs = this.verifyTarget(target); InsnList insns = new InsnList(); Extension extraStack = target.extendStack().add(1); - - Type[] extraArgs = Arrays.copyOfRange(currentArgs, originalArgs.length, currentArgs.length); - int[] extraArgMap = this.storeArgs(target, extraArgs, insns, 0); - this.packArgs(insns, clArgs, originalDesc); - + + int[] afterWindowArgMap = this.storeArgs(target, args, insns, endIndex); + this.packArgs(insns, clArgs, targetMethodDesc); + if (withArgs) { extraStack.add(target.arguments); Bytecode.loadArgs(target.arguments, insns, target.isStatic ? 0 : 1); @@ -107,16 +107,16 @@ protected void injectAtInvoke(Target target, InjectionNode node) { this.invokeHandler(insns); this.unpackArgs(insns, clArgs, originalArgs); - this.pushArgs(extraArgs, insns, extraArgMap, 0, extraArgs.length); - + this.pushArgs(args, insns, afterWindowArgMap, endIndex, args.length); + extraStack.apply(); - target.insns.insertBefore(targetMethod, insns); + target.insns.insertBefore(methodNode, insns); } private boolean verifyTarget(Target target) { String shortDesc = String.format("(L%s;)V", ArgsClassGenerator.ARGS_REF); if (!this.methodNode.desc.equals(shortDesc)) { - String targetDesc = Bytecode.changeDescriptorReturnType(target.method.desc, "V"); + String targetDesc = Bytecode.changeDescriptorReturnType(target.getDesc(), "V"); String longDesc = String.format("(L%s;%s", ArgsClassGenerator.ARGS_REF, targetDesc.substring(1)); if (this.methodNode.desc.equals(longDesc)) { @@ -129,8 +129,8 @@ private boolean verifyTarget(Target target) { return false; } - private void packArgs(InsnList insns, String clArgs, String targetDesc) { - String factoryDesc = Bytecode.changeDescriptorReturnType(targetDesc, "L" + clArgs + ";"); + private void packArgs(InsnList insns, String clArgs, String targetMethodDesc) { + String factoryDesc = Bytecode.changeDescriptorReturnType(targetMethodDesc, "L" + clArgs + ";"); insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, clArgs, "of", factoryDesc, false)); insns.add(new InsnNode(Opcodes.DUP)); diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/RedirectInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/RedirectInjector.java index ee57296ba..dbf703dda 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/RedirectInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/RedirectInjector.java @@ -37,8 +37,10 @@ import org.spongepowered.asm.mixin.injection.InjectionPoint.RestrictTargetLevel; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.code.Injector; +import org.spongepowered.asm.mixin.injection.code.InjectorTarget; import org.spongepowered.asm.mixin.injection.points.BeforeFieldAccess; import org.spongepowered.asm.mixin.injection.points.BeforeNew; +import org.spongepowered.asm.mixin.injection.struct.ArgOffsets; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.struct.Target; @@ -103,7 +105,7 @@ public class RedirectInjector extends InvokeInjector { /** * Meta decoration object for redirector target nodes */ - public class Meta { + class Meta { public static final String KEY = "redirector"; @@ -135,6 +137,8 @@ static class ConstructorRedirectData { public static final String KEY = "ctor"; + String desc = null; + boolean wildcard = false; int injected = 0; @@ -156,6 +160,7 @@ public void throwOrCollect(InvalidInjectionException ex) { static class RedirectedInvokeData extends InjectorData { final MethodInsnNode node; + final boolean isStatic; final Type returnType; final Type[] targetArgs; final Type[] handlerArgs; @@ -163,9 +168,10 @@ static class RedirectedInvokeData extends InjectorData { RedirectedInvokeData(Target target, MethodInsnNode node) { super(target); this.node = node; + this.isStatic = node.getOpcode() == Opcodes.INVOKESTATIC; this.returnType = Type.getReturnType(node.desc); this.targetArgs = Type.getArgumentTypes(node.desc); - this.handlerArgs = node.getOpcode() == Opcodes.INVOKESTATIC + this.handlerArgs = this.isStatic ? this.targetArgs : ObjectArrays.concat(Type.getObjectType(node.owner), this.targetArgs); } @@ -252,8 +258,8 @@ protected void checkTarget(Target target) { } @Override - protected void addTargetNode(Target target, List myNodes, AbstractInsnNode insn, Set nominators) { - InjectionNode node = target.getInjectionNode(insn); + protected void addTargetNode(InjectorTarget injectorTarget, List myNodes, AbstractInsnNode insn, Set nominators) { + InjectionNode node = injectorTarget.getInjectionNode(insn); ConstructorRedirectData ctorData = null; int fuzz = BeforeFieldAccess.ARRAY_SEARCH_FUZZ_DEFAULT; int opcode = 0; @@ -264,7 +270,7 @@ protected void addTargetNode(Target target, List myNodes, Abstrac } if (node != null ) { - Meta other = node.getDecoration(Meta.KEY); + Meta other = node.getDecoration(Meta.KEY); if (other != null && other.getOwner() != this) { if (other.priority >= this.meta.priority) { @@ -280,8 +286,10 @@ protected void addTargetNode(Target target, List myNodes, Abstrac for (InjectionPoint ip : nominators) { if (ip instanceof BeforeNew) { - ctorData = this.getCtorRedirect((BeforeNew)ip); - ctorData.wildcard = !((BeforeNew)ip).hasDescriptor(); + BeforeNew beforeNew = (BeforeNew)ip; + ctorData = this.getCtorRedirect(beforeNew); + ctorData.wildcard = !beforeNew.hasDescriptor(); + ctorData.desc = beforeNew.getDescriptor(); } else if (ip instanceof BeforeFieldAccess) { BeforeFieldAccess bfa = (BeforeFieldAccess)ip; fuzz = bfa.getFuzzFactor(); @@ -289,7 +297,7 @@ protected void addTargetNode(Target target, List myNodes, Abstrac } } - InjectionNode targetNode = target.addInjectionNode(insn); + InjectionNode targetNode = injectorTarget.addInjectionNode(insn); targetNode.decorate(Meta.KEY, this.meta); targetNode.decorate(RedirectInjector.KEY_NOMINATORS, nominators); if (insn instanceof TypeInsnNode && insn.getOpcode() == Opcodes.NEW) { @@ -353,7 +361,7 @@ protected void inject(Target target, InjectionNode node) { } protected boolean preInject(InjectionNode node) { - Meta other = node.getDecoration(Meta.KEY); + Meta other = node.getDecoration(Meta.KEY); if (other.getOwner() != this) { Injector.logger.warn("{} conflict. Skipping {} with priority {}, already redirected by {} with priority {}", this.annotationType, this.info, this.meta.priority, other.name, other.priority); @@ -387,6 +395,7 @@ protected void injectAtInvoke(Target target, InjectionNode node) { Extension extraLocals = target.extendLocals().add(invoke.handlerArgs).add(1); Extension extraStack = target.extendStack().add(1); // Normally only need 1 extra stack pos to store target ref int[] argMap = this.storeArgs(target, invoke.handlerArgs, insns, 0); + ArgOffsets offsets = new ArgOffsets(invoke.isStatic ? 0 : 1, invoke.targetArgs.length); if (invoke.captureTargetArgs > 0) { int argSize = Bytecode.getArgsSize(target.arguments, 0, invoke.captureTargetArgs); extraLocals.add(argSize); @@ -399,6 +408,7 @@ protected void injectAtInvoke(Target target, InjectionNode node) { insns.add(new TypeInsnNode(Opcodes.CHECKCAST, invoke.returnType.getInternalName())); } target.replaceNode(invoke.node, champion, insns); + node.decorate(ArgOffsets.KEY, offsets); extraLocals.apply(); extraStack.apply(); } @@ -613,7 +623,7 @@ protected void injectAtConstructor(Target target, InjectionNode node) { final TypeInsnNode newNode = (TypeInsnNode)node.getCurrentTarget(); final AbstractInsnNode dupNode = target.get(target.indexOf(newNode) + 1); - final MethodInsnNode initNode = target.findInitNodeFor(newNode); + final MethodInsnNode initNode = target.findInitNodeFor(newNode, meta.desc); if (initNode == null) { meta.throwOrCollect(new InvalidInjectionException(this.info, String.format("%s ctor invocation was not found in %s", diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/arg/ArgsClassGenerator.java b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/arg/ArgsClassGenerator.java index 05d47943a..8772344c1 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/arg/ArgsClassGenerator.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/invoke/arg/ArgsClassGenerator.java @@ -61,8 +61,10 @@ public final class ArgsClassGenerator implements IClassGenerator { public static final String ARGS_REF = ArgsClassGenerator.ARGS_NAME.replace('.', '/'); public static final String GETTER_PREFIX = "$"; + + public static final String SYNTHETIC_PACKAGE = Constants.SYNTHETIC_PACKAGE + ".args"; - private static final String CLASS_NAME_BASE = Constants.SYNTHETIC_PACKAGE + ".args.Args$"; + private static final String CLASS_NAME_BASE = ArgsClassGenerator.SYNTHETIC_PACKAGE + ".Args$"; private static final String OBJECT = "java/lang/Object"; private static final String OBJECT_ARRAY = "[L" + ArgsClassGenerator.OBJECT + ";"; 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 42376f7ee..676665b31 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 @@ -102,7 +102,7 @@ int getOrdinal() { } /** - * Injection info + * Injection info */ final InjectionInfo info; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/modify/ModifyVariableInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/modify/ModifyVariableInjector.java index 8253ed152..421e79401 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/modify/ModifyVariableInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/modify/ModifyVariableInjector.java @@ -31,13 +31,14 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; import org.spongepowered.asm.mixin.injection.InjectionPoint; import org.spongepowered.asm.mixin.injection.InjectionPoint.RestrictTargetLevel; import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.code.IInsnListEx; import org.spongepowered.asm.mixin.injection.code.Injector; import org.spongepowered.asm.mixin.injection.code.InjectorTarget; +import org.spongepowered.asm.mixin.injection.code.InsnListEx; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.struct.InjectionPointData; @@ -57,6 +58,9 @@ */ public class ModifyVariableInjector extends Injector { + private static final String KEY_INFO = "mv.info"; + private static final String KEY_TARGET = "mv.target"; + /** * Target context information */ @@ -87,13 +91,22 @@ abstract static class LocalVariableInjectionPoint extends InjectionPoint { @Override public boolean find(String desc, InsnList insns, Collection nodes) { + if (insns instanceof IInsnListEx) { + IInsnListEx xinsns = (IInsnListEx)insns; + Target target = xinsns.getDecoration(ModifyVariableInjector.KEY_TARGET); + if (target != null) { + // Info is not actually used internally so we don't especially care if it's null + return this.find(xinsns.getDecoration(ModifyVariableInjector.KEY_INFO), insns, nodes, target); + } + } + throw new InvalidInjectionException(this.mixin, this.getAtCode() + " injection point must be used in conjunction with @ModifyVariable"); } abstract boolean find(InjectionInfo info, InsnList insns, Collection nodes, Target target); } - + /** * True to consider only method args */ @@ -109,13 +122,21 @@ public ModifyVariableInjector(InjectionInfo info, LocalVariableDiscriminator dis } @Override - protected boolean findTargetNodes(MethodNode into, InjectionPoint injectionPoint, InjectorTarget injectorTarget, - Collection nodes) { - if (injectionPoint instanceof LocalVariableInjectionPoint) { - return ((LocalVariableInjectionPoint)injectionPoint).find(this.info, injectorTarget.getSlice(injectionPoint), nodes, - injectorTarget.getTarget()); + protected boolean findTargetNodes(InjectorTarget target, InjectionPoint injectionPoint, Collection nodes) { + InsnListEx slice = (InsnListEx)target.getSlice(injectionPoint); + slice.decorate(ModifyVariableInjector.KEY_TARGET, target.getTarget()); + slice.decorate(ModifyVariableInjector.KEY_INFO, this.info); + + boolean found = injectionPoint instanceof LocalVariableInjectionPoint + ? ((LocalVariableInjectionPoint)injectionPoint).find(this.info, slice, nodes, target.getTarget()) + : injectionPoint.find(target.getDesc(), slice, nodes); + + if (slice instanceof InsnListEx) { + slice.undecorate(ModifyVariableInjector.KEY_TARGET); + slice.undecorate(ModifyVariableInjector.KEY_INFO); } - return injectionPoint.find(into.desc, injectorTarget.getSlice(injectionPoint), nodes); + + return found; } /* (non-Javadoc) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/points/AfterInvoke.java b/src/main/java/org/spongepowered/asm/mixin/injection/points/AfterInvoke.java index 2bf054850..8ec25593e 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/points/AfterInvoke.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/points/AfterInvoke.java @@ -24,6 +24,7 @@ */ package org.spongepowered.asm.mixin.injection.points; +import java.util.Arrays; import java.util.Collection; import org.objectweb.asm.Opcodes; @@ -55,6 +56,18 @@ * the method is invoked 3 times and you want to match the 3rd then you can * specify an ordinal of 2 (ordinals are zero-indexed). The * default value is -1 which supresses ordinal matching + *
named argument: fuzz
+ *
By default, the injection point inspects only the instruction + * immediately following the matched invocation and will match store + * instructions. Specifying a higher fuzz increases the search range, + * skipping instructions as necessary to find a matching store opcode.
+ *
named argument: skip
+ *
When fuzz is specified, a default list of skippable opcodes is + * used. The list of skippable opcodes can be overridden by specifying a list + * of opcodes (numeric values or constant names from {@link Opcodes} can be + * used). This can be used to restrict the fuzz behaviour to consume + * only expected opcodes (eg. CHECKCAST). Note that store opcodes + * cannot be skipped and specifying them has no effect.
* * *

Example:

@@ -69,9 +82,63 @@ */ @AtCode("INVOKE_ASSIGN") public class AfterInvoke extends BeforeInvoke { - + + /** + * Default opcodes which are eligible to be skipped (see named argument + * skip above) if named argument fuzz is + * increased beyond the default value of 1. + * + *

Skipped opcodes: DUP, IADD, LADD, + * FADD, DADD, ISUB, LSUB, + * FSUB, DSUB, IMUL, LMUL, + * FMUL, DMUL, IDIV, LDIV, + * FDIV, DDIV, IREM, LREM, + * FREM, DREM, INEG, LNEG, + * FNEG, DNEG, ISHL, LSHL, + * ISHR, LSHR, IUSHR, LUSHR, + * IAND, LAND, IOR, LOR, IXOR, + * LXOR, IINC, I2L, I2F, I2D, + * L2I, L2F, L2D, F2I, F2L, + * F2D, D2I, D2L, D2F, I2B, + * I2C, I2S, CHECKCAST, INSTANCEOF

+ */ + public static final int[] DEFAULT_SKIP = new int[] { + // Opcodes which may appear if the targetted method is part of an + // expression eg. int foo = 2 + this.bar(); + Opcodes.DUP, Opcodes.IADD, Opcodes.LADD, Opcodes.FADD, Opcodes.DADD, + Opcodes.ISUB, Opcodes.LSUB, Opcodes.FSUB, Opcodes.DSUB, Opcodes.IMUL, + Opcodes.LMUL, Opcodes.FMUL, Opcodes.DMUL, Opcodes.IDIV, Opcodes.LDIV, + Opcodes.FDIV, Opcodes.DDIV, Opcodes.IREM, Opcodes.LREM, Opcodes.FREM, + Opcodes.DREM, Opcodes.INEG, Opcodes.LNEG, Opcodes.FNEG, Opcodes.DNEG, + Opcodes.ISHL, Opcodes.LSHL, Opcodes.ISHR, Opcodes.LSHR, Opcodes.IUSHR, + Opcodes.LUSHR, Opcodes.IAND, Opcodes.LAND, Opcodes.IOR, Opcodes.LOR, + Opcodes.IXOR, Opcodes.LXOR, Opcodes.IINC, + + // Opcodes which may appear if the targetted method is cast before + // assignment eg. int foo = (int)this.getFloat(); + Opcodes.I2L, Opcodes.I2F, Opcodes.I2D, Opcodes.L2I, Opcodes.L2F, + Opcodes.L2D, Opcodes.F2I, Opcodes.F2L, Opcodes.F2D, Opcodes.D2I, + Opcodes.D2L, Opcodes.D2F, Opcodes.I2B, Opcodes.I2C, Opcodes.I2S, + Opcodes.CHECKCAST, Opcodes.INSTANCEOF + }; + + /** + * Lookahead fuzz factor for finding the corresponding assignment, by + * default only the opcode immediately following the invocation is inspected + */ + private int fuzz = 1; + + /** + * If fuzz is increased beyond the default, the author can specify an allow- + * list of opcodes which can be skipped, by default the lookahead is + * unconstrained. + */ + private int[] skip = null; + public AfterInvoke(InjectionPointData data) { super(data); + this.fuzz = Math.max(data.get("fuzz", this.fuzz), 1); + this.skip = data.getOpcodeList("skip", AfterInvoke.DEFAULT_SKIP); } @Override @@ -80,12 +147,22 @@ protected boolean addInsn(InsnList insns, Collection nodes, Ab if (Type.getReturnType(methodNode.desc) == Type.VOID_TYPE) { return false; } - - insn = InjectionPoint.nextNode(insns, insn); - if (insn instanceof VarInsnNode && insn.getOpcode() >= Opcodes.ISTORE) { - insn = InjectionPoint.nextNode(insns, insn); + + if (this.fuzz > 0) { + int offset = insns.indexOf(insn); + int maxOffset = Math.min(insns.size(), offset + this.fuzz + 1); + for (int index = offset + 1; index < maxOffset; index++) { + AbstractInsnNode candidate = insns.get(index); + if (candidate instanceof VarInsnNode && insn.getOpcode() >= Opcodes.ISTORE) { + insn = candidate; + break; + } else if (this.skip != null && this.skip.length > 0 && Arrays.binarySearch(this.skip, candidate.getOpcode()) < 0) { + break; + } + } } + insn = InjectionPoint.nextNode(insns, insn); nodes.add(insn); return true; } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeConstant.java b/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeConstant.java index bbad13edc..25ee0c513 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeConstant.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeConstant.java @@ -169,7 +169,7 @@ public BeforeConstant(IMixinContext context, AnnotationNode node, String returnT public BeforeConstant(InjectionPointData data) { super(data); - String strNullValue = data.get("nullValue", null); + String strNullValue = data.get("nullValue", (String)null); Boolean empty = strNullValue != null ? Boolean.parseBoolean(strNullValue) : null; this.ordinal = data.getOrdinal(); @@ -178,8 +178,8 @@ public BeforeConstant(InjectionPointData data) { this.floatValue = Floats.tryParse(data.get("floatValue", "")); this.longValue = Longs.tryParse(data.get("longValue", "")); this.doubleValue = Doubles.tryParse(data.get("doubleValue", "")); - this.stringValue = data.get("stringValue", null); - String strClassValue = data.get("classValue", null); + this.stringValue = data.get("stringValue", (String)null); + String strClassValue = data.get("classValue", (String)null); this.typeValue = strClassValue != null ? Type.getObjectType(strClassValue.replace('.', '/')) : null; this.matchByType = this.validateDiscriminator(data.getMixin(), "V", empty, "in @At(\"CONSTANT\") args"); diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeNew.java b/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeNew.java index 722493252..14850b2d3 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeNew.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeNew.java @@ -118,7 +118,7 @@ public BeforeNew(InjectionPointData data) { } ITargetSelectorConstructor targetSelector = (ITargetSelectorConstructor)member; this.target = targetSelector.toCtorType(); - this.desc = targetSelector.toCtorDesc(); + this.desc = org.spongepowered.asm.mixin.FabricUtil.getCompatibility(data.getContext()) >= org.spongepowered.asm.mixin.FabricUtil.COMPATIBILITY_0_14_0 ? targetSelector.toCtorDesc() : null; } /** @@ -127,6 +127,13 @@ public BeforeNew(InjectionPointData data) { public boolean hasDescriptor() { return this.desc != null; } + + /** + * Gets the descriptor from the injection point, can return null + */ + public String getDescriptor() { + return this.desc; + } @SuppressWarnings("unchecked") @Override @@ -152,7 +159,7 @@ public boolean find(String desc, InsnList insns, Collection no if (this.desc != null) { for (TypeInsnNode newNode : newNodes) { - if (this.findCtor(insns, newNode)) { + if (BeforeNew.findInitNodeFor(insns, newNode, this.desc) != null) { nodes.add(newNode); found = true; } @@ -162,18 +169,21 @@ public boolean find(String desc, InsnList insns, Collection no return found; } - protected boolean findCtor(InsnList insns, TypeInsnNode newNode) { + public static MethodInsnNode findInitNodeFor(InsnList insns, TypeInsnNode newNode, String desc) { int indexOf = insns.indexOf(newNode); + int depth = 0; for (Iterator iter = insns.iterator(indexOf); iter.hasNext();) { AbstractInsnNode insn = iter.next(); if (insn instanceof MethodInsnNode && insn.getOpcode() == Opcodes.INVOKESPECIAL) { MethodInsnNode methodNode = (MethodInsnNode)insn; - if (Constants.CTOR.equals(methodNode.name) && methodNode.owner.equals(newNode.desc) && methodNode.desc.equals(this.desc)) { - return true; + if (Constants.CTOR.equals(methodNode.name) && --depth == 0) { + return methodNode.owner.equals(newNode.desc) && (desc == null || methodNode.desc.equals(desc)) ? methodNode : null; } + } else if (insn instanceof TypeInsnNode && insn.getOpcode() == Opcodes.NEW) { + depth++; } } - return false; + return null; } private boolean matchesOwner(TypeInsnNode insn) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeStringInvoke.java b/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeStringInvoke.java index 3f6373d85..82c53ff50 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeStringInvoke.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/points/BeforeStringInvoke.java @@ -99,7 +99,7 @@ public class BeforeStringInvoke extends BeforeInvoke { public BeforeStringInvoke(InjectionPointData data) { super(data); - this.ldcValue = data.get("ldc", null); + this.ldcValue = data.get("ldc", (String)null); if (this.ldcValue == null) { throw new IllegalArgumentException(this.getClass().getSimpleName() + " requires named argument \"ldc\" to specify the desired target"); diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/points/ConstructorHead.java b/src/main/java/org/spongepowered/asm/mixin/injection/points/ConstructorHead.java new file mode 100644 index 000000000..804f7369d --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/points/ConstructorHead.java @@ -0,0 +1,177 @@ +/* + * 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.injection.points; + +import java.util.Collection; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodNode; +import org.spongepowered.asm.logging.ILogger; +import org.spongepowered.asm.mixin.MixinEnvironment.Option; +import org.spongepowered.asm.mixin.injection.InjectionPoint.AtCode; +import org.spongepowered.asm.mixin.injection.code.IInsnListEx; +import org.spongepowered.asm.mixin.injection.code.IInsnListEx.SpecialNodeType; +import org.spongepowered.asm.mixin.injection.struct.InjectionPointData; +import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionPointException; +import org.spongepowered.asm.service.MixinService; + +/** + *

Like {@link MethodHead HEAD}, this injection point can be used to specify + * the first instruction in a method, but provides special handling for + * constructors. For regular method, the behaviour is identical to HEAD + * .

+ * + *

By default, this injection point attempts to select the first instruction + * after any initialisers (including initialisers merged by mixins) but will + * fall back to selecting the first instruction after the delegate call (eg. a + * call to super() or this()) in the case where heuristic + * detection of the initialisers fails. This behaviour can be overridden by + * providing the enforce parameter to enforce selection of a specific + * location.

+ * + *
+ *
enforce=POST_DELEGATE
+ *
Select the instruction immediately after the delegate constructor
+ *
enforce=POST_MIXIN
+ *
Select the instruction immediately after all mixin-initialised field + * initialisers, this is similar to POST_DELEGATE if no applied mixins have + * initialisers for target class fields, except that the injection point + * will be after any mixin-supplied initialisers.
+ *
enforce=PRE_BODY
+ *
Selects the first instruction in the target method body, as determined + * by the line numbers. If the target method does not have line numbers + * available, the result is equivalent to POST_DELEGATE.
+ *
+ * + *

Example default behaviour:

+ *
+ *   @At(value = "CTOR_HEAD", unsafe = true)
+ *
+ * + *

Example behaviour enforcing post-delegate injection point:

+ *
+ *   @At(value = "CTOR_HEAD", unsafe = true, args="enforce=POST_DELEGATE")
+ *   
+ *
+ */ +@AtCode("CTOR_HEAD") +public class ConstructorHead extends MethodHead { + + /** + * Location enforcement + */ + static enum Enforce { + + /** + * Use default behaviour (POST_INIT) + */ + DEFAULT, + + /** + * Enforce selection of post-delegate insn + */ + POST_DELEGATE, + + /** + * Enforce selection of post-initialiser insn + */ + POST_INIT, + + /** + * Enforce selection of the first body insn + */ + PRE_BODY; + + } + + /** + * Logger + */ + protected final ILogger logger = MixinService.getService().getLogger("mixin"); + + /** + * Enforce behaviour parsed from At args + */ + private final Enforce enforce; + + /** + * True to warn when enfored selection fails + */ + private final boolean verbose; + + private final MethodNode method; + + public ConstructorHead(InjectionPointData data) { + super(data); + if (!data.isUnsafe()) { + throw new InvalidInjectionPointException(data.getMixin(), "@At(\"CTOR_HEAD\") requires unsafe=true"); + } + this.enforce = data.get("enforce", Enforce.DEFAULT); + this.verbose = data.getMixin().getOption(Option.DEBUG_VERBOSE); + this.method = data.getMethod(); + } + + @Override + public boolean find(String desc, InsnList insns, Collection nodes) { + if (!(insns instanceof IInsnListEx)) { + return false; + } + + IInsnListEx xinsns = (IInsnListEx)insns; + if (!xinsns.isTargetConstructor()) { + return super.find(desc, insns, nodes); + } + + AbstractInsnNode delegateCtor = xinsns.getSpecialNode(SpecialNodeType.DELEGATE_CTOR); + AbstractInsnNode postDelegate = delegateCtor != null ? delegateCtor.getNext() : null; + if (this.enforce == Enforce.POST_DELEGATE) { + if (postDelegate == null) { + if (this.verbose) { + this.logger.warn("@At(\"{}\") on {}{} targetting {} failed for enforce=POST_DELEGATE because no delegate was found", + this.getAtCode(), this.method.name, this.method.desc, xinsns); + } + return false; + } + nodes.add(postDelegate); + return true; + } + + SpecialNodeType type = this.enforce == Enforce.PRE_BODY ? SpecialNodeType.CTOR_BODY : SpecialNodeType.INITIALISER_INJECTION_POINT; + AbstractInsnNode postInit = xinsns.getSpecialNode(type); + + if (postInit != null) { + nodes.add(postInit); + return true; + } + + if (postDelegate != null) { + nodes.add(postDelegate); + return true; + } + + return super.find(desc, insns, nodes); + } +} diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/points/MethodHead.java b/src/main/java/org/spongepowered/asm/mixin/injection/points/MethodHead.java index 93b3642d6..0d4bf2171 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/points/MethodHead.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/points/MethodHead.java @@ -41,7 +41,10 @@ *

Example:

*
  *   @At("HEAD")
- *
+ * + * + *

Note that for constructors, use of HEAD is discouraged, see + * instead {@link ConstructorHead CTOR_HEAD}. */ @AtCode("HEAD") public class MethodHead extends InjectionPoint { @@ -60,4 +63,5 @@ public boolean find(String desc, InsnList insns, Collection no nodes.add(insns.getFirst()); return true; } + } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/ISelectorContext.java b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/ISelectorContext.java index 763bfee43..78583e406 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/ISelectorContext.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/ISelectorContext.java @@ -83,5 +83,11 @@ public interface ISelectorContext { * not return null! */ public abstract String remap(String reference); + + /** + * Get a human-readable description of the annotation on the method for use + * in error messages + */ + public abstract String getElementDescription(); } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/TargetSelectors.java b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/TargetSelectors.java new file mode 100644 index 000000000..b46ee143e --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/TargetSelectors.java @@ -0,0 +1,375 @@ +/* + * 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.injection.selectors; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.MixinEnvironment.Option; +import org.spongepowered.asm.mixin.injection.selectors.ITargetSelector.Configure; +import org.spongepowered.asm.mixin.injection.selectors.TargetSelector.Result; +import org.spongepowered.asm.mixin.injection.selectors.throwables.SelectorConstraintException; +import org.spongepowered.asm.mixin.injection.struct.InvalidMemberDescriptorException; +import org.spongepowered.asm.mixin.injection.struct.TargetNotSupportedException; +import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; +import org.spongepowered.asm.mixin.refmap.IMixinContext; +import org.spongepowered.asm.mixin.struct.AnnotatedMethodInfo; +import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; +import org.spongepowered.asm.util.Annotations; +import org.spongepowered.asm.util.Bytecode; + +public class TargetSelectors implements Iterable { + + /** + * Selected target method, paired with the selector which identified it + */ + public static class SelectedMethod { + + /** + * The parent target of this target. If this target is a lambda then + * this will be the selector for the enclosing method. Parent is null + * for the outermost method. + */ + private final SelectedMethod parent; + + /** + * The target selector which selected this target + */ + private final ITargetSelector selector; + + /** + * The selected target method + */ + private final MethodNode method; + + SelectedMethod(SelectedMethod parent, ITargetSelector selector, MethodNode method) { + this.parent = parent; + this.selector = selector; + this.method = method; + } + + SelectedMethod(ITargetSelector selector, MethodNode method) { + this(null, selector, method); + } + + @Override + public String toString() { + return this.method.name + this.method.desc; + } + + public SelectedMethod getParent() { + return this.parent; + } + + public ITargetSelector next() { + return this.selector.next(); + } + + public MethodNode getMethod() { + return this.method; + } + + } + + /** + * The selector context for these selectors, for example the injector which + * is running the selectors + */ + private final ISelectorContext context; + + /** + * The target class node within which targets can be resolved + */ + private final ClassNode targetClassNode; + + /** + * The mixin + */ + private final IMixinContext mixin; + + /** + * Annotated method, as MethodNode at runtime, or IAnnotatedElement during + * compile + */ + private final Object method; + + /** + * Whether the annotated method is static + */ + private final boolean isStatic; + + /** + * Root selectors + */ + private final Set selectors = new LinkedHashSet(); + + /** + * Selected targets + */ + private final List targets = new ArrayList(); + + private boolean doPermissivePass; + + public TargetSelectors(ISelectorContext context, ClassNode classNode) { + this.context = context; + this.targetClassNode = classNode; + this.mixin = context.getMixin(); + this.method = context.getMethod(); + this.isStatic = this.method instanceof MethodNode && Bytecode.isStatic((MethodNode)this.method); + } + + public void parse(Set selectors) { + // Validate and attach the parsed selectors + for (ITargetSelector selector : selectors) { + try { + this.addSelector(selector.validate().attach(this.context)); + } catch (InvalidMemberDescriptorException ex) { + throw new InvalidInjectionException(this.context, String.format("%s, has invalid target descriptor: %s. %s", + this.context.getElementDescription(), ex.getMessage(), this.mixin.getReferenceMapper().getStatus())); + } catch (TargetNotSupportedException ex) { + throw new InvalidInjectionException(this.context, String.format("%s specifies a target class '%s', which is not supported", + this.context.getElementDescription(), ex.getMessage())); + } catch (InvalidSelectorException ex) { + throw new InvalidInjectionException(this.context, String.format("%s is decorated with an invalid selector: %s", + this.context.getElementDescription(), ex.getMessage())); + } + } + } + + public TargetSelectors addSelector(ITargetSelector selector) { + this.selectors.add(selector); + return this; + } + + public int size() { + return this.targets.size(); + } + + public void clear() { + this.targets.clear(); + } + + @Override + public Iterator iterator() { + return this.targets.iterator(); + } + + public void remove(SelectedMethod target) { + this.targets.remove(target); + } + + public boolean isPermissivePassEnabled() { + return this.doPermissivePass; + } + + public TargetSelectors setPermissivePass(boolean enabled) { + this.doPermissivePass = enabled; + return this; + } + + /** + * Find methods in the target class which match the parsed selectors + */ + public void find() { + this.findRootTargets(); + // this.findNestedTargets(); + } + + /** + * Evaluate the root selectors parsed from this injector, find the root + * targets and store them in the {@link #targets} collection. + */ + private void findRootTargets() { + int passes = this.doPermissivePass ? 2 : 1; + + for (ITargetSelector selector : this.selectors) { + selector = selector.configure(Configure.SELECT_MEMBER); + + int matchCount = 0; + int maxCount = selector.getMaxMatchCount(); + + // Second pass ignores descriptor + ITargetSelector permissiveSelector = selector.configure(Configure.PERMISSIVE); + int selectorPasses = (permissiveSelector == selector) ? 1 : passes; + + scan: for (int pass = 0; pass < selectorPasses && matchCount < 1; pass++) { + ITargetSelector passSelector = pass == 0 ? selector : permissiveSelector; + for (MethodNode target : this.targetClassNode.methods) { + if (passSelector.match(ElementNode.of(this.targetClassNode, target)).isExactMatch()) { + matchCount++; + + boolean isMixinMethod = Annotations.getVisible(target, MixinMerged.class) != null; + if (maxCount <= 1 || ((this.isStatic || !Bytecode.isStatic(target)) && target != this.method && !isMixinMethod)) { + this.checkTarget(target); + this.targets.add(new SelectedMethod(passSelector, target)); + } + + if (matchCount >= maxCount) { + break scan; + } + } + } + } + + if (matchCount < selector.getMinMatchCount()) { + throw new InvalidInjectionException(this.context, new SelectorConstraintException(selector, String.format( + "Injection validation failed: %s for %s did not match the required number of targets (required=%d, matched=%d). %s%s", + selector, this.context.getElementDescription(), selector.getMinMatchCount(), matchCount, + this.mixin.getReferenceMapper().getStatus(), AnnotatedMethodInfo.getDynamicInfo(this.method)))); + } + } + } + + /** + * For each root target, resolve the nested targets from the target + * descriptor + */ + protected void findNestedTargets() { + boolean recursed = false; + do { + recursed = false; + for (ListIterator iter = this.targets.listIterator(); iter.hasNext();) { + SelectedMethod target = iter.next(); + ITargetSelector next = target.next(); + if (next == null) { + continue; + } + + recursed = true; + Result result = TargetSelector.run(next, ElementNode.dynamicInsnList(target.getMethod().instructions)); + iter.remove(); + for (ElementNode candidate : result.candidates) { + if (candidate.getInsn().getOpcode() != Opcodes.INVOKEDYNAMIC) { + continue; + } + + if (!candidate.getOwner().equals(this.mixin.getTargetClassRef())) { + throw new InvalidInjectionException(this.context, String.format( + "%s, failed to select into child. Cannot select foreign method: %s. %s", + this.context.getElementDescription(), candidate, this.mixin.getReferenceMapper().getStatus())); + } + + MethodNode method = this.findMethod(candidate); + if (method == null) { + throw new InvalidInjectionException(this.context, String.format( + "%s, failed to select into child. %s%s was not found in the target class.", + this.context.getElementDescription(), candidate.getName(), candidate.getDesc())); + } + + iter.add(new SelectedMethod(target, next, method)); + } + } + } + while (recursed); + } + + private void checkTarget(MethodNode target) { + AnnotationNode merged = Annotations.getVisible(target, MixinMerged.class); + if (merged == null) { + return; + } + + if (Annotations.getVisible(target, Final.class) != null) { + throw new InvalidInjectionException(this.context, String.format("%s cannot inject into @Final method %s::%s%s merged by %s", this, + this.mixin.getTargetClassName(), target.name, target.desc, Annotations.getValue(merged, "mixin"))); + } + } + + /** + * Finds a method in the target class + * + * @return Target method matching searchFor, or null if not found + */ + private MethodNode findMethod(ElementNode searchFor) { + for (MethodNode target : this.targetClassNode.methods) { + if (target.name.equals(searchFor.getSyntheticName()) && target.desc.equals(searchFor.getDesc())) { + return target; + } + } + return null; + } + + /** + * Post-search validation that some targets were found, we can fail-fast if + * no targets were actually identified or if the specified limits are + * exceeded. + * + * @param expectedCallbackCount Number of callbacks specified by expect + * @param requiredCallbackCount Number of callbacks specified by require + */ + public void validate(int expectedCallbackCount, int requiredCallbackCount) { + int targetCount = this.targets.size(); + if (targetCount > 0) { + return; + } + + if ((this.mixin.getOption(Option.DEBUG_INJECTORS) && expectedCallbackCount > 0)) { + throw new InvalidInjectionException(this.context, + String.format("Injection validation failed: %s could not find any targets matching %s in %s. %s%s", + this.context.getElementDescription(), TargetSelectors.namesOf(this.selectors), this.mixin.getTargetClassRef(), + this.mixin.getReferenceMapper().getStatus(), AnnotatedMethodInfo.getDynamicInfo(this.method))); + } else if (requiredCallbackCount > 0) { + throw new InvalidInjectionException(this.context, + String.format("Critical injection failure: %s could not find any targets matching %s in %s. %s%s", + this.context.getElementDescription(), TargetSelectors.namesOf(this.selectors), this.mixin.getTargetClassRef(), + this.mixin.getReferenceMapper().getStatus(), AnnotatedMethodInfo.getDynamicInfo(this.method))); + } + } + + /** + * Print the names of the specified members as a human-readable list + * + * @param selectors members to print + * @return human-readable list of member names + */ + private static String namesOf(Collection selectors) { + int index = 0, count = selectors.size(); + StringBuilder sb = new StringBuilder(); + for (ITargetSelector selector : selectors) { + if (index > 0) { + if (index == (count - 1)) { + sb.append(" or "); + } else { + sb.append(", "); + } + } + sb.append('\'').append(selector.toString()).append('\''); + index++; + } + return sb.toString(); + } + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DescriptorResolver.java b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DescriptorResolver.java index 1a6f6f60a..49e4ea4e6 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DescriptorResolver.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/DescriptorResolver.java @@ -73,11 +73,21 @@ static final class Descriptor implements IResolvedDescriptor { * Selector context */ private final ISelectorContext context; - + + /** + * True if this is a debug descriptor + */ + private final boolean debug; + Descriptor(Set searched, IAnnotationHandle desc, ISelectorContext context) { + this(searched, desc, context, false); + } + + Descriptor(Set searched, IAnnotationHandle desc, ISelectorContext context, boolean debug) { this.searched = searched; this.desc = desc; this.context = context; + this.debug = debug; } /** @@ -88,6 +98,15 @@ public boolean isResolved() { return this.desc != null; } + /** + * True if this is a debugging descriptor and shouldn't be treated as + * fully resolved + */ + @Override + public boolean isDebug() { + return this.debug; + } + /** * Get information about the resolution of this descriptor, for * inclusion in error messages when isResolved is false @@ -276,11 +295,13 @@ public static IResolvedDescriptor resolve(IAnnotationHandle desc, ISelectorConte * @return Resolution result */ public static IResolvedDescriptor resolve(String id, ISelectorContext context) { + boolean debug = false; IResolverObserver observer = new ResolverObserverBasic(); if (!Strings.isNullOrEmpty(id)) { if (DescriptorResolver.PRINT_ID.equals(id)) { observer = new ResolverObserverDebug(context); id = ""; + debug = true; } else { observer.visit(id, "", ""); } @@ -288,7 +309,7 @@ public static IResolvedDescriptor resolve(String id, ISelectorContext context) { IAnnotationHandle desc = DescriptorResolver.resolve(id, context, observer, context.getSelectorCoordinate(true)); observer.postResolve(); - return new Descriptor(observer.getSearched(), desc, context); + return new Descriptor(observer.getSearched(), desc, context, debug); } /** 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 a13158505..f3df61903 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 @@ -177,7 +177,7 @@ final class Next extends DynamicSelectorDesc { private final int index; Next(int index, IResolvedDescriptor next) { - super(null, null, next.getOwner(), next.getName(), next.getArgs(), next.getReturnType(), next.getMatches(), null); + super(null, null, next.getOwner(), next.getName(), next.getArgs(), next.getReturnType(), next.getMatches(), null, next.isDebug()); this.index = index; } @@ -239,25 +239,30 @@ public ITargetSelector next() { */ private final List next; + /** + * True if matching is disabled for this selector + */ + private final boolean disabled; + private DynamicSelectorDesc(IResolvedDescriptor desc) { this(null, desc.getId(), desc.getOwner(), desc.getName(), desc.getArgs(), desc.getReturnType(), desc.getMatches(), - desc.getNext()); + desc.getNext(), desc.isDebug()); } private DynamicSelectorDesc(DynamicSelectorDesc desc, Quantifier quantifier) { - this(desc.parseException, desc.id, desc.owner, desc.name, desc.args, desc.returnType, quantifier, desc.next); + this(desc.parseException, desc.id, desc.owner, desc.name, desc.args, desc.returnType, quantifier, desc.next, desc.disabled); } private DynamicSelectorDesc(DynamicSelectorDesc desc, Type owner) { - this(desc.parseException, desc.id, owner, desc.name, desc.args, desc.returnType, desc.matches, desc.next); + this(desc.parseException, desc.id, owner, desc.name, desc.args, desc.returnType, desc.matches, desc.next, desc.disabled); } private DynamicSelectorDesc(InvalidSelectorException ex) { - this(ex, null, null, null, null, null, Quantifier.NONE, null); + this(ex, null, null, null, null, null, Quantifier.NONE, null, true); } protected DynamicSelectorDesc(InvalidSelectorException ex, String id, Type owner, String name, Type[] args, Type returnType, Quantifier matches, - List then) { + List next, boolean disabled) { this.parseException = ex; this.id = id; @@ -267,7 +272,8 @@ protected DynamicSelectorDesc(InvalidSelectorException ex, String id, Type owner this.returnType = returnType; this.methodDesc = returnType != null ? Bytecode.getDescriptor(returnType, args) : null; this.matches = matches; - this.next = then; + this.next = next; + this.disabled = disabled; } /** @@ -281,7 +287,7 @@ protected DynamicSelectorDesc(InvalidSelectorException ex, String id, Type owner */ public static DynamicSelectorDesc parse(String input, ISelectorContext context) { IResolvedDescriptor descriptor = DescriptorResolver.resolve(input, context); - if (!descriptor.isResolved()) { + if (!descriptor.isResolved() && !descriptor.isDebug()) { String extra = input.length() == 0 ? ". " + descriptor.getResolutionInfo() : ""; return new DynamicSelectorDesc(new InvalidSelectorException("Could not resolve @Desc(" + input + ") for " + context + extra)); } @@ -297,7 +303,7 @@ public static DynamicSelectorDesc parse(String input, ISelectorContext context) */ public static DynamicSelectorDesc parse(IAnnotationHandle desc, ISelectorContext context) { IResolvedDescriptor descriptor = DescriptorResolver.resolve(desc, context); - if (!descriptor.isResolved()) { + if (!descriptor.isResolved() && !descriptor.isDebug()) { return new DynamicSelectorDesc(new InvalidSelectorException("Invalid descriptor")); } return DynamicSelectorDesc.of(descriptor); @@ -494,7 +500,7 @@ public MatchResult matches(String owner, String name, String desc) { */ @Override public MatchResult match(ElementNode node) { - if (node == null) { + if (node == null || this.disabled) { return MatchResult.NONE; } else if (node.isField()) { return this.matches(node.getOwner(), node.getName(), node.getDesc(), this.returnType.getInternalName()); diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/IResolvedDescriptor.java b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/IResolvedDescriptor.java index 39a0860fe..d4dbc0bdf 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/IResolvedDescriptor.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/selectors/dynamic/IResolvedDescriptor.java @@ -40,6 +40,12 @@ public interface IResolvedDescriptor { * Get whether the descriptor was successfully resolved */ public abstract boolean isResolved(); + + /** + * Get whether the descriptor is for debugging and shouldn't be used as a + * live descriptor + */ + public abstract boolean isDebug(); /** * Get the resolved descriptor, or null if the descriptor was not resolved diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ArgOffsets.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ArgOffsets.java new file mode 100644 index 000000000..2483cb02f --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ArgOffsets.java @@ -0,0 +1,195 @@ +/* + * 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.injection.struct; + +import org.objectweb.asm.Type; + +/** + * Decoration which stores a linear offset of arguments when a node replacement + * results in a call to a method with the same arguments as the original + * (replaced) call but offset by some fixed amount. Since ModifyArg and + * ModifyArgs always assume the method args are on the top of the stack (which + * they must be), this results in locating the original method args as as a + * contiguous "window" of arguments somewhere in the middle of the args as they + * exist at application time. + * + *

Injectors which mutate the arguments of an invocation should apply this + * decoration to indicate the starting offset and size of the window which + * contains the original args.

+ */ +public class ArgOffsets implements IChainedDecoration { + + /** + * No-op arg offsets to be used when we just want unaltered arg offsets + */ + private static class Default extends ArgOffsets { + + public Default() { + super(0, 255); + } + + @Override + public int getArgIndex(int index) { + return index; + } + + @Override + public Type[] apply(Type[] args) { + return args; + } + + } + + /** + * Null offsets + */ + public static ArgOffsets DEFAULT = new ArgOffsets.Default(); + + /** + * Decoration key for this decoration type + */ + public static final String KEY = "argOffsets"; + + /** + * The offset for the start of the (original) args within the new args + */ + private final int offset; + + /** + * The total number of (original) args + */ + private final int length; + + /** + * If this offset collection replaces a previous mapping, chain to the next + * mapping in order to apply these offsets atop the old ones + */ + private ArgOffsets next; + + /** + * Create contiguous offsets starting from start and continuing for length + * + * @param offset start index + * @param length length + */ + public ArgOffsets(int offset, int length) { + this.offset = offset; + this.length = length; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("ArgOffsets[start=%d(%d),length=%d]", this.offset, this.getStartIndex(), this.length); + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.struct.IChainedDecoration + * #replace( + * org.spongepowered.asm.mixin.injection.struct.IChainedDecoration) + */ + @Override + public void replace(ArgOffsets old) { + this.next = old; + } + + /** + * Get the size of the offset window + */ + public int getLength() { + return this.length; + } + + /** + * Get whether this argument offset window is empty + */ + public boolean isEmpty() { + return this.length == 0; + } + + /** + * Compute the argument index for the start of the window (offet 0) + * + * @return the offset index for the start of the window (inclusive) + */ + public int getStartIndex() { + return this.getArgIndex(0); + } + + /** + * Compute the argument index for the end of the window (offset length) + * + * @return the offset index for the end of the window (inclusive) + */ + public int getEndIndex() { + return this.isEmpty() ? this.getStartIndex() : this.getArgIndex(this.length - 1); + } + + /** + * Compute the argument index for the specified new index + * + * @param index The new index to compute + * @return The original index based on this mapping + */ + public int getArgIndex(int index) { + return this.getArgIndex(index, false); + } + + /** + * Compute the argument index for the specified new index + * + * @param index The new index to compute + * @param mustBeInWindow Throw an exception if the requested index exceeds + * the length of the defined window + * @return The original index based on this mapping + */ + public int getArgIndex(int index, boolean mustBeInWindow) { + if (mustBeInWindow && index > this.length) { + throw new IndexOutOfBoundsException("The specified arg index " + index + " is greater than the window size " + this.length); + } + int offsetIndex = index + this.offset; + return this.next != null ? this.next.getArgIndex(offsetIndex) : offsetIndex; + } + + /** + * Apply this offset collection to the supplied argument array + * + * @param args New arguments + * @return Unmapped arguments + */ + public Type[] apply(Type[] args) { + Type[] transformed = new Type[this.length]; + for (int i = 0; i < this.length; i++) { + int offset = this.getArgIndex(i); + if (offset < args.length) { + transformed[i] = args[offset]; + } + } + return transformed; + } + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackInjectionInfo.java index c1875ba4e..67be0e66d 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackInjectionInfo.java @@ -31,6 +31,7 @@ import org.spongepowered.asm.mixin.injection.callback.LocalCapture; import org.spongepowered.asm.mixin.injection.code.Injector; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.AnnotationType; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.InjectorOrder; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; import org.spongepowered.asm.util.Annotations; @@ -40,6 +41,7 @@ * Information about a callback to inject, usually specified by {@link Inject} */ @AnnotationType(Inject.class) +@InjectorOrder(InjectorOrder.DEFAULT) public class CallbackInjectionInfo extends InjectionInfo { protected CallbackInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/Constructor.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/Constructor.java new file mode 100644 index 000000000..943edadf2 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/Constructor.java @@ -0,0 +1,235 @@ +/* + * 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.injection.struct; + +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.spongepowered.asm.mixin.transformer.ClassInfo; +import org.spongepowered.asm.mixin.transformer.struct.Initialiser; +import org.spongepowered.asm.mixin.transformer.struct.InsnRange; +import org.spongepowered.asm.util.Bytecode; +import org.spongepowered.asm.util.Constants; +import org.spongepowered.asm.util.asm.MarkerNode; +import org.spongepowered.asm.util.Bytecode.DelegateInitialiser; + +/** + * A {@link Target} which is a constructor + */ +public class Constructor extends Target { + + /** + * Cached delegate initialiser call + */ + private DelegateInitialiser delegateInitialiser; + + private MarkerNode initialiserInjectionPoint; + private MarkerNode bodyStart; + + private final String targetName; + private final String targetSuperName; + + private Set mixinInitialisedFields = new HashSet(); + + public Constructor(ClassInfo classInfo, ClassNode classNode, MethodNode method) { + super(classInfo, classNode, method); + + this.targetName = this.classInfo.getName(); + this.targetSuperName = this.classInfo.getSuperName(); + } + + /** + * Find the call to super() or this() in a constructor. + * This attempts to locate the first call to <init> which + * isn't an inline call to another object ctor being passed into the super + * invocation. + * + * @return Call to super(), this() or + * DelegateInitialiser.NONE if not found + */ + public DelegateInitialiser findDelegateInitNode() { + if (this.delegateInitialiser == null) { + this.delegateInitialiser = Bytecode.findDelegateInit(this.method, this.classInfo.getSuperName(), this.classNode.name); + } + + return this.delegateInitialiser; + } + + /** + * Get whether this constructor should have mixin initialisers injected into + * it based on whether a delegate initialiser call is absent or is a call to + * super() + */ + public boolean isInjectable() { + DelegateInitialiser delegateInit = this.findDelegateInitNode(); + return !delegateInit.isPresent || delegateInit.isSuper; + } + + /** + * Inspect an incoming initialiser to determine which fields are touched by + * the mixin. This is then used in {@link #findInitialiserInjectionPoint} to + * determine the instruction after which the mixin initialisers will be + * placed + * + * @param initialiser the initialiser to inspect + */ + public void inspect(Initialiser initialiser) { + if (this.initialiserInjectionPoint != null) { + throw new IllegalStateException("Attempted to inspect an incoming initialiser after the injection point was already determined"); + } + + for (AbstractInsnNode initialiserInsn : initialiser.getInsns()) { + if (initialiserInsn.getOpcode() == Opcodes.PUTFIELD) { + FieldInsnNode fieldInsn = (FieldInsnNode)initialiserInsn; + if (!fieldInsn.owner.equals(this.targetName)) { + continue; + } + this.mixinInitialisedFields.add(Constructor.fieldKey((FieldInsnNode)initialiserInsn)); + } + } + } + + /** + * Find the injection point for injected initialiser insns in the target + * ctor. This starts by assuming that initialiser instructions should be + * placed immediately after the delegate initialiser call, but then searches + * for field assignments and selects the last unique field + * assignment in the ctor body which represents a reasonable heuristic for + * the end of the existing initialisers. + * + * @param mode Injection mode for this specific environment + * @return target node + */ + public AbstractInsnNode findInitialiserInjectionPoint(Initialiser.InjectionMode mode) { + if (this.initialiserInjectionPoint != null) { + return this.initialiserInjectionPoint; + } + + AbstractInsnNode lastInsn = null; + for (Iterator iter = this.insns.iterator(); iter.hasNext();) { + AbstractInsnNode insn = iter.next(); + if (insn.getOpcode() == Opcodes.INVOKESPECIAL && Constants.CTOR.equals(((MethodInsnNode)insn).name)) { + String owner = ((MethodInsnNode)insn).owner; + if (owner.equals(this.targetName) || owner.equals(targetSuperName)) { + lastInsn = insn; + if (mode == Initialiser.InjectionMode.SAFE) { + break; + } + } + } else if (insn.getOpcode() == Opcodes.PUTFIELD && mode == Initialiser.InjectionMode.DEFAULT) { + String key = Constructor.fieldKey((FieldInsnNode)insn); + if (this.mixinInitialisedFields.contains(key)) { + lastInsn = insn; + } + } + } + + if (lastInsn == null) { + return null; + } + + this.initialiserInjectionPoint = new MarkerNode(MarkerNode.INITIALISER_TAIL); + this.insert(lastInsn, this.initialiserInjectionPoint); + return this.initialiserInjectionPoint; + } + + /** + * Find the injection point + */ + public AbstractInsnNode findFirstBodyInsn() { + if (this.bodyStart == null) { + this.bodyStart = new MarkerNode(MarkerNode.BODY_START); + InsnRange range = Constructor.getRange(this.method); + if (range.isValid()) { + Deque body = range.apply(this.insns, true); + this.insertBefore(body.pop(), this.bodyStart); + } else if (range.marker > -1) { + this.insert(this.insns.get(range.marker), this.bodyStart); + } else { + this.bodyStart = null; + } + } + return this.bodyStart; + } + + private static String fieldKey(FieldInsnNode fieldNode) { + return String.format("%s:%s", fieldNode.desc, fieldNode.name); + } + + /** + * Identifies line numbers in the supplied ctor which correspond to the + * start and end of the method body. + * + * @param ctor constructor to scan + * @return range indicating the line numbers of the specified constructor + * and the position of the superclass ctor invocation + */ + public static InsnRange getRange(MethodNode ctor) { + boolean lineNumberIsValid = false; + AbstractInsnNode endReturn = null; + + int line = 0, start = 0, end = 0, delegateIndex = -1; + for (Iterator iter = ctor.instructions.iterator(); iter.hasNext();) { + AbstractInsnNode insn = iter.next(); + if (insn instanceof LineNumberNode) { + line = ((LineNumberNode)insn).line; + lineNumberIsValid = true; + } else if (insn instanceof MethodInsnNode) { + if (insn.getOpcode() == Opcodes.INVOKESPECIAL && Constants.CTOR.equals(((MethodInsnNode)insn).name) && delegateIndex == -1) { + delegateIndex = ctor.instructions.indexOf(insn); + start = line; + } + } else if (insn.getOpcode() == Opcodes.PUTFIELD) { + lineNumberIsValid = false; + } else if (insn.getOpcode() == Opcodes.RETURN) { + if (lineNumberIsValid) { + end = line; + } else { + end = start; + endReturn = insn; + } + } + } + + if (endReturn != null) { + LabelNode label = new LabelNode(new Label()); + ctor.instructions.insertBefore(endReturn, label); + ctor.instructions.insertBefore(endReturn, new LineNumberNode(start, label)); + } + + return new InsnRange(start, end, delegateIndex); + } +} diff --git a/src/main/java/org/spongepowered/asm/util/CompareUtil.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/IChainedDecoration.java similarity index 73% rename from src/main/java/org/spongepowered/asm/util/CompareUtil.java rename to src/main/java/org/spongepowered/asm/mixin/injection/struct/IChainedDecoration.java index 7b1be4f95..41b779e6c 100644 --- a/src/main/java/org/spongepowered/asm/util/CompareUtil.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/IChainedDecoration.java @@ -22,24 +22,22 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.asm.util; +package org.spongepowered.asm.mixin.injection.struct; /** - * Comparison helpers. + * An InjectionNode decoration which can chain to a previously registered + * decoration with the same type and key. + * + * @param the decoration type */ -public final class CompareUtil { - private CompareUtil() { +public interface IChainedDecoration { + + /** + * Called when this decoration replaces a previous decoration with the same + * key + * + * @param old The previous decoration + */ + public abstract void replace(T old); - } - - /** - * Officially added in Java 7. - */ - public static int compare(int a, int b) { - return ((a < b) ? -1 : ((a == b) ? 0 : 1)); - } - - public static int compare(long a, long b) { - return ((a < b) ? -1 : ((a == b) ? 0 : 1)); - } } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java index bf6be2c55..743ca0dff 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java @@ -31,7 +31,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -45,10 +44,9 @@ import org.objectweb.asm.Type; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.MethodNode; -import org.spongepowered.asm.mixin.Dynamic; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import org.spongepowered.asm.mixin.extensibility.IActivityContext.IActivity; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.InjectionPoint; import org.spongepowered.asm.mixin.injection.code.ISliceContext; @@ -56,22 +54,21 @@ import org.spongepowered.asm.mixin.injection.code.InjectorTarget; import org.spongepowered.asm.mixin.injection.code.MethodSlice; import org.spongepowered.asm.mixin.injection.code.MethodSlices; -import org.spongepowered.asm.mixin.injection.selectors.ElementNode; import org.spongepowered.asm.mixin.injection.selectors.ITargetSelector; -import org.spongepowered.asm.mixin.injection.selectors.ITargetSelector.Configure; -import org.spongepowered.asm.mixin.injection.selectors.InvalidSelectorException; import org.spongepowered.asm.mixin.injection.selectors.TargetSelector; -import org.spongepowered.asm.mixin.injection.selectors.throwables.SelectorConstraintException; +import org.spongepowered.asm.mixin.injection.selectors.TargetSelectors; +import org.spongepowered.asm.mixin.injection.selectors.TargetSelectors.SelectedMethod; import org.spongepowered.asm.mixin.injection.selectors.throwables.SelectorException; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.injection.throwables.InjectionError; import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; import org.spongepowered.asm.mixin.refmap.IMixinContext; +import org.spongepowered.asm.mixin.struct.AnnotatedMethodInfo; import org.spongepowered.asm.mixin.struct.SpecialMethodInfo; import org.spongepowered.asm.mixin.throwables.MixinError; import org.spongepowered.asm.mixin.throwables.MixinException; +import org.spongepowered.asm.mixin.transformer.ActivityStack; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; -import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.util.Annotations; import org.spongepowered.asm.util.Bytecode; @@ -80,7 +77,6 @@ import org.spongepowered.asm.util.logging.MessageRouter; import com.google.common.base.Joiner; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; /** @@ -112,12 +108,63 @@ public abstract class InjectionInfo extends SpecialMethodInfo implements ISliceC @java.lang.annotation.Target(ElementType.TYPE) public @interface HandlerPrefix { + /** + * Default conform prefix for handler methods + */ + public static final String DEFAULT = "handler"; + /** * String prefix for conforming handler methods */ public String value(); } + + /** + * Decoration for subclasses which specifies the order (phase) in which the + * injector should be applied relative to other injectors. Built-in + * injectors except for redirectors all run at DEFAULT unless specified in + * the injector annotation. + * + *

Injectors in the same order are sorted by mixin priority and + * declaration order within the mixin as always.

+ */ + @Retention(RetentionPolicy.RUNTIME) + @java.lang.annotation.Target(ElementType.TYPE) + public @interface InjectorOrder { + + /** + * An early injector, run before most injectors + */ + public static final int EARLY = 0; + + /** + * Default order, all injectors except redirect run here unless manually + * adjusted + */ + public static final int DEFAULT = 1000; + + /** + * Late injector, runs after most injectors but before redirects + */ + public static final int LATE = 2000; + + /** + * Built-in order for Redirect injectors + */ + public static final int REDIRECT = 10000; + + /** + * Injector which should run after redirect injector + */ + public static final int AFTER_REDIRECT = 20000; + + /** + * String prefix for conforming handler methods + */ + public int value() default InjectorOrder.DEFAULT; + + } /** * An injector registration entry @@ -141,7 +188,7 @@ static class InjectorEntry { this.annotationDesc = Type.getDescriptor(annotationType); HandlerPrefix handlerPrefix = type.getAnnotation(HandlerPrefix.class); - this.prefix = handlerPrefix != null ? handlerPrefix.value() : InjectionInfo.DEFAULT_PREFIX; + this.prefix = handlerPrefix != null ? handlerPrefix.value() : HandlerPrefix.DEFAULT; } InjectionInfo create(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { @@ -160,38 +207,6 @@ InjectionInfo create(MixinTargetContext mixin, MethodNode method, AnnotationNode } } - /** - * Selected target, paired with the selector which identified it - */ - static class SelectedTarget { - - private final ITargetSelector root; - - final ITargetSelector selector; - - final MethodNode method; - - SelectedTarget(ITargetSelector root, ITargetSelector selector, MethodNode method) { - this.root = root; - this.selector = selector; - this.method = method; - } - - SelectedTarget(ITargetSelector selector, MethodNode method) { - this(null, selector, method); - } - - ITargetSelector getRoot() { - return this.root != null ? this.root : this.selector; - } - - } - - /** - * Default conform prefix for handler methods - */ - public static final String DEFAULT_PREFIX = "handler"; - /** * Registry of subclasses */ @@ -213,6 +228,11 @@ ITargetSelector getRoot() { InjectionInfo.register(ModifyVariableInjectionInfo.class); // @ModifyVariable InjectionInfo.register(ModifyConstantInjectionInfo.class); // @ModifyConstant } + + /** + * Activity tracker + */ + protected final ActivityStack activities = new ActivityStack(null); /** * Annotated method is static @@ -220,14 +240,9 @@ ITargetSelector getRoot() { protected final boolean isStatic; /** - * Target selector(s) + * Targets */ - protected final Set selectors = new LinkedHashSet(); - - /** - * Target method(s) - */ - protected final List targets = new ArrayList(); + protected final TargetSelectors targets; /** * Method slice descriptors parsed from the annotation @@ -238,6 +253,8 @@ ITargetSelector getRoot() { * The key into the annotation which contains the injection points */ protected final String atKey; + + protected final List injectionPointAnnotations = new ArrayList(); /** * Injection points parsed from @@ -297,6 +314,12 @@ ITargetSelector getRoot() { */ private List messages; + /** + * Injector order, parsed from either the injector annotation or uses the + * default for this injection type + */ + private int order = InjectorOrder.DEFAULT; + /** * ctor * @@ -311,6 +334,7 @@ protected InjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationN protected InjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation, String atKey) { super(mixin, method, annotation); this.isStatic = Bytecode.isStatic(method); + this.targets = new TargetSelectors(this, mixin.getTargetClassNode()); this.slices = MethodSlices.parse(this); this.atKey = atKey; this.readAnnotation(); @@ -323,56 +347,46 @@ protected void readAnnotation() { if (this.annotation == null) { return; } - - List injectionPoints = this.readInjectionPoints(); - this.parseRequirements(); - this.parseSelectors(); - this.findTargets(); - this.parseInjectionPoints(injectionPoints); - this.injector = this.parseInjector(this.annotation); - } - - protected void parseSelectors() { - Set selectors = new LinkedHashSet(); - TargetSelector.parse(Annotations.getValue(this.annotation, "method", false), this, selectors); - TargetSelector.parse(Annotations.getValue(this.annotation, "target", false), this, selectors); - - // Raise an error if we have no selectors - if (selectors.size() == 0) { - throw new InvalidInjectionException(this, String.format("%s annotation on %s is missing 'method' or 'target' to specify targets", - this.annotationType, this.methodName)); - } - - // Validate and attach the parsed selectors - for (ITargetSelector selector : selectors) { - try { - this.selectors.add(selector.validate().attach(this)); - } catch (InvalidMemberDescriptorException ex) { - throw new InvalidInjectionException(this, String.format("%s annotation on %s, has invalid target descriptor: %s. %s", - this.annotationType, this.methodName, ex.getMessage(), this.mixin.getReferenceMapper().getStatus())); - } catch (TargetNotSupportedException ex) { - throw new InvalidInjectionException(this, - String.format("%s annotation on %s specifies a target class '%s', which is not supported", - this.annotationType, this.methodName, ex.getMessage())); - } catch (InvalidSelectorException ex) { - throw new InvalidInjectionException(this, - String.format("%s annotation on %s is decorated with an invalid selector: %s", this.annotationType, this.methodName, - ex.getMessage())); - } - } - } - protected List readInjectionPoints() { + this.activities.clear(); + try { + // When remapping refmap is enabled this implies we are in a development environment. In + // certain circumstances including the descriptor for the method may actually fail, so we + // will do a second "permissive" pass without the descriptor if this happens. + this.targets.setPermissivePass(this.mixin.getOption(Option.REFMAP_REMAP)); + + IActivity activity = this.activities.begin("Read Injection Points"); + this.readInjectionPoints(); + activity.next("Parse Requirements"); + this.parseRequirements(); + activity.next("Parse Order"); + this.parseOrder(); + activity.next("Parse Selectors"); + this.parseSelectors(); + activity.next("Find Targets"); + this.targets.find(); + activity.next("Validate Targets"); + this.targets.validate(this.expectedCallbackCount, this.requiredCallbackCount); + activity.next("Parse Injection Points"); + this.parseInjectionPoints(this.injectionPointAnnotations); + activity.next("Parse Injector"); + this.injector = this.parseInjector(this.annotation); + activity.end(); + } catch (InvalidMixinException ex) { + ex.prepend(this.activities); + throw ex; + } catch (Exception ex) { + throw new InvalidMixinException(this.mixin, "Unexpected " + ex.getClass().getSimpleName() + " parsing " + + this.getElementDescription(), ex, this.activities); + } + } + + protected void readInjectionPoints() { List ats = Annotations.getValue(this.annotation, this.atKey, false); if (ats == null) { - throw new InvalidInjectionException(this, String.format("%s annotation on %s is missing '%s' value(s)", - this.annotationType, this.methodName, this.atKey)); + throw new InvalidInjectionException(this, String.format("%s is missing '%s' value(s)", this.getElementDescription(), this.atKey)); } - return ats; - } - - protected void parseInjectionPoints(List ats) { - this.injectionPoints.addAll(InjectionPoint.parse(this, ats)); + this.injectionPointAnnotations.addAll(ats); } protected void parseRequirements() { @@ -395,6 +409,35 @@ protected void parseRequirements() { this.maxCallbackCount = Math.max(Math.max(this.requiredCallbackCount, 1), allow); } } + + protected void parseOrder() { + Integer userOrder = Annotations.getValue(this.annotation, "order"); + if (userOrder != null) { + this.order = userOrder.intValue(); + return; + } + + InjectorOrder injectorDefault = this.getClass().getAnnotation(InjectorOrder.class); + this.order = injectorDefault != null ? injectorDefault.value() : InjectorOrder.DEFAULT; + } + + protected void parseSelectors() { + Set selectors = new LinkedHashSet(); + TargetSelector.parse(Annotations.getValue(this.annotation, "method", false), this, selectors); + TargetSelector.parse(Annotations.getValue(this.annotation, "target", false), this, selectors); + + // Raise an error if we have no selectors + if (selectors.size() == 0) { + throw new InvalidInjectionException(this, String.format("%s is missing 'method' or 'target' to specify targets", + this.getElementDescription())); + } + + this.targets.parse(selectors); + } + + protected void parseInjectionPoints(List ats) { + this.injectionPoints.addAll(InjectionPoint.parse(this, ats)); + } // stub protected abstract Injector parseInjector(AnnotationNode injectAnnotation); @@ -409,22 +452,42 @@ public boolean isValid() { return this.targets.size() > 0 && this.injectionPoints.size() > 0; } + /** + * Get the application order for this injector type + */ + public int getOrder() { + return this.order; + } + /** * Discover injection points */ public void prepare() { - this.targetNodes.clear(); - for (SelectedTarget targetMethod : this.targets) { - Target target = this.mixin.getTargetMethod(targetMethod.method); - InjectorTarget injectorTarget = new InjectorTarget(this, target, targetMethod.selector); - try { - this.targetNodes.put(target, this.injector.find(injectorTarget, this.injectionPoints)); - } catch (SelectorException ex) { - throw new InvalidInjectionException(this, String.format("Injection validation failed: %s on %s: %s. %s%s", - this.annotationType, this.methodName, ex.getMessage(), this.mixin.getReferenceMapper().getStatus(), this.getDynamicInfo())); - } finally { - injectorTarget.dispose(); + this.activities.clear(); + try { + this.targetNodes.clear(); + IActivity activity = this.activities.begin("?"); + for (SelectedMethod targetMethod : this.targets) { + activity.next("{ target: %s }", targetMethod); + Target target = this.mixin.getTargetMethod(targetMethod.getMethod()); + InjectorTarget injectorTarget = new InjectorTarget(this, target, targetMethod); + try { + this.targetNodes.put(target, this.injector.find(injectorTarget, this.injectionPoints)); + } catch (SelectorException ex) { + throw new InvalidInjectionException(this, String.format("Injection validation failed: %s: %s. %s%s", + this.getElementDescription(), ex.getMessage(), this.mixin.getReferenceMapper().getStatus(), + AnnotatedMethodInfo.getDynamicInfo(this.method))); + } finally { + injectorTarget.dispose(); + } } + activity.end(); + } catch (InvalidMixinException ex) { + ex.prepend(this.activities); + throw ex; + } catch (Exception ex) { + throw new InvalidMixinException(this.mixin, "Unexpecteded " + ex.getClass().getSimpleName() + " preparing " + + this.getElementDescription(), ex, this.activities); } } @@ -457,7 +520,7 @@ public void postInject() { String description = this.getDescription(); String refMapStatus = this.mixin.getReferenceMapper().getStatus(); - String extraInfo = this.getDynamicInfo() + this.getMessages(); + String extraInfo = AnnotatedMethodInfo.getDynamicInfo(this.method) + this.getMessages(); if ((this.mixin.getOption(Option.DEBUG_INJECTORS) && this.injectedCallbackCount < this.expectedCallbackCount)) { throw new InvalidInjectionException(this, String.format("Injection validation failed: %s %s%s in %s expected %d invocation(s) but %d succeeded. Scanned %d target(s). %s%s", @@ -473,7 +536,7 @@ public void postInject() { String.format("Critical injection failure: %s %s%s in %s failed injection check, %d succeeded of %d allowed.%s", description, this.methodName, this.method.desc, this.mixin, this.injectedCallbackCount, this.maxCallbackCount, extraInfo)); } - + this.slices.postInject(); } @@ -579,115 +642,6 @@ public void addMessage(String format, Object... args) { protected String getMessages() { return this.messages != null ? " Messages: { " + Joiner.on(" ").join(this.messages) + "}" : ""; } - - /** - * Find methods in the target class which match the parsed selectors - */ - protected void findTargets() { - this.targets.clear(); - this.findRootTargets(); - this.validateTargets(); - } - - /** - * Evaluate the root selectors parsed from this injector, find the root - * targets and store them in the {@link #targets} collection. - */ - private void findRootTargets() { - // When remapping refmap is enabled this implies we are in a development - // environment. In certain circumstances including the descriptor for - // the method may actually fail, so we will do a second pass without the - // descriptor if this happens. - int passes = this.mixin.getOption(Option.REFMAP_REMAP) ? 2 : 1; - - for (ITargetSelector selector : this.selectors) { - selector = selector.configure(Configure.SELECT_MEMBER); - - int matchCount = 0; - int maxCount = selector.getMaxMatchCount(); - - // Second pass ignores descriptor - ITargetSelector permissiveSelector = selector.configure(Configure.PERMISSIVE); - int selectorPasses = (permissiveSelector == selector) ? 1 : passes; - - scan: for (int pass = 0; pass < selectorPasses && matchCount < 1; pass++) { - ITargetSelector passSelector = pass == 0 ? selector : permissiveSelector; - for (MethodNode target : this.classNode.methods) { - if (passSelector.match(ElementNode.of(this.classNode, target)).isExactMatch()) { - matchCount++; - - boolean isMixinMethod = Annotations.getVisible(target, MixinMerged.class) != null; - if (maxCount <= 1 || ((this.isStatic || !Bytecode.isStatic(target)) && target != this.method && !isMixinMethod)) { - this.checkTarget(target); - this.targets.add(new SelectedTarget(passSelector, target)); - } - - if (matchCount >= maxCount) { - break scan; - } - } - } - } - - if (matchCount < selector.getMinMatchCount()) { - throw new InvalidInjectionException(this, new SelectorConstraintException(selector, String.format( - "Injection validation failed: %s for %s on %s did not match the required number of targets (required=%d, matched=%d). %s%s", - selector, this.annotationType, this.methodName, selector.getMinMatchCount(), matchCount, - this.mixin.getReferenceMapper().getStatus(), this.getDynamicInfo()))); - } - } - } - - /** - * Post-search validation that some targets were found, we can fail-fast if - * no targets were actually identified - */ - protected void validateTargets() { - this.targetCount = this.targets.size(); - if (this.targetCount > 0) { - return; - } - - if ((this.mixin.getOption(Option.DEBUG_INJECTORS) && this.expectedCallbackCount > 0)) { - throw new InvalidInjectionException(this, - String.format("Injection validation failed: %s annotation on %s could not find any targets matching %s in %s. %s%s", - this.annotationType, this.methodName, InjectionInfo.namesOf(this.selectors), this.mixin.getTarget(), - this.mixin.getReferenceMapper().getStatus(), this.getDynamicInfo())); - } else if (this.requiredCallbackCount > 0) { - throw new InvalidInjectionException(this, - String.format("Critical injection failure: %s annotation on %s could not find any targets matching %s in %s. %s%s", - this.annotationType, this.methodName, InjectionInfo.namesOf(this.selectors), this.mixin.getTarget(), - this.mixin.getReferenceMapper().getStatus(), this.getDynamicInfo())); - } - } - - protected void checkTarget(MethodNode target) { - AnnotationNode merged = Annotations.getVisible(target, MixinMerged.class); - if (merged == null) { - return; - } - - if (Annotations.getVisible(target, Final.class) != null) { - throw new InvalidInjectionException(this, String.format("%s cannot inject into @Final method %s::%s%s merged by %s", this, - this.classNode.name, target.name, target.desc, Annotations.getValue(merged, "mixin"))); - } - } - - /** - * Get info from a decorating {@link Dynamic} annotation. If the annotation - * is present, a descriptive string suitable for inclusion in an error - * message is returned. If the annotation is not present then an empty - * string is returned. - */ - protected String getDynamicInfo() { - AnnotationNode annotation = Annotations.getInvisible(this.method, Dynamic.class); - String description = Strings.nullToEmpty(Annotations.getValue(annotation)); - Type upstream = Annotations.getValue(annotation, "mixin"); - if (upstream != null) { - description = String.format("{%s} %s", upstream.getClassName(), description).trim(); - } - return description.length() > 0 ? String.format(" Method is @Dynamic(%s).", description) : ""; - } /** * Parse an injector from the specified method (if an injector annotation is @@ -742,7 +696,7 @@ public static AnnotationNode getInjectorAnnotation(IMixinInfo mixin, MethodNode */ public static String getInjectorPrefix(AnnotationNode annotation) { if (annotation == null) { - return InjectionInfo.DEFAULT_PREFIX; + return HandlerPrefix.DEFAULT; } for (InjectorEntry injector : InjectionInfo.registry.values()) { @@ -751,36 +705,13 @@ public static String getInjectorPrefix(AnnotationNode annotation) { } } - return InjectionInfo.DEFAULT_PREFIX; + return HandlerPrefix.DEFAULT; } static String describeInjector(IMixinContext mixin, AnnotationNode annotation, MethodNode method) { return String.format("%s->@%s::%s%s", mixin.toString(), Annotations.getSimpleName(annotation), MethodNodeEx.getName(method), method.desc); } - /** - * Print the names of the specified members as a human-readable list - * - * @param selectors members to print - * @return human-readable list of member names - */ - private static String namesOf(Collection selectors) { - int index = 0, count = selectors.size(); - StringBuilder sb = new StringBuilder(); - for (ITargetSelector selector : selectors) { - if (index > 0) { - if (index == (count - 1)) { - sb.append(" or "); - } else { - sb.append(", "); - } - } - sb.append('\'').append(selector.toString()).append('\''); - index++; - } - return sb.toString(); - } - /** * Register an injector info class. The supplied class must be decorated * with an {@link AnnotationType} annotation for registration purposes. diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionNodes.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionNodes.java index e21756004..44bcb0ee8 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionNodes.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionNodes.java @@ -30,7 +30,6 @@ import org.objectweb.asm.tree.AbstractInsnNode; import org.spongepowered.asm.util.Bytecode; -import org.spongepowered.asm.util.CompareUtil; /** * Used to keep track of instruction nodes in a {@link Target} method which are @@ -163,10 +162,17 @@ public boolean isRemoved() { * @param value meta value * @param value type */ + @SuppressWarnings("unchecked") public InjectionNode decorate(String key, V value) { if (this.decorations == null) { this.decorations = new HashMap(); } + if (value instanceof IChainedDecoration && this.decorations.containsKey(key)) { + Object previous = this.decorations.get(key); + if (previous.getClass().equals(value.getClass())) { + ((IChainedDecoration)value).replace(previous); + } + } this.decorations.put(key, value); return this; } @@ -192,13 +198,27 @@ public boolean hasDecoration(String key) { public V getDecoration(String key) { return (V) (this.decorations == null ? null : this.decorations.get(key)); } + + /** + * Get the specified decoration or default value + * + * @param key meta key + * @param defaultValue default value to return + * @param value type + * @return decoration value or null if absent + */ + @SuppressWarnings("unchecked") + public V getDecoration(String key, V defaultValue) { + V existing = (V) (this.decorations == null ? null : this.decorations.get(key)); + return existing != null ? existing : defaultValue; + } /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ @Override public int compareTo(InjectionNode other) { - return other == null ? Integer.MAX_VALUE : CompareUtil.compare(this.hashCode(), other.hashCode()); + return other == null ? Integer.MAX_VALUE : Integer.compare(this.hashCode(), other.hashCode()); } /* (non-Javadoc) @@ -259,11 +279,12 @@ public boolean contains(AbstractInsnNode node) { * @param oldNode node being replaced * @param newNode node to replace with */ - public void replace(AbstractInsnNode oldNode, AbstractInsnNode newNode) { + public InjectionNode replace(AbstractInsnNode oldNode, AbstractInsnNode newNode) { InjectionNode injectionNode = this.get(oldNode); if (injectionNode != null) { injectionNode.replace(newNode); - } + } + return injectionNode; } /** @@ -272,11 +293,12 @@ public void replace(AbstractInsnNode oldNode, AbstractInsnNode newNode) { * * @param node node being removed */ - public void remove(AbstractInsnNode node) { + public InjectionNode remove(AbstractInsnNode node) { InjectionNode injectionNode = this.get(node); if (injectionNode != null) { injectionNode.remove(); } + return injectionNode; } } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionPointData.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionPointData.java index b41cdd7ce..2a36721c7 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionPointData.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionPointData.java @@ -27,6 +27,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,6 +38,8 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.IInjectionPointContext; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.InjectionPoint; +import org.spongepowered.asm.mixin.injection.InjectionPoint.RestrictTargetLevel; import org.spongepowered.asm.mixin.injection.InjectionPoint.Specifier; import org.spongepowered.asm.mixin.injection.modify.LocalVariableDiscriminator; import org.spongepowered.asm.mixin.injection.selectors.ITargetSelector; @@ -46,11 +50,13 @@ import org.spongepowered.asm.mixin.refmap.IMixinContext; import org.spongepowered.asm.util.Annotations; import org.spongepowered.asm.util.Annotations.Handle; +import org.spongepowered.asm.util.Bytecode; import org.spongepowered.asm.util.IMessageSink; import org.spongepowered.asm.util.asm.IAnnotationHandle; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.common.primitives.Ints; /** * Data read from an {@link org.spongepowered.asm.mixin.injection.At} annotation @@ -62,7 +68,7 @@ public class InjectionPointData { * Regex for recognising at declarations */ private static final Pattern AT_PATTERN = InjectionPointData.createPattern(); - + /** * K/V arguments parsed from the "args" node in the {@link At} annotation */ @@ -88,6 +94,11 @@ public class InjectionPointData { */ private final Specifier specifier; + /** + * Target restriction from the at annotation, if present + */ + private final RestrictTargetLevel targetRestriction; + /** * Target */ @@ -113,8 +124,13 @@ public class InjectionPointData { */ private final String id; + /** + * Flags from annotation parser + */ + private final int flags; + public InjectionPointData(IInjectionPointContext context, String at, List args, String target, - String slice, int ordinal, int opcode, String id) { + String slice, int ordinal, int opcode, String id, int flags) { this.context = context; this.at = at; this.target = target; @@ -122,6 +138,7 @@ public InjectionPointData(IInjectionPointContext context, String at, List args) { @@ -177,7 +196,14 @@ public String getType() { public Specifier getSpecifier() { return this.specifier; } - + + /** + * Get the target restriction specified in the annotation + */ + public RestrictTargetLevel getTargetRestriction() { + return this.targetRestriction; + } + /** * Get the injection point context */ @@ -260,6 +286,19 @@ public int get(String key, int defaultValue) { public boolean get(String key, boolean defaultValue) { return InjectionPointData.parseBoolean(this.get(key, String.valueOf(defaultValue)), defaultValue); } + + /** + * Get the supplied value from the named args, return defaultValue if the + * arg is not set + * + * @param enum type + * @param key argument name + * @param defaultValue value to return if the arg is not set + * @return argument value or default if not set + */ + public > T get(String key, T defaultValue) { + return InjectionPointData.parseEnum(this.get(key, defaultValue.name()), defaultValue); + } /** * Get the supplied value from the named args as a target selector, @@ -348,7 +387,37 @@ public int getOpcode(int defaultOpcode, int... validOpcodes) { } return defaultOpcode; } - + + /** + * Get a list of opcodes specified in the injection point arguments. The + * opcodes can be specified as raw integer values or as their corresponding + * constant name from the {@link Opcodes} interface. All the values should + * be separated by spaces or commas. The returned array is sorted in order + * to make it suitable for use with the {@link Arrays#binarySearch} method. + * + * @param key argument name + * @param defaultValue value to return if the key is not specified + * @return parsed opcodes as array or default value if the key is not + * specified in the args + */ + public int[] getOpcodeList(String key, int[] defaultValue) { + String value = this.args.get(key); + if (value == null) { + return defaultValue; + } + + Set parsed = new TreeSet(); + String[] values = value.split("[ ,;]"); + for (String strOpcode : values) { + int opcode = Bytecode.parseOpcodeName(strOpcode.trim()); + if (opcode > 0) { + parsed.add(opcode); + } + } + + return Ints.toArray(parsed); + } + /** * Get the id specified on the injection point (or null if not specified) */ @@ -356,6 +425,13 @@ public String getId() { return this.id; } + /** + * Get whether the unsafe option is set on the injection point + */ + public boolean isUnsafe() { + return (this.flags & InjectionPoint.Flags.UNSAFE) != 0; + } + @Override public String toString() { return this.type; @@ -400,4 +476,12 @@ private static boolean parseBoolean(String string, boolean defaultValue) { } } + @SuppressWarnings("unchecked") + private static > T parseEnum(String string, T defaultValue) { + try { + return (T)Enum.valueOf(defaultValue.getClass(), string); + } catch (Exception ex) { + return defaultValue; + } + } } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgInjectionInfo.java index 9446e9805..aa8f615bd 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgInjectionInfo.java @@ -31,6 +31,7 @@ import org.spongepowered.asm.mixin.injection.invoke.ModifyArgInjector; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.AnnotationType; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.HandlerPrefix; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.InjectorOrder; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; import org.spongepowered.asm.util.Annotations; @@ -39,6 +40,7 @@ */ @AnnotationType(ModifyArg.class) @HandlerPrefix("modify") +@InjectorOrder(InjectorOrder.DEFAULT) public class ModifyArgInjectionInfo extends InjectionInfo { public ModifyArgInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgsInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgsInjectionInfo.java index 52ff5573e..a97b8fedb 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgsInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyArgsInjectionInfo.java @@ -31,6 +31,7 @@ import org.spongepowered.asm.mixin.injection.invoke.ModifyArgsInjector; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.AnnotationType; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.HandlerPrefix; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.InjectorOrder; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; /** @@ -38,6 +39,7 @@ */ @AnnotationType(ModifyArgs.class) @HandlerPrefix("args") +@InjectorOrder(InjectorOrder.DEFAULT) public class ModifyArgsInjectionInfo extends InjectionInfo { public ModifyArgsInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyConstantInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyConstantInjectionInfo.java index ad184f183..4573bacd1 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyConstantInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyConstantInjectionInfo.java @@ -36,16 +36,17 @@ import org.spongepowered.asm.mixin.injection.points.BeforeConstant; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.AnnotationType; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.HandlerPrefix; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.InjectorOrder; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; /** * Information about a constant modifier injector */ @AnnotationType(ModifyConstant.class) @HandlerPrefix("constant") +@InjectorOrder(InjectorOrder.REDIRECT) public class ModifyConstantInjectionInfo extends InjectionInfo { private static final String CONSTANT_ANNOTATION_CLASS = Constant.class.getName().replace('.', '/'); @@ -55,14 +56,13 @@ public ModifyConstantInjectionInfo(MixinTargetContext mixin, MethodNode method, } @Override - protected List readInjectionPoints() { - List ats = super.readInjectionPoints(); - if (ats.isEmpty()) { + protected void readInjectionPoints() { + super.readInjectionPoints(); + if (this.injectionPointAnnotations.isEmpty()) { AnnotationNode c = new AnnotationNode(ModifyConstantInjectionInfo.CONSTANT_ANNOTATION_CLASS); c.visit("log", Boolean.TRUE); - ats = ImmutableList.of(c); + this.injectionPointAnnotations.add(c); } - return ats; } @Override diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyVariableInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyVariableInjectionInfo.java index cefb00ae2..2fa7c753a 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyVariableInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/ModifyVariableInjectionInfo.java @@ -32,6 +32,7 @@ import org.spongepowered.asm.mixin.injection.modify.ModifyVariableInjector; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.AnnotationType; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.HandlerPrefix; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.InjectorOrder; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; /** @@ -39,6 +40,7 @@ */ @AnnotationType(ModifyVariable.class) @HandlerPrefix("localvar") +@InjectorOrder(InjectorOrder.DEFAULT) public class ModifyVariableInjectionInfo extends InjectionInfo { public ModifyVariableInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/RedirectInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/RedirectInjectionInfo.java index 91ee7cd8d..0f048ff33 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/RedirectInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/RedirectInjectionInfo.java @@ -31,6 +31,7 @@ import org.spongepowered.asm.mixin.injection.invoke.RedirectInjector; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.AnnotationType; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.HandlerPrefix; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.InjectorOrder; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; /** @@ -38,6 +39,7 @@ */ @AnnotationType(Redirect.class) @HandlerPrefix("redirect") +@InjectorOrder(InjectorOrder.REDIRECT) public class RedirectInjectionInfo extends InjectionInfo { public RedirectInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/SelectorAnnotationContext.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/SelectorAnnotationContext.java index 605d0b087..76756c8d3 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/SelectorAnnotationContext.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/SelectorAnnotationContext.java @@ -127,5 +127,14 @@ public String getSelectorCoordinate(boolean leaf) { public String remap(String reference) { return this.parent.remap(reference); } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.injection.selectors.ISelectorContext + * #getElementDescription() + */ + @Override + public String getElementDescription() { + return String.format("%s in %s", this.selectorAnnotation, this.parent.getElementDescription()); + } } diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/Target.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/Target.java index 971a8d595..912b833e5 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/Target.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/Target.java @@ -39,16 +39,17 @@ import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.points.BeforeNew; import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; import org.spongepowered.asm.mixin.transformer.ClassInfo; import org.spongepowered.asm.util.Bytecode; -import org.spongepowered.asm.util.Bytecode.DelegateInitialiser; import org.spongepowered.asm.util.Constants; import org.spongepowered.asm.util.Locals.SyntheticLocalVariableNode; /** - * Information about the current injection target, mainly just convenience - * rather than passing a bunch of values around. + * Information about the current injection target (method) which bundles common + * injection context with the target method in order to allow injectors to + * interoperate. */ public class Target implements Comparable, Iterable { @@ -156,6 +157,11 @@ public void apply() { } } + + /** + * Target class info + */ + public final ClassInfo classInfo; /** * Target class node @@ -177,11 +183,6 @@ public void apply() { */ public final boolean isStatic; - /** - * True if the method is a constructor - */ - public final boolean isCtor; - /** * Method arguments */ @@ -231,23 +232,18 @@ public void apply() { * Labels for LVT ranges, generated as needed */ private LabelNode start, end; - - /** - * Cached delegate initialiser call - */ - private DelegateInitialiser delegateInitialiser; /** * Make a new Target for the supplied method * * @param method target method */ - public Target(ClassNode classNode, MethodNode method) { + Target(ClassInfo classInfo, ClassNode classNode, MethodNode method) { + this.classInfo = classInfo; this.classNode = classNode; this.method = method; this.insns = method.instructions; this.isStatic = Bytecode.isStatic(method); - this.isCtor = method.name.equals(Constants.CTOR); this.arguments = Type.getArgumentTypes(method.desc); this.returnType = Type.getReturnType(method.desc); @@ -277,6 +273,27 @@ public InjectionNode getInjectionNode(AbstractInsnNode node) { return this.injectionNodes.get(node); } + /** + * Get the target method name + */ + public String getName() { + return this.method.name; + } + + /** + * Get the target method descriptor + */ + public String getDesc() { + return this.method.desc; + } + + /** + * Get the target method signature + */ + public String getSignature() { + return this.method.signature; + } + /** * Get the original max locals of the method * @@ -424,6 +441,22 @@ private void setMaxStack(int maxStack) { * of the supplied args array */ public int[] generateArgMap(Type[] args, int start) { + return this.generateArgMap(args, start, false); + } + + /** + * Generate an array containing local indexes for the specified args, + * returns an array of identical size to the supplied array with an + * allocated local index in each corresponding position + * + * @param args Argument types + * @param start starting index + * @param fresh allocate fresh locals only, do not reuse existing argmap + * slots + * @return array containing a corresponding local arg index for each member + * of the supplied args array + */ + public int[] generateArgMap(Type[] args, int start, boolean fresh) { if (this.argMapVars == null) { this.argMapVars = new ArrayList(); } @@ -431,7 +464,7 @@ public int[] generateArgMap(Type[] args, int start) { int[] argMap = new int[args.length]; for (int arg = start, index = 0; arg < args.length; arg++) { int size = args[arg].getSize(); - argMap[arg] = this.allocateArgMapLocal(index, size); + argMap[arg] = fresh ? this.allocateLocals(size) : this.allocateArgMapLocal(index, size); index += size; } return argMap; @@ -545,7 +578,7 @@ public String getCallbackDescriptor(final Type[] locals, Type[] argumentTypes) { */ public String getCallbackDescriptor(final boolean captureLocals, final Type[] locals, Type[] argumentTypes, int startIndex, int extra) { if (this.callbackDescriptor == null) { - this.callbackDescriptor = String.format("(%sL%s;)V", this.method.desc.substring(1, this.method.desc.indexOf(')')), + this.callbackDescriptor = String.format("(%sL%s;)V", this.getDesc().substring(1, this.getDesc().indexOf(')')), this.getCallbackInfoClass()); } @@ -566,7 +599,7 @@ public String getCallbackDescriptor(final boolean captureLocals, final Type[] lo @Override public String toString() { - return String.format("%s::%s%s", this.classNode.name, this.method.name, this.method.desc); + return String.format("%s::%s%s", this.classNode.name, this.getName(), this.getDesc()); } @Override @@ -617,47 +650,70 @@ public Iterator iterator() { /** * Find the first <init> invocation after the specified - * NEW insn + * NEW insn * * @param newNode NEW insn * @return INVOKESPECIAL opcode of ctor, or null if not found */ public MethodInsnNode findInitNodeFor(TypeInsnNode newNode) { - int start = this.indexOf(newNode); - for (Iterator iter = this.insns.iterator(start); iter.hasNext();) { - AbstractInsnNode insn = iter.next(); - if (insn instanceof MethodInsnNode && insn.getOpcode() == Opcodes.INVOKESPECIAL) { - MethodInsnNode methodNode = (MethodInsnNode)insn; - if (Constants.CTOR.equals(methodNode.name) && methodNode.owner.equals(newNode.desc)) { - return methodNode; - } - } - } - return null; + return this.findInitNodeFor(newNode, null); + } + + /** + * Find the matching <init> invocation after the specified + * NEW insn, ensuring that the supplied descriptor matches. If the + * supplied descriptor is null then any invocation matches. If + * additional NEW insns are encountered then corresponding + * <init> calls are skipped. + * + * @param newNode NEW insn + * @param desc Descriptor to match + * @return INVOKESPECIAL opcode of ctor, or null if not found + */ + public MethodInsnNode findInitNodeFor(TypeInsnNode newNode, String desc) { + return BeforeNew.findInitNodeFor(this.insns, newNode, desc); } /** - * Find the call to super() or this() in a constructor. - * This attempts to locate the first call to <init> which - * isn't an inline call to another object ctor being passed into the super - * invocation. + * Insert the supplied instructions after the specified instruction * - * @return Call to super(), this() or - * DelegateInitialiser.NONE if not found + * @param location Instruction to insert before + * @param insns Instructions to insert */ - public DelegateInitialiser findDelegateInitNode() { - if (!this.isCtor) { - return null; - } - - if (this.delegateInitialiser == null) { - String superName = ClassInfo.forName(this.classNode.name).getSuperName(); - this.delegateInitialiser = Bytecode.findDelegateInit(this.method, superName, this.classNode.name); - } - - return this.delegateInitialiser; + public void insert(InjectionNode location, final InsnList insns) { + this.insns.insert(location.getCurrentTarget(), insns); } + /** + * Insert the supplied instruction after the specified instruction + * + * @param location Instruction to insert before + * @param insn Instruction to insert + */ + public void insert(InjectionNode location, final AbstractInsnNode insn) { + this.insns.insert(location.getCurrentTarget(), insn); + } + + /** + * Insert the supplied instructions after the specified instruction + * + * @param location Instruction to insert before + * @param insns Instructions to insert + */ + public void insert(AbstractInsnNode location, final InsnList insns) { + this.insns.insert(location, insns); + } + + /** + * Insert the supplied instruction after the specified instruction + * + * @param location Instruction to insert before + * @param insn Instruction to insert + */ + public void insert(AbstractInsnNode location, final AbstractInsnNode insn) { + this.insns.insert(location, insn); + } + /** * Insert the supplied instructions before the specified instruction * @@ -667,6 +723,16 @@ public DelegateInitialiser findDelegateInitNode() { public void insertBefore(InjectionNode location, final InsnList insns) { this.insns.insertBefore(location.getCurrentTarget(), insns); } + + /** + * Insert the supplied instruction before the specified instruction + * + * @param location Instruction to insert before + * @param insn Instruction to insert + */ + public void insertBefore(InjectionNode location, final AbstractInsnNode insn) { + this.insns.insertBefore(location.getCurrentTarget(), insn); + } /** * Insert the supplied instructions before the specified instruction @@ -678,6 +744,16 @@ public void insertBefore(AbstractInsnNode location, final InsnList insns) { this.insns.insertBefore(location, insns); } + /** + * Insert the supplied instruction before the specified instruction + * + * @param location Instruction to insert before + * @param insn Instruction to insert + */ + public void insertBefore(AbstractInsnNode location, final AbstractInsnNode insn) { + this.insns.insertBefore(location, insn); + } + /** * Replace an instruction in this target with the specified instruction and * mark the node as replaced for other injectors @@ -806,4 +882,11 @@ private LabelNode getEndLabel() { return this.end; } + public static Target of(ClassInfo classInfo, ClassNode classNode, MethodNode method) { + if (method.name.equals(Constants.CTOR)) { + return new Constructor(classInfo, classNode, method); + } + return new Target(classInfo, classNode, method); + } + } diff --git a/src/main/java/org/spongepowered/asm/mixin/refmap/IMixinContext.java b/src/main/java/org/spongepowered/asm/mixin/refmap/IMixinContext.java index 22ed77d16..0ab2e95e5 100644 --- a/src/main/java/org/spongepowered/asm/mixin/refmap/IMixinContext.java +++ b/src/main/java/org/spongepowered/asm/mixin/refmap/IMixinContext.java @@ -56,6 +56,13 @@ public interface IMixinContext { * @return internal class name */ public abstract String getClassRef(); + + /** + * Get the name of the target class for this context + * + * @return target class name + */ + public abstract String getTargetClassName(); /** * Get the internal name of the target class for this context diff --git a/src/main/java/org/spongepowered/asm/mixin/struct/AnnotatedMethodInfo.java b/src/main/java/org/spongepowered/asm/mixin/struct/AnnotatedMethodInfo.java index 4fb3c7fdb..5ac54bb87 100644 --- a/src/main/java/org/spongepowered/asm/mixin/struct/AnnotatedMethodInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/struct/AnnotatedMethodInfo.java @@ -28,17 +28,23 @@ import javax.tools.Diagnostic.Kind; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.MethodNode; +import org.spongepowered.asm.mixin.Dynamic; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.injection.IInjectionPointContext; import org.spongepowered.asm.mixin.injection.selectors.ISelectorContext; import org.spongepowered.asm.mixin.refmap.IMixinContext; import org.spongepowered.asm.mixin.refmap.IReferenceMapper; import org.spongepowered.asm.util.Annotations; +import org.spongepowered.asm.util.asm.IAnnotatedElement; import org.spongepowered.asm.util.asm.IAnnotationHandle; +import org.spongepowered.asm.util.asm.MethodNodeEx; import org.spongepowered.asm.util.logging.MessageRouter; +import com.google.common.base.Strings; + /** * Data bundle for an annotated method in a mixin */ @@ -59,12 +65,33 @@ public class AnnotatedMethodInfo implements IInjectionPointContext { */ protected final AnnotationNode annotation; + /** + * Human-readable annotation type + */ + protected final String annotationType; + + /** + * Original name of the method, if available + */ + protected final String methodName; + public AnnotatedMethodInfo(IMixinContext mixin, MethodNode method, AnnotationNode annotation) { this.context = mixin; this.method = method; this.annotation = annotation; + this.annotationType = this.annotation != null ? "@" + Annotations.getSimpleName(this.annotation) : "Undecorated method"; + this.methodName = MethodNodeEx.getName(method); } + /** + * Get a human-readable description of the annotation on the method for use + * in error messages + */ + @Override + public final String getElementDescription() { + return String.format("%s annotation on %s", this.annotationType, this.methodName); + } + @Override public String remap(String reference) { if (this.context != null) { @@ -160,4 +187,57 @@ public void addMessage(String format, Object... args) { } } + /** + * Get info from a decorating {@link Dynamic} annotation. If the annotation + * is present, a descriptive string suitable for inclusion in an error + * message is returned. If the annotation is not present then an empty + * string is returned. + * + * @param method method to inspect + */ + public static final String getDynamicInfo(Object method) { + if (method instanceof MethodNode) { + return AnnotatedMethodInfo.getDynamicInfo((MethodNode)method); + } else if (method instanceof IAnnotatedElement) { + return AnnotatedMethodInfo.getDynamicInfo((IAnnotatedElement)method); + } + return ""; + } + + /** + * Get info from a decorating {@link Dynamic} annotation. If the annotation + * is present, a descriptive string suitable for inclusion in an error + * message is returned. If the annotation is not present then an empty + * string is returned. + * + * @param method method to inspect + */ + public static final String getDynamicInfo(MethodNode method) { + return AnnotatedMethodInfo.getDynamicInfo(Annotations.handleOf(Annotations.getInvisible(method, Dynamic.class))); + } + + /** + * Get info from a decorating {@link Dynamic} annotation. If the annotation + * is present, a descriptive string suitable for inclusion in an error + * message is returned. If the annotation is not present then an empty + * string is returned. + * + * @param method method to inspect + */ + public static final String getDynamicInfo(IAnnotatedElement method) { + return AnnotatedMethodInfo.getDynamicInfo(method.getAnnotation(Dynamic.class)); + } + + private static String getDynamicInfo(IAnnotationHandle annotation) { + if (annotation == null) { + return ""; + } + String description = Strings.nullToEmpty(annotation.getValue()); + Type upstream = annotation.getTypeValue("mixin"); + if (upstream != null) { + description = String.format("{%s} %s", upstream.getClassName(), description).trim(); + } + return description.length() > 0 ? String.format(" Method is @Dynamic(%s).", description) : ""; + } + } diff --git a/src/main/java/org/spongepowered/asm/mixin/struct/SpecialMethodInfo.java b/src/main/java/org/spongepowered/asm/mixin/struct/SpecialMethodInfo.java index d509a8982..8201aa00c 100644 --- a/src/main/java/org/spongepowered/asm/mixin/struct/SpecialMethodInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/struct/SpecialMethodInfo.java @@ -29,8 +29,6 @@ import org.objectweb.asm.tree.MethodNode; import org.spongepowered.asm.mixin.transformer.ClassInfo; import org.spongepowered.asm.mixin.transformer.MixinTargetContext; -import org.spongepowered.asm.util.Annotations; -import org.spongepowered.asm.util.asm.MethodNodeEx; /** * Information about a special mixin method such as an injector or accessor @@ -38,19 +36,9 @@ public class SpecialMethodInfo extends AnnotatedMethodInfo { /** - * Human-readable annotation type - */ - protected final String annotationType; - - /** - * Class + * Target class node */ protected final ClassNode classNode; - - /** - * Original name of the method, if available - */ - protected final String methodName; /** * Mixin data @@ -60,20 +48,36 @@ public class SpecialMethodInfo extends AnnotatedMethodInfo { public SpecialMethodInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { super(mixin, method, annotation); this.mixin = mixin; - this.annotationType = this.annotation != null ? "@" + Annotations.getSimpleName(this.annotation) : "Undecorated injector"; this.classNode = mixin.getTargetClassNode(); - this.methodName = MethodNodeEx.getName(method); } /** * Get the class node for this injection * * @return the class containing the injector and the target + * @deprecated use getTargetClassNode instead */ + @Deprecated public final ClassNode getClassNode() { return this.classNode; } + + /** + * Get the target class node for this injection + * + * @return the class containing the injector and the target + */ + public final ClassNode getTargetClassNode() { + return this.classNode; + } + /** + * Get the class metadata for the target class + */ + public final ClassInfo getTargetClassInfo() { + return this.mixin.getTargetClassInfo(); + } + /** * Get the class metadata for the mixin */ 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 538198cda..66fa2a278 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/ClassInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/ClassInfo.java @@ -36,6 +36,7 @@ import org.spongepowered.asm.logging.Level; import org.spongepowered.asm.logging.ILogger; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; @@ -50,6 +51,7 @@ import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.mixin.gen.Accessor; import org.spongepowered.asm.mixin.gen.Invoker; @@ -232,7 +234,11 @@ public static class FrameData { * Frame local size */ public final int size; - public final int rawSize; // Fabric non-adjusted frame size for legacy support + + /** + * Fabric: non-adjusted frame size for legacy support + */ + public final int rawSize; FrameData(int index, int type, int locals, int size) { this.index = index; @@ -2019,7 +2025,8 @@ public static ClassInfo forName(String className) { ClassInfo info = ClassInfo.cache.get(className); if (info == null) { try { - ClassNode classNode = MixinService.getService().getBytecodeProvider().getClassNode(className); + 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); diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/Config.java b/src/main/java/org/spongepowered/asm/mixin/transformer/Config.java index f0a1d177f..805cb302b 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/Config.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/Config.java @@ -31,6 +31,7 @@ import org.spongepowered.asm.launch.MixinInitialisationError; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.extensibility.IMixinConfig; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigSource; import org.spongepowered.asm.service.MixinService; import com.google.common.base.Strings; @@ -132,20 +133,34 @@ public int hashCode() { /** * Factory method, create a config from the specified config file and fail * over to the specified environment if no selector is present in the config - * + * * @param configFile config resource * @param outer failover environment * @return new config or null if invalid config version */ @Deprecated public static Config create(String configFile, MixinEnvironment outer) { + return Config.create(configFile, outer, null); + } + + /** + * Factory method, create a config from the specified config file and fail + * over to the specified environment if no selector is present in the config + * + * @param configFile config resource + * @param outer failover environment + * @param source config source + * @return new config or null if invalid config version + */ + @Deprecated + public static Config create(String configFile, MixinEnvironment outer, IMixinConfigSource source) { Config config = Config.allConfigs.get(configFile); if (config != null) { return config; } try { - config = MixinConfig.create(configFile, outer); + config = MixinConfig.create(configFile, outer, source); if (config != null) { Config.allConfigs.put(config.getName(), config); } @@ -161,7 +176,7 @@ public static Config create(String configFile, MixinEnvironment outer) { if (!Strings.isNullOrEmpty(parent)) { Config parentConfig; try { - parentConfig = Config.create(parent, outer); + parentConfig = Config.create(parent, outer, source); if (parentConfig != null) { if (!config.get().assignParent(parentConfig)) { config = null; @@ -178,14 +193,25 @@ public static Config create(String configFile, MixinEnvironment outer) { return config; } +// /** +// * Factory method, create a config from the specified config resource +// * +// * @param configFile config resource +// * @return new config or null if invalid config version +// */ +// public static Config create(String configFile) { +// return Config.create(configFile, (IMixinConfigSource)null); +// } + /** * Factory method, create a config from the specified config resource * * @param configFile config resource + * @param source config source * @return new config or null if invalid config version */ - public static Config create(String configFile) { - return MixinConfig.create(configFile, MixinEnvironment.getDefaultEnvironment()); + public static Config create(String configFile, IMixinConfigSource source) { + return MixinConfig.create(configFile, MixinEnvironment.getDefaultEnvironment(), source); } } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java b/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java index cc4031d11..85a68882d 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java @@ -30,6 +30,7 @@ import org.spongepowered.asm.mixin.transformer.ext.extensions.ExtensionCheckClass; import org.spongepowered.asm.mixin.transformer.ext.extensions.ExtensionCheckInterfaces; import org.spongepowered.asm.mixin.transformer.ext.extensions.ExtensionClassExporter; +import org.spongepowered.asm.mixin.transformer.ext.extensions.ExtensionLVTCleaner; import org.spongepowered.asm.service.ISyntheticClassInfo; import org.spongepowered.asm.util.IConsumer; @@ -54,6 +55,7 @@ public void accept(ISyntheticClassInfo item) { extensions.add(new InnerClassGenerator(registryDelegate, nestHostCoprocessor)); extensions.add(new ExtensionClassExporter(environment)); + extensions.add(new ExtensionLVTCleaner()); extensions.add(new ExtensionCheckClass()); extensions.add(new ExtensionCheckInterfaces()); } 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 179deb592..4a5a56b9d 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/InnerClassGenerator.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/InnerClassGenerator.java @@ -34,7 +34,6 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.commons.Remapper; import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.commons.ClassRemapper; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.mixin.transformer.ClassInfo.Field; import org.spongepowered.asm.mixin.transformer.ClassInfo.Method; @@ -221,18 +220,17 @@ public String toString() { * Just a basic remapping adapter, but we also decorate the transformed * class with a meta annotation describing the original class. */ - static class InnerClassAdapter extends ClassRemapper { + static class InnerClassAdapter extends ClassVisitor { private final InnerClassInfo info; InnerClassAdapter(ClassVisitor cv, InnerClassInfo info) { - super(ASM.API_VERSION, cv, info); + super(ASM.API_VERSION, cv); this.info = info; } /* (non-Javadoc) - * @see org.objectweb.asm.commons.ClassRemapper - * #visitNestHost(java.lang.String) + * @see org.objectweb.asm.ClassVisitor#visitNestHost(java.lang.String) */ @Override public void visitNestHost(String nestHost) { @@ -271,6 +269,21 @@ public void visitInnerClass(String name, String outerName, String innerName, int } + /** + * ClassRemapper from ASM 5.2 onward + */ + private static final String REMAPPER_CLASS = "org.objectweb.asm.commons.ClassRemapper"; + + /** + * RemappingClassAdapter prior to ASM 5.2 + */ + private static final String REMAPPER_CLASS_LEGACY = "org.objectweb.asm.commons.RemappingClassAdapter"; + + /** + * Resolved ClassRemapper class + */ + private static Class clRemapper; + /** * Logger */ @@ -384,7 +397,7 @@ public boolean generate(String name, ClassNode classNode) { private boolean generate(InnerClassInfo info, ClassNode classNode) { try { InnerClassGenerator.logger.debug("Generating mapped inner class {} (originally {})", info.getName(), info.getOriginalName()); - info.accept(new InnerClassAdapter(classNode, info)); + info.accept(new InnerClassAdapter(InnerClassGenerator.createRemappingAdapter(classNode, info), info)); return true; } catch (InvalidMixinException ex) { throw ex; @@ -411,4 +424,37 @@ private static String getUniqueReference(String originalName, ClassInfo targetCl return String.format("%s$%s$%s", targetClass, name, UUID.randomUUID().toString().replace("-", "")); } + /** + * Since we still technically support ASM5, and the remapping visitor class + * was refactored between ASM 5.0.3 and ASM 5.2, we can instatiate it using + * reflection in order to try both variants. Throws CNFE if the class can't + * be loaded for some reason + * + * @param cv Upstream ClassVisitor + * @param remapper Remapper to use + * @return New ClassRemapper or RemappingClassAdapter + * @throws ReflectiveOperationException if something goes wrong + */ + @SuppressWarnings("unchecked") + private static ClassVisitor createRemappingAdapter(ClassVisitor cv, Remapper remapper) throws ReflectiveOperationException { + if (InnerClassGenerator.clRemapper == null) { + try { + InnerClassGenerator.clRemapper = (Class)Class.forName(InnerClassGenerator.REMAPPER_CLASS); + } catch (ClassNotFoundException ex) { + // expected under ASM 5.0.3 since the new class doesn't exist yet + } + + if (InnerClassGenerator.clRemapper == null) { + try { + InnerClassGenerator.clRemapper = (Class)Class.forName(InnerClassGenerator.REMAPPER_CLASS_LEGACY); + } catch (ClassNotFoundException ex) { + // Not expected + throw new ClassNotFoundException(InnerClassGenerator.REMAPPER_CLASS + " or " + InnerClassGenerator.REMAPPER_CLASS_LEGACY); + } + } + } + + return InnerClassGenerator.clRemapper.getConstructor(ClassVisitor.class, Remapper.class).newInstance(cv, remapper); + } + } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MethodMapper.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MethodMapper.java index 369dcff8b..8cbb9d08b 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MethodMapper.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MethodMapper.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; +import com.google.common.primitives.Chars; import org.spongepowered.asm.logging.ILogger; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; @@ -39,10 +40,9 @@ import org.spongepowered.asm.mixin.transformer.MixinInfo.MixinMethodNode; import org.spongepowered.asm.service.MixinService; import org.spongepowered.asm.util.Counter; -import org.spongepowered.asm.util.asm.MethodNodeEx; import com.google.common.base.Strings; -import com.google.common.primitives.Chars; +import org.spongepowered.asm.util.asm.MethodNodeEx; /** * Maintains method remaps for a target class @@ -76,7 +76,15 @@ public MethodMapper(MixinEnvironment env, ClassInfo info) { public ClassInfo getClassInfo() { return this.info; } - + + /** + * Resets the counters to prepare for application, which can happen multiple times due to hotswap. + */ + public void reset() { + this.nextUniqueMethodIndex = 0; + this.nextUniqueFieldIndex = 0; + } + /** * Conforms an injector handler method * @@ -98,7 +106,7 @@ public void remapHandlerMethod(MixinInfo mixin, MethodNode handler, Method metho return; } - String handlerName = this.getHandlerName((MixinMethodNode)handler); + String handlerName = this.getHandlerName(mixin, (MixinMethodNode)handler); handler.name = method.conform(handlerName); } @@ -108,17 +116,17 @@ public void remapHandlerMethod(MixinInfo mixin, MethodNode handler, Method metho * @param method mixin method * @return conformed handler name */ - public String getHandlerName(MixinMethodNode method) { + public String getHandlerName(MixinInfo mixin, MixinMethodNode method) { String prefix = InjectionInfo.getInjectorPrefix(method.getInjectorAnnotation()); String classUID = MethodMapper.getClassUID(method.getOwner().getClassRef()); - String mod = FabricUtil.getModId(method.getOwner().getConfig(), ""); + String mod = MethodMapper.getMixinSourceId(mixin, ""); String methodName = method.name; if (!mod.isEmpty()) { - //It's common for mods to prefix their own handlers, let's account for that happening - if (methodName.startsWith(mod) && methodName.length() > mod.length() + 1 && Chars.contains(new char[] {'_', '$'}, methodName.charAt(mod.length()))) { - methodName = methodName.substring(mod.length() + 1); - } - mod += '$'; + //It's common for mods to prefix their own handlers, let's account for that happening + if (methodName.startsWith(mod) && methodName.length() > mod.length() + 1 && Chars.contains(new char[] {'_', '$'}, methodName.charAt(mod.length()))) { + methodName = methodName.substring(mod.length() + 1); + } + mod += '$'; } String methodUID = MethodMapper.getMethodUID(methodName, method.desc, !method.isSurrogate()); return String.format("%s$%s%s$%s%s", prefix, classUID, methodUID, mod, methodName); @@ -133,22 +141,22 @@ public String getHandlerName(MixinMethodNode method) { * method name prefix * @return Unique method name */ - public String getUniqueName(MethodNode method, String sessionId, boolean preservePrefix) { + public String getUniqueName(MixinInfo mixin, MethodNode method, String sessionId, boolean preservePrefix) { String uniqueIndex = Integer.toHexString(this.nextUniqueMethodIndex++); String methodName = method.name; if (method instanceof MethodNodeEx) { - String mod = FabricUtil.getModId(((MethodNodeEx) method).getOwner().getConfig(), ""); - if (!mod.isEmpty()) { - //It's rarer for mods to prefix their @Unique methods, but let's account for it anyway - if (methodName.startsWith(mod) && methodName.length() > mod.length() + 1 && Chars.contains(new char[] {'_', '$'}, methodName.charAt(mod.length()))) { - methodName = methodName.substring(mod.length() + 1); - } - if (preservePrefix) { - methodName += '$' + mod; - } else { - methodName = mod + '$' + methodName; - } - } + String mod = MethodMapper.getMixinSourceId(mixin, ""); + if (!mod.isEmpty()) { + //It's rarer for mods to prefix their @Unique methods, but let's account for it anyway + if (methodName.startsWith(mod) && methodName.length() > mod.length() + 1 && Chars.contains(new char[] {'_', '$'}, methodName.charAt(mod.length()))) { + methodName = methodName.substring(mod.length() + 1); + } + if (preservePrefix) { + methodName += '$' + mod; + } else { + methodName = mod + '$' + methodName; + } + } } String pattern = preservePrefix ? "%2$s_$md$%1$s$%3$s" : "md%s$%s$%s"; return String.format(pattern, sessionId.substring(30), methodName, uniqueIndex); @@ -161,9 +169,30 @@ public String getUniqueName(MethodNode method, String sessionId, boolean preserv * @param sessionId Session ID, for uniqueness * @return Unique field name */ - public String getUniqueName(FieldNode field, String sessionId) { + public String getUniqueName(MixinInfo mixin, FieldNode field, String sessionId) { String uniqueIndex = Integer.toHexString(this.nextUniqueFieldIndex++); - return String.format("fd%s$%s$%s", sessionId.substring(30), field.name, uniqueIndex); + return String.format("fd%s$%s%s$%s", sessionId.substring(30), MethodMapper.getMixinSourceId(mixin, "$"), field.name, uniqueIndex); + } + + /** + * Get clean sourceId from mixin + * + * @param mixin mixin info + * @return clean source id with dollar suffix or empty string + */ + private static String getMixinSourceId(MixinInfo mixin, String separator) { + String sourceId = mixin.getConfig().getCleanSourceId(); + if (sourceId == null) { + String modId = FabricUtil.getModId(mixin.getConfig(), null); + if (modId == null) { + return ""; + } + return modId + separator; + } + if (sourceId.length() > 12) { + sourceId = sourceId.substring(0, 12); + } + return String.format("%s%s", sourceId, separator); } /** diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorInterface.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorInterface.java index 4a387331e..99aec8db3 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorInterface.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorInterface.java @@ -30,6 +30,7 @@ import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.MixinEnvironment.Feature; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; import org.spongepowered.asm.mixin.transformer.ClassInfo.Field; @@ -123,20 +124,46 @@ protected void applyInitialisers(MixinTargetContext mixin) { */ @Override protected void prepareInjections(MixinTargetContext mixin) { - try { - super.prepareInjections(mixin); - } catch (InvalidInjectionException ex) { - String description = ex.getContext() != null ? ex.getContext().toString() : "Injection"; - throw new InvalidInterfaceMixinException(mixin, description + " is not supported in interface mixin", ex); + if (Feature.INJECTORS_IN_INTERFACE_MIXINS.isEnabled()) { + try { + super.prepareInjections(mixin); + } catch (InvalidInjectionException ex) { + String description = ex.getContext() != null ? ex.getContext().toString() : "Injection"; + throw new InvalidInterfaceMixinException(mixin, description + " is not supported in interface mixin", ex); + } + return; } InjectionInfo injectInfo = mixin.getFirstInjectionInfo(); if (injectInfo != null) { - //Make sure we're running on a Java version which supports interfaces having method bodies - if (!MixinEnvironment.getCompatibilityLevel().supports(LanguageFeatures.METHODS_IN_INTERFACES)) { - throw new InvalidInterfaceMixinException(mixin, injectInfo + " is not supported on interface mixin method " + injectInfo.getMethodName()); - } + throw new InvalidInterfaceMixinException(mixin, injectInfo + " is not supported on interface mixin method " + injectInfo.getMethodName()); + } + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.MixinApplicator + * #applyPreInjections( + * org.spongepowered.asm.mixin.transformer.MixinTargetContext) + */ + @Override + protected void applyPreInjections(MixinTargetContext mixin) { + if (Feature.INJECTORS_IN_INTERFACE_MIXINS.isEnabled()) { + super.applyPreInjections(mixin); + return; + } + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.MixinApplicator + * #applyInjections( + * org.spongepowered.asm.mixin.transformer.MixinTargetContext, int) + */ + @Override + protected void applyInjections(MixinTargetContext mixin, int injectorOrder) { + if (Feature.INJECTORS_IN_INTERFACE_MIXINS.isEnabled()) { + super.applyInjections(mixin, injectorOrder); + return; } } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorStandard.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorStandard.java index 27cef3057..2e61a4c43 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorStandard.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinApplicatorStandard.java @@ -49,11 +49,13 @@ import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.struct.Constructor; import org.spongepowered.asm.mixin.throwables.MixinError; import org.spongepowered.asm.mixin.transformer.ClassInfo.Field; import org.spongepowered.asm.mixin.transformer.ext.extensions.ExtensionClassExporter; import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; import org.spongepowered.asm.mixin.transformer.meta.MixinRenamed; +import org.spongepowered.asm.mixin.transformer.struct.Initialiser; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.mixin.transformer.throwables.MixinApplicatorException; import org.spongepowered.asm.service.IMixinAuditTrail; @@ -70,6 +72,7 @@ import org.spongepowered.asm.util.throwables.InvalidConstraintException; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; /** * Applies mixins to a target class @@ -102,123 +105,29 @@ enum ApplicatorPass { /** * Enumerate injectors and scan for injection points */ - PREINJECT, + INJECT_PREPARE, /** - * Apply injectors from previous pass - */ - INJECT - - } - - /** - * Strategy for injecting initialiser insns - */ - enum InitialiserInjectionMode { - - /** - * Default mode, attempts to place initialisers after all other - * competing initialisers in the target ctor - */ - DEFAULT, - - /** - * Safe mode, only injects initialiser directly after the super-ctor - * invocation - */ - SAFE - - } - - /** - * Internal struct for representing a range - */ - class Range { - - /** - * Start of the range + * Apply accessors and invokers */ - final int start; + ACCESSOR, /** - * End of the range - */ - final int end; - - /** - * Range marker - */ - final int marker; - - /** - * Create a range with the specified values. - * - * @param start Start of the range - * @param end End of the range - * @param marker Arbitrary marker value + * Apply preinjection steps on injectors from previous pass */ - Range(int start, int end, int marker) { - this.start = start; - this.end = end; - this.marker = marker; - } + INJECT_PREINJECT, /** - * Range is valid if both start and end are nonzero and end is after or - * at start - * - * @return true if valid - */ - boolean isValid() { - return (this.start != 0 && this.end != 0 && this.end >= this.start); - } - - /** - * Returns true if the supplied value is between or equal to start and - * end - * - * @param value true if the range contains value - */ - boolean contains(int value) { - return value >= this.start && value <= this.end; - } - - /** - * Returns true if the supplied value is outside the range - * - * @param value true if the range does not contain value - */ - boolean excludes(int value) { - return value < this.start || value > this.end; - } - - /* (non-Javadoc) - * @see java.lang.Object#toString() + * Apply injectors from previous pass */ - @Override - public String toString() { - return String.format("Range[%d-%d,%d,valid=%s)", this.start, this.end, this.marker, this.isValid()); - } + INJECT_APPLY } /** - * List of opcodes which must not appear in a class initialiser, mainly a - * sanity check so that if any of the specified opcodes are found, we can - * log it as an error condition and then people can bitch at me to fix it. - * Essentially if it turns out that field initialisers can somehow make use - * of local variables, then I need to write some code to ensure that said - * locals are shifted so that they don't interfere with locals in the - * receiving constructor. + * Order collection to use for all passes except ApplicatorPass.INJECT */ - protected static final int[] INITIALISER_OPCODE_BLACKLIST = { - Opcodes.RETURN, Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, - Opcodes.ISTORE, Opcodes.LSTORE, Opcodes.FSTORE, Opcodes.DSTORE, Opcodes.ASTORE, - // Fabric: Array opcodes cause no problems in initialisers and should not be needlessly restricted. - // Opcodes.IALOAD, Opcodes.LALOAD, Opcodes.FALOAD, Opcodes.DALOAD, Opcodes.AALOAD, - // Opcodes.BALOAD, Opcodes.CALOAD, Opcodes.SALOAD, Opcodes.IASTORE, Opcodes.LASTORE, - // Opcodes.FASTORE, Opcodes.DASTORE, Opcodes.AASTORE, Opcodes.BASTORE, Opcodes.CASTORE, Opcodes.SASTORE - }; + protected static final Set ORDERS_NONE = ImmutableSet.of(Integer.valueOf(0)); /** * Log more things @@ -320,17 +229,28 @@ final void apply(SortedSet mixins) { activity.next("%s Applicator Phase", pass); Section timer = this.profiler.begin("pass", pass.name().toLowerCase(Locale.ROOT)); IActivity applyActivity = this.activities.begin("Mixin"); - for (Iterator iter = mixinContexts.iterator(); iter.hasNext();) { - current = iter.next(); - applyActivity.next(current.toString()); - try { - this.applyMixin(current, pass); - } catch (InvalidMixinException ex) { - if (current.isRequired()) { - throw ex; + + Set orders = MixinApplicatorStandard.ORDERS_NONE; + if (pass == ApplicatorPass.INJECT_APPLY) { + orders = new TreeSet(); + for (MixinTargetContext context : mixinContexts) { + context.getInjectorOrders(orders); + } + } + + for (Integer injectorOrder : orders) { + for (Iterator iter = mixinContexts.iterator(); iter.hasNext();) { + current = iter.next(); + applyActivity.next(current.toString()); + try { + this.applyMixin(current, pass, injectorOrder.intValue()); + } catch (InvalidMixinException ex) { + if (current.isRequired()) { + throw ex; + } + this.context.addSuppressed(ex); + iter.remove(); // Do not process this mixin further } - this.context.addSuppressed(ex); - iter.remove(); // Do not process this mixin further } } applyActivity.end(); @@ -370,7 +290,7 @@ final void apply(SortedSet mixins) { * * @param mixin Mixin to apply */ - protected final void applyMixin(MixinTargetContext mixin, ApplicatorPass pass) { + protected final void applyMixin(MixinTargetContext mixin, ApplicatorPass pass, int injectorOrder) { IActivity activity = this.activities.begin("Apply"); switch (pass) { case MAIN: @@ -390,16 +310,24 @@ protected final void applyMixin(MixinTargetContext mixin, ApplicatorPass pass) { this.applyInitialisers(mixin); break; - case PREINJECT: + case INJECT_PREPARE: activity.next("Prepare Injections"); this.prepareInjections(mixin); break; - case INJECT: + case ACCESSOR: activity.next("Apply Accessors"); this.applyAccessors(mixin); + break; + + case INJECT_PREINJECT: activity.next("Apply Injections"); - this.applyInjections(mixin); + this.applyPreInjections(mixin); + break; + + case INJECT_APPLY: + activity.next("Apply Injections"); + this.applyInjections(mixin, injectorOrder); break; default: @@ -490,7 +418,7 @@ protected void mergeNewFields(MixinTargetContext mixin) { // This is just a local field, so add it this.targetClass.fields.add(field); mixin.fieldMerged(field); - + if (field.signature != null) { if (this.mergeSignatures) { SignatureVisitor sv = mixin.getSignature().getRemapper(); @@ -776,272 +704,22 @@ protected final void appendInsns(MixinTargetContext mixin, MethodNode method) { * @param mixin mixin target context */ protected void applyInitialisers(MixinTargetContext mixin) { - // Try to find a suitable constructor, we need a constructor with line numbers in order to extract the initialiser - MethodNode ctor = this.getConstructor(mixin); - if (ctor == null) { - return; - } - - // Find the initialiser instructions in the candidate ctor - Deque initialiser = this.getInitialiser(mixin, ctor); + // Find the initialiser in the candidate ctor + Initialiser initialiser = mixin.getInitialiser(); if (initialiser == null || initialiser.size() == 0) { return; } - - String superName = this.context.getClassInfo().getSuperName(); - - // Patch the initialiser into the target class ctors - for (MethodNode method : this.targetClass.methods) { - if (Constants.CTOR.equals(method.name)) { - DelegateInitialiser superCall = Bytecode.findDelegateInit(method, superName, this.targetClass.name); - if (!superCall.isPresent || superCall.isSuper) { - method.maxStack = Math.max(method.maxStack, ctor.maxStack); - this.injectInitialiser(mixin, method, initialiser); - } - } - } - } - - /** - * Finds a suitable ctor for reading the instance initialiser bytecode - * - * @param mixin mixin to search - * @return appropriate ctor or null if none found - */ - protected MethodNode getConstructor(MixinTargetContext mixin) { - MethodNode ctor = null; - - for (MethodNode mixinMethod : mixin.getMethods()) { - if (Constants.CTOR.equals(mixinMethod.name) && Bytecode.methodHasLineNumbers(mixinMethod)) { - if (ctor == null) { - ctor = mixinMethod; - } else { - // Not an error condition, just weird - this.logger.warn("Mixin {} has multiple constructors, {} was selected\n", mixin, ctor.desc); - } - } - } - - return ctor; - } - /** - * Identifies line numbers in the supplied ctor which correspond to the - * start and end of the method body. - * - * @param ctor constructor to scan - * @return range indicating the line numbers of the specified constructor - * and the position of the superclass ctor invocation - */ - private Range getConstructorRange(MethodNode ctor) { - boolean lineNumberIsValid = false; - AbstractInsnNode endReturn = null; - - int line = 0, start = 0, end = 0, superIndex = -1; - for (Iterator iter = ctor.instructions.iterator(); iter.hasNext();) { - AbstractInsnNode insn = iter.next(); - if (insn instanceof LineNumberNode) { - line = ((LineNumberNode)insn).line; - lineNumberIsValid = true; - } else if (insn instanceof MethodInsnNode) { - if (insn.getOpcode() == Opcodes.INVOKESPECIAL && Constants.CTOR.equals(((MethodInsnNode)insn).name) && superIndex == -1) { - superIndex = ctor.instructions.indexOf(insn); - start = line; - } - } else if (insn.getOpcode() == Opcodes.PUTFIELD) { - lineNumberIsValid = false; - } else if (insn.getOpcode() == Opcodes.RETURN) { - if (lineNumberIsValid) { - end = line; - } else { - end = start; - endReturn = insn; - } - } - } - - if (endReturn != null) { - LabelNode label = new LabelNode(new Label()); - ctor.instructions.insertBefore(endReturn, label); - ctor.instructions.insertBefore(endReturn, new LineNumberNode(start, label)); - } - - return new Range(start, end, superIndex); - } - - /** - * Get insns corresponding to the instance initialiser (hopefully) from the - * supplied constructor. - * - * @param mixin mixin target context - * @param ctor constructor to inspect - * @return initialiser bytecode extracted from the supplied constructor, or - * null if the constructor range could not be parsed - */ - protected final Deque getInitialiser(MixinTargetContext mixin, MethodNode ctor) { - // - // TODO Potentially rewrite this to be less horrible. - // - - // Find the range of line numbers which corresponds to the constructor body - Range init = this.getConstructorRange(ctor); - if (!init.isValid()) { - return null; - } - - // Now we know where the constructor is, look for insns which lie OUTSIDE the method body - int line = 0; - Deque initialiser = new ArrayDeque(); - boolean gatherNodes = false; - int trimAtOpcode = -1; - LabelNode optionalInsn = null; - for (Iterator iter = ctor.instructions.iterator(init.marker); iter.hasNext();) { - AbstractInsnNode insn = iter.next(); - if (insn instanceof LineNumberNode) { - line = ((LineNumberNode)insn).line; - AbstractInsnNode next = ctor.instructions.get(ctor.instructions.indexOf(insn) + 1); - if (line == init.end && next.getOpcode() != Opcodes.RETURN) { - gatherNodes = true; - trimAtOpcode = Opcodes.RETURN; - } else { - gatherNodes = init.excludes(line); - trimAtOpcode = -1; - } - } else if (gatherNodes) { - if (optionalInsn != null) { - initialiser.add(optionalInsn); - optionalInsn = null; - } - - if (insn instanceof LabelNode) { - optionalInsn = (LabelNode)insn; - } else { - int opcode = insn.getOpcode(); - if (opcode == trimAtOpcode) { - trimAtOpcode = -1; - continue; - } - for (int ivalidOp : MixinApplicatorStandard.INITIALISER_OPCODE_BLACKLIST) { - if (opcode == ivalidOp) { - // At the moment I don't handle any transient locals because I haven't seen any in the wild, but let's avoid writing - // code which will likely break things and fix it if a real test case ever appears - throw new InvalidMixinException(mixin, "Cannot handle " + Bytecode.getOpcodeName(opcode) + " opcode (0x" - + Integer.toHexString(opcode).toUpperCase(Locale.ROOT) + ") in class initialiser"); - } - } - - initialiser.add(insn); + // Patch the initialiser into the target class ctors + for (Constructor ctor : this.context.getConstructors()) { + if (ctor.isInjectable()) { + int extraStack = initialiser.getMaxStack() - ctor.getMaxStack(); + if (extraStack > 0) { + ctor.extendStack().add(extraStack); } + initialiser.injectInto(ctor); } } - - // Check that the last insn is a PUTFIELD, if it's not then - AbstractInsnNode last = initialiser.peekLast(); - if (last != null) { - if (last.getOpcode() != Opcodes.PUTFIELD) { - throw new InvalidMixinException(mixin, "Could not parse initialiser, expected 0xB5, found 0x" - + Integer.toHexString(last.getOpcode()) + " in " + mixin); - } - } - - return initialiser; - } - - /** - * Inject initialiser code into the target constructor - * - * @param mixin mixin target context - * @param ctor target constructor - * @param initialiser initialiser instructions - */ - protected final void injectInitialiser(MixinTargetContext mixin, MethodNode ctor, Deque initialiser) { - Map labels = Bytecode.cloneLabels(ctor.instructions); - // Fabric: also clone labels from the initialiser as they will be merged. - for (AbstractInsnNode node : initialiser) { - if (node instanceof LabelNode) { - labels.put((LabelNode) node, new LabelNode()); - } - } - - AbstractInsnNode insn = this.findInitialiserInjectionPoint(mixin, ctor, initialiser); - if (insn == null) { - this.logger.warn("Failed to locate initialiser injection point in {}, initialiser was not mixed in.", ctor.desc); - return; - } - - for (AbstractInsnNode node : initialiser) { - if (node instanceof LabelNode) { - // Fabric: Merge cloned labels instead of skipping them. - // continue; - } - if (node instanceof JumpInsnNode) { - // Fabric: Jumps cause no issues if labels are cloned properly and should not be needlessly restricted. - // throw new InvalidMixinException(mixin, "Unsupported JUMP opcode in initialiser in " + mixin); - } - AbstractInsnNode imACloneNow = node.clone(labels); - ctor.instructions.insert(insn, imACloneNow); - insn = imACloneNow; - } - } - - /** - * Find the injection point for injected initialiser insns in the target - * ctor - * - * @param mixin target context for mixin being applied - * @param ctor target ctor - * @param initialiser source initialiser insns - * @return target node - */ - protected AbstractInsnNode findInitialiserInjectionPoint(MixinTargetContext mixin, MethodNode ctor, Deque initialiser) { - Set initialisedFields = new HashSet(); - for (AbstractInsnNode initialiserInsn : initialiser) { - if (initialiserInsn.getOpcode() == Opcodes.PUTFIELD) { - initialisedFields.add(MixinApplicatorStandard.fieldKey((FieldInsnNode)initialiserInsn)); - } - } - - InitialiserInjectionMode mode = this.getInitialiserInjectionMode(mixin.getEnvironment()); - String targetName = this.targetClassInfo.getName(); - String targetSuperName = this.targetClassInfo.getSuperName(); - AbstractInsnNode targetInsn = null; - - for (Iterator iter = ctor.instructions.iterator(); iter.hasNext();) { - AbstractInsnNode insn = iter.next(); - if (insn.getOpcode() == Opcodes.INVOKESPECIAL && Constants.CTOR.equals(((MethodInsnNode)insn).name)) { - String owner = ((MethodInsnNode)insn).owner; - if (owner.equals(targetName) || owner.equals(targetSuperName)) { - targetInsn = insn; - if (mode == InitialiserInjectionMode.SAFE) { - break; - } - } - } else if (insn.getOpcode() == Opcodes.PUTFIELD && mode == InitialiserInjectionMode.DEFAULT) { - String key = MixinApplicatorStandard.fieldKey((FieldInsnNode)insn); - if (initialisedFields.contains(key)) { - targetInsn = insn; - } - } - } - - return targetInsn; - } - - private InitialiserInjectionMode getInitialiserInjectionMode(MixinEnvironment environment) { - String strMode = environment.getOptionValue(Option.INITIALISER_INJECTION_MODE); - if (strMode == null) { - return InitialiserInjectionMode.DEFAULT; - } - try { - return InitialiserInjectionMode.valueOf(strMode.toUpperCase(Locale.ROOT)); - } catch (Exception ex) { - this.logger.warn("Could not parse unexpected value \"{}\" for mixin.initialiserInjectionMode, reverting to DEFAULT", strMode); - return InitialiserInjectionMode.DEFAULT; - } - } - - private static String fieldKey(FieldInsnNode fieldNode) { - return String.format("%s:%s", fieldNode.desc, fieldNode.name); } /** @@ -1053,13 +731,25 @@ protected void prepareInjections(MixinTargetContext mixin) { mixin.prepareInjections(); } + /** + * Run preinject application on all injectors discovered in the previous + * pass + * + * @param mixin Mixin being applied + * @param injectorOrder injector order for this pass + */ + protected void applyPreInjections(MixinTargetContext mixin) { + mixin.applyPreInjections(); + } + /** * Apply all injectors discovered in the previous pass * * @param mixin Mixin being applied + * @param injectorOrder injector order for this pass */ - protected void applyInjections(MixinTargetContext mixin) { - mixin.applyInjections(); + protected void applyInjections(MixinTargetContext mixin, int injectorOrder) { + mixin.applyInjections(injectorOrder); } /** @@ -1138,7 +828,7 @@ protected final void checkConstraints(MixinTargetContext mixin, MethodNode metho /** * Finds a method in the target class - * @param searchFor + * @param searchFor The method node to search for * * @return Target method matching searchFor, or null if not found */ @@ -1154,7 +844,7 @@ protected final MethodNode findTargetMethod(MethodNode searchFor) { /** * Finds a field in the target class - * @param searchFor + * @param searchFor The field node to search for * * @return Target field matching searchFor, or null if not found */ 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 351409f61..d3a550f5e 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinConfig.java @@ -31,6 +31,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.google.common.base.Joiner; import org.spongepowered.asm.logging.Level; import org.spongepowered.asm.logging.ILogger; import org.spongepowered.asm.launch.MixinInitialisationError; @@ -38,11 +39,13 @@ import org.objectweb.asm.tree.InsnList; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; +import org.spongepowered.asm.mixin.MixinEnvironment.Feature; import org.spongepowered.asm.mixin.MixinEnvironment.Option; import org.spongepowered.asm.mixin.MixinEnvironment.Phase; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.extensibility.IMixinConfig; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigSource; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.InjectionPoint; @@ -56,7 +59,6 @@ import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.service.IMixinService; import org.spongepowered.asm.service.MixinService; -import org.spongepowered.asm.util.CompareUtil; import org.spongepowered.asm.util.VersionNumber; import com.google.common.base.Strings; @@ -75,21 +77,46 @@ final class MixinConfig implements Comparable, IMixinConfig { */ static class InjectorOptions { + /** + * Specifies the default value for require to be used when no + * explicit value is defined on the injector. Setting this value to 1 + * essentially makes all injectors in the config automatically required. + * Individual injectors can still be marked optional by explicitly + * setting their require value to 0. + */ @SerializedName("defaultRequire") int defaultRequireValue = 0; + /** + * Specifies the name for injector groups which have no explicit group + * name defined. It is recommended to set this value when grouping + * injectors to support global injector groupings in the future. + */ @SerializedName("defaultGroup") String defaultGroup = "default"; + /** + * The namespace for custom injection points and dynamic selectors + */ @SerializedName("namespace") String namespace; + /** + * List of fully-qualified custom injection point classes to register + */ @SerializedName("injectionPoints") List injectionPoints; + /** + * List of fully-qualified dynamic selector classes to register + */ @SerializedName("dynamicSelectors") List dynamicSelectors; + /** + * Allows the max Shift.By value to adjusted from the environment + * default, max value is 5 + */ @SerializedName("maxShiftBy") int maxShiftBy = InjectionPoint.DEFAULT_ALLOWED_SHIFT_BY; @@ -112,9 +139,18 @@ void mergeFrom(InjectorOptions parent) { */ static class OverwriteOptions { + /** + * Flag which specifies whether an overwrite with lower visibility than + * its target is allowed to be applied, the visibility will be upgraded + * if the target method is nonprivate but the merged method is private. + */ @SerializedName("conformVisibility") boolean conformAccessModifiers; + /** + * Changes the default always-overwrite behaviour of mixins to + * explicitly require {@link Overwrite} annotations on overwrite methods + */ @SerializedName("requireAnnotations") boolean requireOverwriteAnnotations; @@ -212,6 +248,13 @@ interface IListener { @SerializedName("minVersion") private String version; + /** + * List of required {@link Feature} flags, can be used with or in place + * of {@link #minVersion} to provide sanity checking when a config is loaded + */ + @SerializedName("requiredFeatures") + private List requiredFeatures; + /** * Minimum compatibility level required for mixins in this set */ @@ -330,6 +373,11 @@ interface IListener { */ private transient String name; + /** + * The source of this mixin config + */ + private transient IMixinConfigSource source; + /** * Name of the {@link IMixinConfigPlugin} to hook onto this MixinConfig */ @@ -404,10 +452,11 @@ private MixinConfig() {} * returned, or false if initialisation failed and the config should * be discarded */ - private boolean onLoad(IMixinService service, String name, MixinEnvironment fallbackEnvironment) { + private boolean onLoad(IMixinService service, String name, MixinEnvironment fallbackEnvironment, IMixinConfigSource source) { this.service = service; this.name = name; - + this.source = source; + // If parent is specified, don't perform postinit until parent is assigned if (!Strings.isNullOrEmpty(this.parentName)) { return true; @@ -501,7 +550,7 @@ private boolean postInit() throws MixinInitialisationError { this.initialised = true; this.initCompatibilityLevel(); this.initExtensions(); - return this.checkVersion(); + return this.checkVersion() && this.checkFeatures(); } @SuppressWarnings("deprecation") @@ -673,7 +722,10 @@ private boolean checkVersion() throws MixinInitialisationError { if (this.parent != null && this.parent.version != null) { return true; } - this.logger.debug("Mixin config {} does not specify \"minVersion\" property", this.name); + // requiredFeatures can be used instead of minVersion going forward + if (this.requiredFeatures == null || this.requiredFeatures.isEmpty()) { + this.logger.debug("Mixin config {} does not specify \"minVersion\" or \"requiredFeatures\" property", this.name); + } } VersionNumber minVersion = VersionNumber.parse(this.version); @@ -692,6 +744,35 @@ private boolean checkVersion() throws MixinInitialisationError { return true; } + private boolean checkFeatures() throws MixinInitialisationError { + if (this.requiredFeatures == null || this.requiredFeatures.isEmpty()) { + return true; + } + + Set missingFeatures = new LinkedHashSet(); + for (String featureId : this.requiredFeatures) { + featureId = featureId.trim().toUpperCase(Locale.ROOT); + if (!Feature.isActive(featureId)) { + missingFeatures.add(featureId); + } + } + + if (missingFeatures.isEmpty()) { + return true; + } + + String strMissingFeatures = Joiner.on(", ").join(missingFeatures); + this.logger.warn("Mixin config {} requires features [{}] which are not available. The mixin config will not be applied.", + this.name, strMissingFeatures); + + if (this.required) { + throw new MixinInitialisationError("Required mixin config " + this.name + " requires features [" + strMissingFeatures + + " which are not available"); + } + + return false; + } + /** * Add a new listener * @@ -937,6 +1018,30 @@ public String getName() { return this.name; } + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.extensibility.IMixinConfig#getSource() + */ + @Override + public IMixinConfigSource getSource() { + return this.source; + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.extensibility.IMixinConfig + * #getCleanSourceId() + */ + @Override + public String getCleanSourceId() { + if (this.source == null) { + return null; + } + String sourceId = this.source.getId(); + if (sourceId == null) { + return null; + } + return sourceId.replaceAll("[^A-Za-z]", ""); + } + /** * Get the package containing all mixin classes */ @@ -982,7 +1087,7 @@ public String getDefaultInjectorGroup() { } /** - * Get whether visibility levelfor overwritten methods should be conformed + * Get whether visibility level for overwritten methods should be conformed * to the target class * * @return true if conform is enabled @@ -1268,7 +1373,7 @@ public int compareTo(MixinConfig other) { return 0; } if (other.priority == this.priority) { - return CompareUtil.compare(this.order, other.order); + return Integer.compare(this.order, other.order); } else { return (this.priority < other.priority) ? -1 : 1; } @@ -1282,7 +1387,7 @@ public int compareTo(MixinConfig other) { * @param outer fallback environment * @return new Config */ - static Config create(String configFile, MixinEnvironment outer) { + static Config create(String configFile, MixinEnvironment outer, IMixinConfigSource source) { try { IMixinService service = MixinService.getService(); InputStream resource = service.getResourceAsStream(configFile); @@ -1290,7 +1395,7 @@ static Config create(String configFile, MixinEnvironment outer) { 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)) { + if (config.onLoad(service, configFile, outer, source)) { return config.getHandle(); } return null; 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 989264644..6f3116190 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessor.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessor.java @@ -156,8 +156,8 @@ ProcessResult process(String className, ClassNode classNode) { /** * Perform postprocessing actions on the supplied class. This is called for * all classes. For passthrough classes and classes which are not mixin - * targets this is called immediately after {@link process} is completed for - * all coprocessors. For mixin targets this is called after mixins are + * targets this is called immediately after {@link #process} is completed + * for all coprocessors. For mixin targets this is called after mixins are * applied. * * @param className Name of the target class 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 967d0380c..423c16394 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorAccessor.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinCoprocessorAccessor.java @@ -132,7 +132,7 @@ private Method getAccessorMethod(MixinInfo mixin, MethodNode methodNode, ClassIn // Normally the target will be renamed when the mixin is conformed to the target, if we get here // without this happening then we will end up invoking an undecorated method, which is bad! if (!method.isConformed()) { - String uniqueName = targetClass.getMethodMapper().getUniqueName(methodNode, this.sessionId, true); + String uniqueName = targetClass.getMethodMapper().getUniqueName(mixin, methodNode, this.sessionId, true); method.conform(uniqueName); } @@ -145,7 +145,8 @@ private static void createProxy(MethodNode methodNode, ClassInfo targetClass, Me Type[] args = Type.getArgumentTypes(methodNode.desc); Type returnType = Type.getReturnType(methodNode.desc); Bytecode.loadArgs(args, methodNode.instructions, 0); - methodNode.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, targetClass.getName(), method.getName(), methodNode.desc, targetClass.isInterface())); + methodNode.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, targetClass.getName(), method.getName(), methodNode.desc, + targetClass.isInterface())); methodNode.instructions.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); methodNode.maxStack = Bytecode.getFirstNonArgLocalIndex(args, false); methodNode.maxLocals = 0; 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 24eafa86d..980bcd26a 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java @@ -73,7 +73,6 @@ import org.spongepowered.asm.util.LanguageFeatures; import org.spongepowered.asm.util.asm.ASM; import org.spongepowered.asm.util.asm.MethodNodeEx; -import org.spongepowered.asm.util.CompareUtil; import org.spongepowered.asm.util.perf.Profiler; import org.spongepowered.asm.util.perf.Profiler.Section; @@ -579,7 +578,7 @@ void validate(State state, List targetClasses) { if (!targetClass.hasSuperClass(classNode.superName, ClassInfo.Traversal.SUPER)) { ClassInfo superClass = ClassInfo.forName(classNode.superName); - if (superClass.isMixin()) { + if (superClass != null && superClass.isMixin()) { // If superclass is a mixin, check for hierarchy derp for (ClassInfo superTarget : superClass.getTargets()) { if (targetClasses.contains(superTarget)) { @@ -1309,7 +1308,8 @@ private ClassNode loadMixinClass(String mixinClassName) throws ClassNotFoundExce this.logger.error("Classloader restrictions [{}] encountered loading {}, name: {}", restrictions, this, mixinClassName); } } - classNode = this.service.getBytecodeProvider().getClassNode(mixinClassName, true); + int readerFlags = this.parent.getEnvironment().getOption(Option.CLASSREADER_EXPAND_FRAMES) ? ClassReader.EXPAND_FRAMES : 0; + classNode = this.service.getBytecodeProvider().getClassNode(mixinClassName, true, readerFlags); } catch (ClassNotFoundException ex) { throw new ClassNotFoundException(String.format("The specified mixin '%s' was not found", mixinClassName)); } catch (IOException ex) { @@ -1342,7 +1342,7 @@ public int compareTo(MixinInfo other) { return 0; } if (other.priority == this.priority) { - return CompareUtil.compare(this.order, other.order); + return Integer.compare(this.order, other.order); } else { return (this.priority < other.priority) ? -1 : 1; } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInheritanceTracker.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInheritanceTracker.java index e76e20125..e6e26ca66 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInheritanceTracker.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInheritanceTracker.java @@ -38,62 +38,70 @@ import org.spongepowered.asm.util.Bytecode; public enum MixinInheritanceTracker implements IListener { - INSTANCE; - - @Override - public void onPrepare(MixinInfo mixin) { - } - - @Override - public void onInit(MixinInfo mixin) { - ClassInfo mixinInfo = mixin.getClassInfo(); - assert mixinInfo.isMixin(); //The mixin should certainly be a mixin - - for (ClassInfo superType = mixinInfo.getSuperClass(); superType != null && superType.isMixin(); superType = superType.getSuperClass()) { - List children = parentMixins.get(superType.getName()); - - if (children == null) { - parentMixins.put(superType.getName(), children = new ArrayList()); - } - - children.add(mixin); - } - } - - public List findOverrides(ClassInfo owner, String name, String desc) { - return findOverrides(owner.getName(), name, desc); - } - - public List findOverrides(String owner, String name, String desc) { - List children = parentMixins.get(owner); - if (children == null) return Collections.emptyList(); - - List out = new ArrayList(children.size()); - - for (MixinInfo child : children) { - ClassNode node = child.getClassNode(ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - - MethodNode method = Bytecode.findMethod(node, name, desc); - if (method == null || Bytecode.isStatic(method)) continue; - - switch (Bytecode.getVisibility(method)) { - case PRIVATE: - break; - - case PACKAGE: - int ownerSplit = owner.lastIndexOf('/'); - int childSplit = node.name.lastIndexOf('/'); - //There is a reasonable chance mixins are in the same package, so it is viable that a package private method is overridden - if (ownerSplit != childSplit || (ownerSplit > 0 && !owner.regionMatches(0, node.name, 0, ownerSplit + 1))) break; - - default: - out.add(method); - break; - } - } - - return out.isEmpty() ? Collections.emptyList() : out; - } - - private final Map> parentMixins = new HashMap>(); + INSTANCE; + + @Override + public void onPrepare(MixinInfo mixin) { + } + + @Override + public void onInit(MixinInfo mixin) { + ClassInfo mixinInfo = mixin.getClassInfo(); + assert mixinInfo.isMixin(); //The mixin should certainly be a mixin + + for (ClassInfo superType = mixinInfo.getSuperClass(); superType != null && superType.isMixin(); superType = superType.getSuperClass()) { + List children = parentMixins.get(superType.getName()); + + if (children == null) { + parentMixins.put(superType.getName(), children = new ArrayList()); + } + + children.add(mixin); + } + } + + public List findOverrides(ClassInfo owner, String name, String desc) { + return findOverrides(owner.getName(), name, desc); + } + + public List findOverrides(String owner, String name, String desc) { + List children = parentMixins.get(owner); + if (children == null) { + return Collections.emptyList(); + } + + List out = new ArrayList(children.size()); + + for (MixinInfo child : children) { + ClassNode node = child.getClassNode(ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + + MethodNode method = Bytecode.findMethod(node, name, desc); + if (method == null || Bytecode.isStatic(method)) { + continue; + } + + switch (Bytecode.getVisibility(method)) { + case PRIVATE: + break; + + case PACKAGE: + int ownerSplit = owner.lastIndexOf('/'); + int childSplit = node.name.lastIndexOf('/'); + //There is a reasonable chance mixins are in the same package, so it is viable that a package private method is overridden + if (ownerSplit != childSplit || (ownerSplit > 0 && !owner.regionMatches(0, node.name, 0, ownerSplit + 1))) { + break; + } + + out.add(method); + break; + default: + out.add(method); + break; + } + } + + return out.isEmpty() ? Collections.emptyList() : out; + } + + private final Map> parentMixins = new HashMap>(); } \ No newline at end of file diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorInterface.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorInterface.java index e5b779027..a01aa57fd 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorInterface.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorInterface.java @@ -24,15 +24,14 @@ */ package org.spongepowered.asm.mixin.transformer; -import java.lang.reflect.Modifier; - import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.FieldNode; import org.spongepowered.asm.mixin.MixinEnvironment; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel; import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.transformer.ClassInfo.Method; import org.spongepowered.asm.mixin.transformer.MixinInfo.MixinClassNode; import org.spongepowered.asm.mixin.transformer.MixinInfo.MixinMethodNode; @@ -43,6 +42,8 @@ import org.spongepowered.asm.util.Constants; import org.spongepowered.asm.util.LanguageFeatures; +import java.lang.reflect.Modifier; + /** * Bytecode preprocessor for interface mixins, simply performs some additional * verification for things which are unsupported in interfaces @@ -66,25 +67,54 @@ class MixinPreProcessorInterface extends MixinPreProcessorStandard { */ @Override protected void prepareMethod(MixinMethodNode mixinMethod, Method method) { - // Userland interfaces should not have non-public methods except for lambda bodies - if (!Bytecode.hasFlag(mixinMethod, Opcodes.ACC_PUBLIC)) { - if (Bytecode.hasFlag(mixinMethod, Opcodes.ACC_SYNTHETIC)) { - CompatibilityLevel requiredLevel = CompatibilityLevel.requiredFor(LanguageFeatures.PRIVATE_SYNTHETIC_METHODS_IN_INTERFACES); - if (MixinEnvironment.getCompatibilityLevel().isLessThan(requiredLevel)) { + boolean isPublic = Bytecode.hasFlag(mixinMethod, Opcodes.ACC_PUBLIC); + MixinEnvironment.Feature injectorsInInterfaceMixins = MixinEnvironment.Feature.INJECTORS_IN_INTERFACE_MIXINS; + CompatibilityLevel currentLevel = MixinEnvironment.getCompatibilityLevel(); + CompatibilityLevel requiredLevelSynthetic = CompatibilityLevel.requiredFor(LanguageFeatures.PRIVATE_SYNTHETIC_METHODS_IN_INTERFACES); + + if (!isPublic && mixinMethod.isSynthetic()) { + if (mixinMethod.isSynthetic()) { + if (currentLevel.isLessThan(requiredLevelSynthetic)) { throw new InvalidInterfaceMixinException(this.mixin, String.format( "Interface mixin contains a synthetic private method but compatibility level %s is required! Found %s in %s", - requiredLevel, method, this.mixin)); + requiredLevelSynthetic, method, this.mixin)); } - } else if (Constants.CLINIT.equals(mixinMethod.name) && "()V".equals(mixinMethod.desc)) { - return; //In order to shadow fields they must have a value set, this may result in a static initialiser being included - } else if (!Bytecode.hasFlag(mixinMethod, Opcodes.ACC_PRIVATE) || !MixinEnvironment.getCompatibilityLevel().supports(LanguageFeatures.PRIVATE_METHODS_IN_INTERFACES)) { - //On versions that support it private methods are also allowed - throw new InvalidInterfaceMixinException(this.mixin, "Interface mixin contains a non-public method! Found " + method + " in " - + this.mixin); + // Private synthetic is ok, do not process further + return; } } - super.prepareMethod(mixinMethod, method); + if (!isPublic) { + if (Constants.CLINIT.equals(mixinMethod.name) && "()V".equals(mixinMethod.desc)) { + return; //In order to shadow fields they must have a value set, this may result in a static initialiser being included + } + CompatibilityLevel requiredLevelPrivate = CompatibilityLevel.requiredFor(LanguageFeatures.PRIVATE_METHODS_IN_INTERFACES); + if (currentLevel.isLessThan(requiredLevelPrivate)) { + throw new InvalidInterfaceMixinException(this.mixin, String.format( + "Interface mixin contains a private method but compatibility level %s is required! Found %s in %s", + requiredLevelPrivate, method, this.mixin)); + } + } + + AnnotationNode injectorAnnotation = InjectionInfo.getInjectorAnnotation(this.mixin, mixinMethod); + if (injectorAnnotation == null) { + super.prepareMethod(mixinMethod, method); + return; + } + + if (injectorsInInterfaceMixins.isAvailable() && !injectorsInInterfaceMixins.isEnabled()) { + throw new InvalidInterfaceMixinException(this.mixin, String.format( + "Interface mixin contains an injector but Feature.INJECTORS_IN_INTERFACE_MIXINS is disabled! Found %s in %s", + method, this.mixin)); + } + + // Make injectors private synthetic if the current runtime supports it + if (isPublic + && !currentLevel.supports(LanguageFeatures.PRIVATE_METHODS_IN_INTERFACES) + && currentLevel.supports(LanguageFeatures.PRIVATE_SYNTHETIC_METHODS_IN_INTERFACES)) { + Bytecode.setVisibility(mixinMethod, Bytecode.Visibility.PRIVATE); + mixinMethod.access |= Opcodes.ACC_SYNTHETIC; + } } /* (non-Javadoc) @@ -101,7 +131,7 @@ protected boolean validateField(MixinTargetContext context, FieldNode field, Ann throw new InvalidInterfaceMixinException(this.mixin, String.format("Interface mixin contains an illegal field! Found %s %s in %s", Modifier.toString(field.access), field.name, this.mixin)); } - + //Whilst we could support adding constants, they'd always be public so there's little benefit to allowing it if (shadow == null) { throw new InvalidInterfaceMixinException(this.mixin, String.format("Interface mixin %s contains a non-shadow field: %s", @@ -110,7 +140,7 @@ protected boolean validateField(MixinTargetContext context, FieldNode field, Ann //Making a field non-final will result in verification crashes, so @Mutable is always a mistake if (Annotations.getVisible(field, Mutable.class) != null) { - throw new InvalidInterfaceMixinException(this.mixin, String.format("@Shadow field %s.%s is marked as mutable. This is not allowed.", + throw new InvalidInterfaceMixinException(this.mixin, String.format("@Shadow field %s.%s is marked as mutable. This is not allowed.", this.mixin, field.name)); } 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 d6fcac4ba..5be4f8f22 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorStandard.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinPreProcessorStandard.java @@ -649,15 +649,20 @@ protected void attachFields(MixinTargetContext context) { iter.remove(); continue; } else if (!Bytecode.compareFlags(mixinField, target, Opcodes.ACC_STATIC)) { - if (isShadow) { - throw new InvalidMixinException(this.mixin, String.format("STATIC modifier of @Shadow field %s in %s does not match the target", - mixinField.name, this.mixin)); - } else { - throw new InvalidMixinException(this.mixin, String.format("Field %s in %s conflicts with %sstatic field in the target (%s)", - mixinField.name, mixin, Bytecode.isStatic(target) ? "" : "non-", context.getTarget())); - } + if (isShadow) { + throw new InvalidMixinException(this.mixin, String.format("STATIC modifier of @Shadow field %s in %s does not match the target", + mixinField.name, this.mixin)); + } else { + throw new InvalidMixinException(this.mixin, String.format("Field %s in %s conflicts with %sstatic field in the target (%s)", + mixinField.name, mixin, Bytecode.isStatic(target) ? "" : "non-", context.getTarget())); + } } + if (!Bytecode.compareFlags(mixinField, target, Opcodes.ACC_STATIC)) { + throw new InvalidMixinException(this.mixin, String.format("STATIC modifier of @Shadow field %s in %s does not match the target", + mixinField.name, this.mixin)); + } + // Check that the shadow field has a matching descriptor if (!target.desc.equals(mixinField.desc)) { throw new InvalidMixinException(this.mixin, String.format("The field %s in the target class has a conflicting signature", 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 d67cc10be..2cfa27c33 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinProcessor.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinProcessor.java @@ -543,7 +543,7 @@ public void onInit(MixinInfo mixin) { this.handleMixinPrepareError(config, ex, environment); } catch (Exception ex) { String message = ex.getMessage(); - MixinProcessor.logger.error("Error encountered whilst initialising mixin config '" + config.getName() + "' from mod '"+org.spongepowered.asm.mixin.FabricUtil.getModId(config)+"': " + message, ex); + MixinProcessor.logger.error("Error encountered whilst initialising mixin config '" + config.getName() + "' from mod '" + org.spongepowered.asm.mixin.FabricUtil.getModId(config) + "': " + message, ex); } } @@ -570,7 +570,7 @@ public void onInit(MixinInfo mixin) { this.handleMixinPrepareError(config, ex, environment); } catch (Exception ex) { String message = ex.getMessage(); - MixinProcessor.logger.error("Error encountered during mixin config postInit step '" + config.getName() + "' from mod '"+org.spongepowered.asm.mixin.FabricUtil.getModId(config)+ "': " + message, ex); + MixinProcessor.logger.error("Error encountered during mixin config postInit step '" + config.getName() + "' from mod '" + org.spongepowered.asm.mixin.FabricUtil.getModId(config) + "': " + message, ex); } } 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 2ff0862a4..a97fc0b78 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTargetContext.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinTargetContext.java @@ -25,16 +25,8 @@ package org.spongepowered.asm.mixin.transformer; import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import org.spongepowered.asm.logging.Level; import org.spongepowered.asm.logging.ILogger; @@ -52,6 +44,7 @@ import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.mixin.gen.AccessorInfo; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.struct.Constructor; import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; import org.spongepowered.asm.mixin.injection.struct.InjectorGroupInfo; import org.spongepowered.asm.mixin.injection.struct.Target; @@ -68,6 +61,8 @@ import org.spongepowered.asm.mixin.transformer.ClassInfo.Traversal; import org.spongepowered.asm.mixin.transformer.ext.Extensions; import org.spongepowered.asm.mixin.transformer.meta.MixinMerged; +import org.spongepowered.asm.mixin.transformer.struct.Initialiser; +import org.spongepowered.asm.mixin.transformer.struct.InsnRange; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.mixin.transformer.throwables.MixinTransformerError; import org.spongepowered.asm.obfuscation.RemapperChain; @@ -92,7 +87,7 @@ * in the mixin to the appropriate members in the target class hierarchy. */ public class MixinTargetContext extends ClassContext implements IMixinContext { - + /** * Logger */ @@ -183,6 +178,11 @@ public class MixinTargetContext extends ClassContext implements IMixinContext { * upgraded if the version is below this value */ private int minRequiredClassVersion = CompatibilityLevel.JAVA_6.getClassVersion(); + + /** + * The mixin's initialiser + */ + private Initialiser initialiser; /** * ctor @@ -204,6 +204,8 @@ public class MixinTargetContext extends ClassContext implements IMixinContext { InnerClassGenerator icg = context.getExtensions().getGenerator(InnerClassGenerator.class); this.innerClasses = icg.getInnerClasses(this.mixin, this.getTargetClassRef()); + + this.initialiser = this.findInitialiser(); } /** @@ -338,6 +340,14 @@ public String getClassRef() { public TargetClassContext getTarget() { return this.targetClass; } + + /** + * Get the target class name + */ + @Override + public String getTargetClassName() { + return this.getTarget().getClassName(); + } /** * Get the target class reference @@ -448,7 +458,7 @@ public InjectorGroupInfo.Map getInjectorGroups() { public boolean requireOverwriteAnnotations() { return this.mixin.getParent().requireOverwriteAnnotations(); } - + /** * Handles "re-parenting" the method supplied, changes all references to the * mixin class to refer to the target class (for field accesses and method @@ -561,6 +571,7 @@ private void transformLVT(MethodNode method) { localVarActivity.next("var=%s", local.name); local.desc = this.transformSingleDescriptor(Type.getType(local.desc)); + local.signature = null; } localVarActivity.end(); } @@ -860,6 +871,10 @@ private void processImaginarySuper(MethodNode method, FieldInsnNode fieldInsn) { * @param methodRef Unbound reference to the method */ private void updateStaticBinding(MethodNode method, MemberRef methodRef) { + if (!methodRef.getOwner().equals(this.classNode.superName)) { + // Must be to an interface, no need to re-parent. + return; + } this.updateBinding(method, methodRef, Traversal.SUPER); } @@ -1018,6 +1033,61 @@ private String transformMethodDescriptor(String desc) { return newDesc.append(')').append(this.transformSingleDescriptor(Type.getReturnType(desc))).toString(); } + /** + * Get insns corresponding to the instance initialiser (hopefully) from the + * supplied constructor. + * + * @return initialiser bytecode extracted from the supplied constructor, or + * null if the constructor range could not be parsed + */ + final Initialiser getInitialiser() { + return this.initialiser; + } + + private Initialiser findInitialiser() { + // Try to find a suitable constructor, we need a constructor with line numbers in order to extract the initialiser + MethodNode ctor = this.getConstructor(); + if (ctor == null) { + return null; + } + + // Find the range of line numbers which corresponds to the constructor body + InsnRange init = Constructor.getRange(ctor); + if (!init.isValid()) { + return null; + } + + Initialiser initialiser = new Initialiser(this, ctor, init); + for (Constructor targetCtor : this.getTarget().getConstructors()) { + if (targetCtor.isInjectable()) { + targetCtor.inspect(initialiser); + } + } + return initialiser; + } + + /** + * Finds a suitable ctor for reading the instance initialiser bytecode + * + * @return appropriate ctor or null if none found + */ + private MethodNode getConstructor() { + MethodNode ctor = null; + + for (MethodNode method : this.getMethods()) { + if (Constants.CTOR.equals(method.name) && Bytecode.methodHasLineNumbers(method)) { + if (ctor == null) { + ctor = method; + } else { + // Not an error condition, just weird + MixinTargetContext.logger.warn("Mixin {} has multiple constructors, {} was selected\n", this, ctor.desc); + } + } + } + + return ctor; + } + /** * Get a target method handle from the target class * @@ -1298,7 +1368,7 @@ void postApply(String transformedName, ClassNode targetClass) { * @return unique method name */ String getUniqueName(MethodNode method, boolean preservePrefix) { - return this.targetClassInfo.getMethodMapper().getUniqueName(method, this.sessionId, preservePrefix); + return this.targetClassInfo.getMethodMapper().getUniqueName(this.mixin, method, this.sessionId, preservePrefix); } /** @@ -1309,7 +1379,7 @@ String getUniqueName(MethodNode method, boolean preservePrefix) { * @return unique field name */ String getUniqueName(FieldNode field) { - return this.targetClassInfo.getMethodMapper().getUniqueName(field, this.sessionId); + return this.targetClassInfo.getMethodMapper().getUniqueName(this.mixin, field, this.sessionId); } /** @@ -1357,9 +1427,20 @@ InjectionInfo getFirstInjectionInfo() { } /** - * Apply injectors discovered in the {@link #prepareInjections()} pass + * Get all injector order values for injectors in this mixin + * + * @param orders Orders Set to add this mixin's orders to + */ + void getInjectorOrders(Set orders) { + for (InjectionInfo injectInfo : this.injectors) { + orders.add(injectInfo.getOrder()); + } + } + + /** + * Run the preinject step for all discovered injectors */ - void applyInjections() { + void applyPreInjections() { this.activities.clear(); try { @@ -1369,23 +1450,48 @@ void applyInjections() { preInjectActivity.next(injectInfo.toString()); injectInfo.preInject(); } + applyActivity.end(); + } catch (InvalidMixinException ex) { + ex.prepend(this.activities); + throw ex; + } catch (Exception ex) { + throw new InvalidMixinException(this, "Unexpecteded " + ex.getClass().getSimpleName() + " whilst transforming the mixin class:", ex, + this.activities); + } + } - applyActivity.next("Inject"); + /** + * Apply injectors discovered in the {@link #prepareInjections()} pass + * + * @param injectorOrder injector order for this pass + */ + void applyInjections(int injectorOrder) { + this.activities.clear(); + + List injectors = new ArrayList(); + for (InjectionInfo injectInfo : this.injectors) { + if (injectInfo.getOrder() == injectorOrder) { + injectors.add(injectInfo); + } + } + + try { + IActivity applyActivity = this.activities.begin("Inject"); IActivity injectActivity = this.activities.begin("?"); - for (InjectionInfo injectInfo : this.injectors) { + for (InjectionInfo injectInfo : injectors) { injectActivity.next(injectInfo.toString()); injectInfo.inject(); } applyActivity.next("PostInject"); IActivity postInjectActivity = this.activities.begin("?"); - for (InjectionInfo injectInfo : this.injectors) { + for (InjectionInfo injectInfo : injectors) { postInjectActivity.next(injectInfo.toString()); injectInfo.postInject(); } applyActivity.end(); - this.injectors.clear(); + this.injectors.removeAll(injectors); } catch (InvalidMixinException ex) { ex.prepend(this.activities); throw ex; diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/TargetClassContext.java b/src/main/java/org/spongepowered/asm/mixin/transformer/TargetClassContext.java index 1e3b32ff8..658943c05 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/TargetClassContext.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/TargetClassContext.java @@ -41,6 +41,7 @@ import org.spongepowered.asm.mixin.Debug; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.MixinEnvironment.Option; +import org.spongepowered.asm.mixin.injection.struct.Constructor; import org.spongepowered.asm.mixin.injection.struct.Target; import org.spongepowered.asm.mixin.struct.SourceMap; import org.spongepowered.asm.mixin.transformer.ext.Extensions; @@ -51,6 +52,7 @@ import org.spongepowered.asm.util.Annotations; import org.spongepowered.asm.util.Bytecode; import org.spongepowered.asm.util.ClassSignature; +import org.spongepowered.asm.util.Constants; import org.spongepowered.asm.util.perf.Profiler; import org.spongepowered.asm.util.perf.Profiler.Section; @@ -231,7 +233,20 @@ public ClassNode getClassNode() { List getMethods() { return this.classNode.methods; } - + + /** + * Get the class constructors + */ + List getConstructors() { + List ctors = new ArrayList(); + for (MethodNode method : this.classNode.methods) { + if (Constants.CTOR.equals(method.name)) { + ctors.add((Constructor)this.getTargetMethod(method)); + } + } + return ctors; + } + /** * Get the class fields (from the tree) */ @@ -383,7 +398,7 @@ Target getTargetMethod(MethodNode method) { String targetName = method.name + method.desc; Target target = this.targetMethods.get(targetName); if (target == null) { - target = new Target(this.classNode, method); + target = Target.of(this.classInfo, this.classNode, method); this.targetMethods.put(targetName, target); } return target; @@ -407,9 +422,10 @@ void applyMixins() { } /** - * Run extensions before apply + * Run extensions before apply and clean up any global state in case this is a hotswap */ private void preApply() { + this.getClassInfo().getMethodMapper().reset(); this.extensions.preApply(this); } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/ext/extensions/ExtensionClassExporter.java b/src/main/java/org/spongepowered/asm/mixin/transformer/ext/extensions/ExtensionClassExporter.java index 02ed7a38c..352e83b96 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/ext/extensions/ExtensionClassExporter.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/ext/extensions/ExtensionClassExporter.java @@ -44,8 +44,6 @@ import org.spongepowered.asm.util.perf.Profiler.Section; import com.google.common.io.Files; -import com.google.common.io.MoreFiles; -import com.google.common.io.RecursiveDeleteOption; /** * Debug exporter @@ -76,7 +74,7 @@ public ExtensionClassExporter(MixinEnvironment env) { this.decompiler = this.initDecompiler(env, new File(Constants.DEBUG_OUTPUT_DIR, ExtensionClassExporter.EXPORT_JAVA_DIR)); try { - MoreFiles.deleteRecursively(this.classExportDir.toPath(), RecursiveDeleteOption.ALLOW_INSECURE); + org.spongepowered.asm.util.Files.deleteRecursively(this.classExportDir); } catch (IOException ex) { ExtensionClassExporter.logger.debug("Error cleaning class output directory: {}", ex.getMessage()); } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/ext/extensions/ExtensionLVTCleaner.java b/src/main/java/org/spongepowered/asm/mixin/transformer/ext/extensions/ExtensionLVTCleaner.java new file mode 100644 index 000000000..6729b5360 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/ext/extensions/ExtensionLVTCleaner.java @@ -0,0 +1,85 @@ +/* + * 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.ext.extensions; + +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodNode; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.transformer.ext.IExtension; +import org.spongepowered.asm.mixin.transformer.ext.ITargetClassContext; +import org.spongepowered.asm.util.Locals; + +import java.util.Iterator; + +/** + * Strips synthetic local variables from the LVT after exporting to avoid debuggers becoming confused by them. + */ +public class ExtensionLVTCleaner implements IExtension { + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.ext.IExtension#checkActive( + * org.spongepowered.asm.mixin.MixinEnvironment) + */ + @Override + public boolean checkActive(MixinEnvironment environment) { + return true; + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.IMixinTransformerModule + * #preApply(org.spongepowered.asm.mixin.transformer.TargetClassContext) + */ + @Override + public void preApply(ITargetClassContext context) { + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.IMixinTransformerModule + * #postApply(org.spongepowered.asm.mixin.transformer.TargetClassContext) + */ + @Override + public void postApply(ITargetClassContext context) { + } + + /* (non-Javadoc) + * @see org.spongepowered.asm.mixin.transformer.ext.IExtension + * #export(org.spongepowered.asm.mixin.MixinEnvironment, + * java.lang.String, boolean, org.objectweb.asm.tree.ClassNode) + */ + @Override + public void export(MixinEnvironment env, String name, boolean force, ClassNode classNode) { + for (MethodNode methodNode : classNode.methods) { + if (methodNode.localVariables != null) { + for (Iterator it = methodNode.localVariables.iterator(); it.hasNext(); ) { + if (it.next() instanceof Locals.SyntheticLocalVariableNode) { + it.remove(); + } + } + } + } + } + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/struct/Initialiser.java b/src/main/java/org/spongepowered/asm/mixin/transformer/struct/Initialiser.java new file mode 100644 index 000000000..8dfac1be2 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/struct/Initialiser.java @@ -0,0 +1,217 @@ +/* + * 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.struct; + +import java.util.Deque; +import java.util.Locale; +import java.util.Map; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodNode; +import org.spongepowered.asm.logging.ILogger; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.MixinEnvironment.Option; +import org.spongepowered.asm.mixin.injection.struct.Constructor; +import org.spongepowered.asm.mixin.transformer.MixinTargetContext; +import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; +import org.spongepowered.asm.service.MixinService; +import org.spongepowered.asm.util.Bytecode; + +public class Initialiser { + + /** + * Strategy for injecting initialiser insns + */ + public enum InjectionMode { + + /** + * Default mode, attempts to place initialisers after all other + * competing initialisers in the target ctor + */ + DEFAULT, + + /** + * Safe mode, only injects initialiser directly after the super-ctor + * invocation + */ + SAFE; + + /** + * Get the injection mode based on the the environment + * + * @param env Environment to query for the injection mode option + */ + public static InjectionMode ofEnvironment(MixinEnvironment env) { + String strMode = env.getOptionValue(Option.INITIALISER_INJECTION_MODE); + if (strMode == null) { + return Initialiser.InjectionMode.DEFAULT; + } + try { + return Initialiser.InjectionMode.valueOf(strMode.toUpperCase(Locale.ROOT)); + } catch (Exception ex) { + Initialiser.logger.warn("Could not parse unexpected value \"{}\" for mixin.initialiserInjectionMode, reverting to DEFAULT", + strMode); + return Initialiser.InjectionMode.DEFAULT; + } + } + + } + + /** + * Logger + */ + static final ILogger logger = MixinService.getService().getLogger("mixin"); + + /** + * List of opcodes which must not appear in a class initialiser, mainly a + * sanity check so that if any of the specified opcodes are found, we can + * log it as an error condition and then people can bitch at me to fix it. + * Essentially if it turns out that field initialisers can somehow make use + * of local variables, then I need to write some code to ensure that said + * locals are shifted so that they don't interfere with locals in the + * receiving constructor. + */ + protected static final int[] OPCODE_BLACKLIST = { + Opcodes.RETURN, Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, + Opcodes.ISTORE, Opcodes.LSTORE, Opcodes.FSTORE, Opcodes.DSTORE, Opcodes.ASTORE, + // Fabric: Array opcodes cause no problems in initialisers and should not be needlessly restricted. + // Opcodes.IALOAD, Opcodes.LALOAD, Opcodes.FALOAD, Opcodes.DALOAD, Opcodes.AALOAD, + // Opcodes.BALOAD, Opcodes.CALOAD, Opcodes.SALOAD, Opcodes.IASTORE, Opcodes.LASTORE, + // Opcodes.FASTORE, Opcodes.DASTORE, Opcodes.AASTORE, Opcodes.BASTORE, Opcodes.CASTORE, Opcodes.SASTORE + }; + + + /** + * Mixin context which contains the source constructor + */ + private final MixinTargetContext mixin; + + /** + * Source constructor + */ + private final MethodNode ctor; + + /** + * Filtered instructions + */ + private Deque insns; + + public Initialiser(MixinTargetContext mixin, MethodNode ctor, InsnRange range) { + this.mixin = mixin; + this.ctor = ctor; + this.initInstructions(range); + } + + private void initInstructions(InsnRange range) { + // Now we know where the constructor is, look for insns which lie OUTSIDE the method body + this.insns = range.apply(this.ctor.instructions, false); + + for (AbstractInsnNode insn : this.insns) { + int opcode = insn.getOpcode(); + for (int ivalidOp : Initialiser.OPCODE_BLACKLIST) { + if (opcode == ivalidOp) { + // At the moment I don't handle any transient locals because I haven't seen any in the wild, but let's avoid writing + // code which will likely break things and fix it if a real test case ever appears + throw new InvalidMixinException(this.mixin, "Cannot handle " + Bytecode.getOpcodeName(opcode) + " opcode (0x" + + Integer.toHexString(opcode).toUpperCase(Locale.ROOT) + ") in class initialiser"); + } + } + } + + // Check that the last insn is a PUTFIELD, if it's not then + AbstractInsnNode last = this.insns.peekLast(); + if (last != null) { + if (last.getOpcode() != Opcodes.PUTFIELD) { + throw new InvalidMixinException(this.mixin, "Could not parse initialiser, expected 0xB5, found 0x" + + Integer.toHexString(last.getOpcode()) + " in " + this); + } + } + } + + /** + * Get the number of instructions in the extracted initialiser + */ + public int size() { + return this.insns.size(); + } + + /** + * Get the MAXS for the original (source) constructor + */ + public int getMaxStack() { + return this.ctor.maxStack; + } + + /** + * Get the source constructor + */ + public MethodNode getCtor() { + return this.ctor; + } + + /** + * Get the extracted instructions + */ + public Deque getInsns() { + return this.insns; + } + + /** + * Inject initialiser code into the target constructor + * + * @param ctor Constructor to inject into + */ + public void injectInto(Constructor ctor) { + AbstractInsnNode marker = ctor.findInitialiserInjectionPoint(Initialiser.InjectionMode.ofEnvironment(this.mixin.getEnvironment())); + if (marker == null) { + Initialiser.logger.warn("Failed to locate initialiser injection point in {}, initialiser was not mixed in.", ctor.getDesc()); + return; + } + + Map labels = Bytecode.cloneLabels(ctor.insns); + // Fabric: also clone labels from the initialiser as they will be merged. + for (AbstractInsnNode node : this.insns) { + if (node instanceof LabelNode) { + labels.put((LabelNode) node, new LabelNode()); + } + } + for (AbstractInsnNode node : this.insns) { + if (node instanceof LabelNode) { + // Fabric: Merge cloned labels instead of skipping them. + // continue; + } + if (node instanceof JumpInsnNode) { + // Fabric: Jumps cause no issues if labels are cloned properly and should not be needlessly restricted. + // throw new InvalidMixinException(this.mixin, "Unsupported JUMP opcode in initialiser in " + this.mixin); + } + + ctor.insertBefore(marker, node.clone(labels)); + } + } + +} diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/struct/InsnRange.java b/src/main/java/org/spongepowered/asm/mixin/transformer/struct/InsnRange.java new file mode 100644 index 000000000..86ed33f38 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/struct/InsnRange.java @@ -0,0 +1,156 @@ +/* + * 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.struct; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LineNumberNode; + +/** + * Struct for representing a range of instructions + */ +public class InsnRange { + + /** + * Start of the range (line number) + */ + public final int start; + + /** + * End of the range (line number) + */ + public final int end; + + /** + * Range marker (index of insn) + */ + public final int marker; + + /** + * Create a range with the specified values. + * + * @param start Start of the range + * @param end End of the range + * @param marker Arbitrary marker value + */ + public InsnRange(int start, int end, int marker) { + this.start = start; + this.end = end; + this.marker = marker; + } + + /** + * Range is valid if both start and end are nonzero and end is after or + * at start + * + * @return true if valid + */ + public boolean isValid() { + return (this.start != 0 && this.end != 0 && this.end >= this.start); + } + + /** + * Returns true if the supplied value is between or equal to start and + * end + * + * @param value true if the range contains value + */ + public boolean contains(int value) { + return value >= this.start && value <= this.end; + } + + /** + * Returns true if the supplied value is outside the range + * + * @param value true if the range does not contain value + */ + public boolean excludes(int value) { + return value < this.start || value > this.end; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("Range[%d-%d,%d,valid=%s)", this.start, this.end, this.marker, this.isValid()); + } + + /** + * Apply this range to the specified insn list + * + * @param insns insn list to filter + * @param inclusive whether to include or exclude instructions + * @return filtered list + */ + public Deque apply(InsnList insns, boolean inclusive) { + Deque filtered = new ArrayDeque(); + int line = 0; + + boolean gatherNodes = false; + int trimAtOpcode = -1; + LabelNode optionalInsn = null; + for (Iterator iter = insns.iterator(this.marker); iter.hasNext();) { + AbstractInsnNode insn = iter.next(); + if (insn instanceof LineNumberNode) { + line = ((LineNumberNode)insn).line; + AbstractInsnNode next = insns.get(insns.indexOf(insn) + 1); + if (line == this.end && next.getOpcode() != Opcodes.RETURN) { + gatherNodes = !inclusive; + trimAtOpcode = Opcodes.RETURN; + } else { + gatherNodes = inclusive ? this.contains(line) : this.excludes(line); + trimAtOpcode = -1; + } + } else if (gatherNodes) { + if (optionalInsn != null) { + filtered.add(optionalInsn); + optionalInsn = null; + } + + if (insn instanceof LabelNode) { + optionalInsn = (LabelNode)insn; + } else { + int opcode = insn.getOpcode(); + if (opcode == trimAtOpcode) { + trimAtOpcode = -1; + continue; + } + + filtered.add(insn); + } + } + } + + return filtered; + } + +} diff --git a/src/main/java/org/spongepowered/asm/service/IClassBytecodeProvider.java b/src/main/java/org/spongepowered/asm/service/IClassBytecodeProvider.java index bc8ac4cf2..0525d9723 100644 --- a/src/main/java/org/spongepowered/asm/service/IClassBytecodeProvider.java +++ b/src/main/java/org/spongepowered/asm/service/IClassBytecodeProvider.java @@ -54,4 +54,17 @@ public interface IClassBytecodeProvider { */ public abstract ClassNode getClassNode(String name, boolean runTransformers) throws ClassNotFoundException, IOException; + + /** + * Retrieve transformed class as an ASM tree + * + * @param name full class name + * @param runTransformers true to run transformers when loading the class + * @param readerFlags Flags to pass in to ClassReader.accept + * @return tree + * @throws ClassNotFoundException if class not found + * @throws IOException propagated + */ + public abstract ClassNode getClassNode(String name, boolean runTransformers, int readerFlags) throws ClassNotFoundException, IOException; + } diff --git a/src/main/java/org/spongepowered/asm/service/MixinService.java b/src/main/java/org/spongepowered/asm/service/MixinService.java index d4a752e2d..6c54333ae 100644 --- a/src/main/java/org/spongepowered/asm/service/MixinService.java +++ b/src/main/java/org/spongepowered/asm/service/MixinService.java @@ -221,7 +221,9 @@ private IMixinService initService() { if (serviceCls != null) { try { IMixinService service = (IMixinService) Class.forName(serviceCls).getConstructor().newInstance(); - if (!service.isValid()) throw new RuntimeException("invalid service "+serviceCls+" configured via system property"); + if (!service.isValid()) { + throw new RuntimeException("invalid service " + serviceCls + " configured via system property"); + } return service; } catch (ReflectiveOperationException e) { diff --git a/src/main/java/org/spongepowered/asm/service/MixinServiceAbstract.java b/src/main/java/org/spongepowered/asm/service/MixinServiceAbstract.java index c01c814d7..6c4fb5fca 100644 --- a/src/main/java/org/spongepowered/asm/service/MixinServiceAbstract.java +++ b/src/main/java/org/spongepowered/asm/service/MixinServiceAbstract.java @@ -54,12 +54,6 @@ public abstract class MixinServiceAbstract implements IMixinService { protected static final String MIXIN_PACKAGE = "org.spongepowered.asm.mixin."; protected static final String SERVICE_PACKAGE = "org.spongepowered.asm.service."; - /** - * Logger adapter, replacement for log4j2 logger as services should use - * their own loggers now in order to avoid contamination - */ - private static ILogger logger; - /** * Cached logger adapters */ @@ -86,12 +80,6 @@ public abstract class MixinServiceAbstract implements IMixinService { */ private String sideName; - protected MixinServiceAbstract() { - if (MixinServiceAbstract.logger == null) { - MixinServiceAbstract.logger = this.getLogger("mixin"); - } - } - /* (non-Javadoc) * @see org.spongepowered.asm.service.IMixinService#prepare() */ @@ -226,7 +214,7 @@ public final String getSideName() { return this.sideName = side; } } catch (Exception ex) { - MixinServiceAbstract.logger.catching(ex); + this.getLogger("mixin").catching(ex); } } diff --git a/src/main/java/org/spongepowered/asm/util/Bytecode.java b/src/main/java/org/spongepowered/asm/util/Bytecode.java index d2544c416..eae400113 100644 --- a/src/main/java/org/spongepowered/asm/util/Bytecode.java +++ b/src/main/java/org/spongepowered/asm/util/Bytecode.java @@ -43,6 +43,7 @@ import org.objectweb.asm.util.CheckClassAdapter; import org.objectweb.asm.util.TraceClassVisitor; import org.spongepowered.asm.util.asm.ASM; +import org.spongepowered.asm.util.asm.MarkerNode; import org.spongepowered.asm.util.throwables.SyntheticBridgeException; import org.spongepowered.asm.util.throwables.SyntheticBridgeException.Problem; @@ -440,6 +441,11 @@ public static String describeNode(AbstractInsnNode node, boolean listFormat) { return listFormat ? String.format(" %-14s ", "null") : "null"; } + if (node instanceof MarkerNode) { + MarkerNode marker = (MarkerNode)node; + return String.format("[%s] Marker type=%d", marker.getLabel(), marker.type); + } + if (node instanceof LabelNode) { return String.format("[%s]", ((LabelNode)node).getLabel()); } @@ -521,6 +527,57 @@ private static String getOpcodeName(int opcode, String start, int min) { return opcode >= 0 ? String.valueOf(opcode) : "UNKNOWN"; } + + /** + * Uses reflection to find 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 + * found or -1 if not matched. Note that no validation is performed on + * numeric opcode values. + * + * @param opcodeName Opcode string to match + * @return matched opcode value or -1 if not matched. + */ + public static int parseOpcodeName(String opcodeName) { + if (opcodeName == null) { + return -1; + } + + if (opcodeName.matches("^1[0-9]{0,2}|[1-9][0-9]?$")) { + return Integer.parseInt(opcodeName); + } + + if (opcodeName.startsWith("Opcodes.")) { + opcodeName = opcodeName.substring(8); + } + + if (!opcodeName.matches("^[A-Z][A-Z0-9_]+$")) { + return -1; + } + + return Bytecode.parseOpcodeName(opcodeName, "UNINITIALIZED_THIS", 1); + } + + 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); + } + } + } catch (Exception ex) { + // well this is embarrassing + } + + return -1; + } /** * Returns true if the supplied method contains any line number information diff --git a/src/main/java/org/spongepowered/asm/util/Files.java b/src/main/java/org/spongepowered/asm/util/Files.java index 82a26778d..a28931247 100644 --- a/src/main/java/org/spongepowered/asm/util/Files.java +++ b/src/main/java/org/spongepowered/asm/util/Files.java @@ -25,6 +25,7 @@ package org.spongepowered.asm.util; import java.io.File; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -75,4 +76,42 @@ public static File toFile(URI uri) { return new File(uri); } + /** + * Recursively delete a directory. Does not support symlinks but this is + * mainly used by mixin internals which only write files and normal + * directories so it should be fine + * + * @param dir Root directory to delete + * @throws IOException Security errors and any other IO errors encountered + * are raised as IOExceptions + */ + public static void deleteRecursively(File dir) throws IOException { + if (dir == null || !dir.isDirectory()) { + return; + } + + try { + File[] files = dir.listFiles(); + if (files == null) { + throw new IOException("Error enumerating directory during recursive delete operation: " + dir.getAbsolutePath()); + } + + for (File child : files) { + if (child.isDirectory()) { + Files.deleteRecursively(child); + } else if (child.isFile()) { + if (!child.delete()) { + throw new IOException("Error deleting file during recursive delete operation: " + child.getAbsolutePath()); + } + } + } + + if (!dir.delete()) { + throw new IOException("Error deleting directory during recursive delete operation: " + dir.getAbsolutePath()); + } + } catch (SecurityException ex) { + throw new IOException("Security error during recursive delete operation", ex); + } + } + } diff --git a/src/main/java/org/spongepowered/asm/util/JavaVersion.java b/src/main/java/org/spongepowered/asm/util/JavaVersion.java index c95c93302..9382f27b3 100644 --- a/src/main/java/org/spongepowered/asm/util/JavaVersion.java +++ b/src/main/java/org/spongepowered/asm/util/JavaVersion.java @@ -97,25 +97,25 @@ public abstract class JavaVersion { */ public static final double JAVA_18 = 18.0; - /** - * Version number for Java 19 - */ - public static final double JAVA_19 = 19.0; + /** + * Version number for Java 19 + */ + public static final double JAVA_19 = 19.0; - /** - * Version number for Java 20 - */ - public static final double JAVA_20 = 20.0; + /** + * Version number for Java 20 + */ + public static final double JAVA_20 = 20.0; - /** - * Version number for Java 21 - */ - public static final double JAVA_21 = 21.0; + /** + * Version number for Java 21 + */ + public static final double JAVA_21 = 21.0; - /** - * Version number for Java 22 - */ - public static final double JAVA_22 = 22.0; + /** + * Version number for Java 22 + */ + public static final double JAVA_22 = 22.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 a7f01b209..0fcc960c1 100644 --- a/src/main/java/org/spongepowered/asm/util/Locals.java +++ b/src/main/java/org/spongepowered/asm/util/Locals.java @@ -328,6 +328,7 @@ public static void loadLocals(Type[] locals, InsnList insns, int pos, int limit) * bear in mind that if the specified node is itself a STORE opcode, * then we will be looking at the state of the locals PRIOR to its * invocation + * @param fabricCompatibility Fabric compatibility level * @return A sparse array containing a view (hopefully) of the locals at the * specified location */ @@ -335,7 +336,7 @@ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode me if (fabricCompatibility >= org.spongepowered.asm.mixin.FabricUtil.COMPATIBILITY_0_10_0) { return Locals.getLocalsAt(classNode, method, node, Settings.DEFAULT); } else { - return getLocalsAt_0_9_2(classNode, method, node); + return getLocalsAt092(classNode, method, node); } } @@ -542,8 +543,9 @@ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode me VarInsnNode varInsn = (VarInsnNode)insn; boolean isLoad = insn.getOpcode() >= Opcodes.ILOAD && insn.getOpcode() <= Opcodes.SALOAD; if (isLoad) { - frame[varInsn.var] = Locals.getLocalVariableAt(classNode, method, insn, varInsn.var); - int varSize = frame[varInsn.var].desc != null ? Type.getType(frame[varInsn.var].desc).getSize() : 1; + LocalVariableNode toLoad = Locals.getLocalVariableAt(classNode, method, insn, varInsn.var); + frame[varInsn.var] = toLoad; + int varSize = toLoad != null && toLoad.desc != null ? Type.getType(frame[varInsn.var].desc).getSize() : 1; knownFrameSize = Math.max(knownFrameSize, varInsn.var + varSize); if (settings.hasFlags(Settings.RESURRECT_EXPOSED_ON_LOAD)) { Locals.resurrect(frame, knownFrameSize, settings); @@ -579,7 +581,7 @@ public static LocalVariableNode[] getLocalsAt(ClassNode classNode, MethodNode me return frame; } - private static LocalVariableNode[] getLocalsAt_0_9_2(ClassNode classNode, MethodNode method, AbstractInsnNode node) { + private static LocalVariableNode[] getLocalsAt092(ClassNode classNode, MethodNode method, AbstractInsnNode node) { for (int i = 0; i < 3 && (node instanceof LabelNode || node instanceof LineNumberNode); i++) { node = Locals.nextNode(method.instructions, node); } diff --git a/src/main/java/org/spongepowered/asm/util/SignaturePrinter.java b/src/main/java/org/spongepowered/asm/util/SignaturePrinter.java index 8e64ae984..220f7d5d6 100644 --- a/src/main/java/org/spongepowered/asm/util/SignaturePrinter.java +++ b/src/main/java/org/spongepowered/asm/util/SignaturePrinter.java @@ -148,14 +148,15 @@ public String getReturnType() { */ public void setModifiers(MethodNode method) { String returnType = SignaturePrinter.getTypeName(Type.getReturnType(method.desc), false, this.fullyQualified); + String staticType = (method.access & Opcodes.ACC_STATIC) != 0 ? "static " : ""; if ((method.access & Opcodes.ACC_PUBLIC) != 0) { - this.setModifiers("public " + returnType); + this.setModifiers("public " + staticType + returnType); } else if ((method.access & Opcodes.ACC_PROTECTED) != 0) { - this.setModifiers("protected " + returnType); + this.setModifiers("protected " + staticType + returnType); } else if ((method.access & Opcodes.ACC_PRIVATE) != 0) { - this.setModifiers("private " + returnType); + this.setModifiers("private " + staticType + returnType); } else { - this.setModifiers(returnType); + this.setModifiers(staticType + returnType); } } diff --git a/src/main/java/org/spongepowered/asm/util/VersionNumber.java b/src/main/java/org/spongepowered/asm/util/VersionNumber.java index d764dd36b..ff9e8b06e 100644 --- a/src/main/java/org/spongepowered/asm/util/VersionNumber.java +++ b/src/main/java/org/spongepowered/asm/util/VersionNumber.java @@ -141,7 +141,7 @@ public int compareTo(VersionNumber other) { if (other == null) { return 1; } - return CompareUtil.compare(this.value, other.value); + return Long.compare(this.value, other.value); } /* (non-Javadoc) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/util/InvokeUtil.java b/src/main/java/org/spongepowered/asm/util/asm/MarkerNode.java similarity index 52% rename from src/main/java/org/spongepowered/asm/mixin/injection/invoke/util/InvokeUtil.java rename to src/main/java/org/spongepowered/asm/util/asm/MarkerNode.java index 8e874ed60..64321baf7 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/invoke/util/InvokeUtil.java +++ b/src/main/java/org/spongepowered/asm/util/asm/MarkerNode.java @@ -22,29 +22,40 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.asm.mixin.injection.invoke.util; +package org.spongepowered.asm.util.asm; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.MethodInsnNode; -import org.spongepowered.asm.mixin.injection.invoke.RedirectInjector; -import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.tree.LabelNode; -import java.util.Arrays; +/** + * A label node used as a marker in the bytecode. Does not actually visit the + * label when visited. + */ +public class MarkerNode extends LabelNode { + + /** + * Marks the end of the initialiser in a constructor + */ + public static final int INITIALISER_TAIL = 1; + + /** + * Marks the start of the body in a constructor + */ + public static final int BODY_START = 2; + + /** + * The type for this marker + */ + public final int type; -public class InvokeUtil { - public static Type[] getOriginalArgs(InjectionNode node) { - return Type.getArgumentTypes(((MethodInsnNode) node.getOriginalTarget()).desc); + public MarkerNode(int type) { + super(null); + this.type = type; } - public static Type[] getCurrentArgs(InjectionNode node) { - MethodInsnNode original = (MethodInsnNode) node.getOriginalTarget(); - MethodInsnNode current = (MethodInsnNode) node.getCurrentTarget(); - Type[] currentArgs = Type.getArgumentTypes(current.desc); - if (node.isReplaced() && node.hasDecoration(RedirectInjector.Meta.KEY) && original.getOpcode() != Opcodes.INVOKESTATIC) { - // A redirect on a non-static target method will have an extra arg at the start that we don't care about. - return Arrays.copyOfRange(currentArgs, 1, currentArgs.length); - } - return currentArgs; + @Override + public void accept(MethodVisitor methodVisitor) { + // Noop } + } diff --git a/src/main/java/org/spongepowered/asm/util/asm/MixinVerifier.java b/src/main/java/org/spongepowered/asm/util/asm/MixinVerifier.java index 8519887b7..cdbd02ef4 100644 --- a/src/main/java/org/spongepowered/asm/util/asm/MixinVerifier.java +++ b/src/main/java/org/spongepowered/asm/util/asm/MixinVerifier.java @@ -24,83 +24,161 @@ */ package org.spongepowered.asm.util.asm; -import java.util.List; - import org.objectweb.asm.Type; +import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.SimpleVerifier; import org.spongepowered.asm.mixin.transformer.ClassInfo; -import org.spongepowered.asm.mixin.transformer.ClassInfo.TypeLookup; + +import java.util.List; /** * Verifier which handles class info lookups via {@link ClassInfo} */ public class MixinVerifier extends SimpleVerifier { - - private Type currentClass; - private Type currentSuperClass; - private List currentClassInterfaces; - private boolean isInterface; + private static final Type OBJECT_TYPE = Type.getType(Object.class); public MixinVerifier(int api, Type currentClass, Type currentSuperClass, List currentClassInterfaces, boolean isInterface) { super(api, currentClass, currentSuperClass, currentClassInterfaces, isInterface); - this.currentClass = currentClass; - this.currentSuperClass = currentSuperClass; - this.currentClassInterfaces = currentClassInterfaces; - this.isInterface = isInterface; } @Override - protected boolean isInterface(final Type type) { - if (this.currentClass != null && type.equals(this.currentClass)) { - return this.isInterface; + protected boolean isInterface(Type type) { + if (type.getSort() != Type.OBJECT) { + return false; } - return ClassInfo.forType(type, TypeLookup.ELEMENT_TYPE).isInterface(); + return ClassInfo.forType(type, ClassInfo.TypeLookup.DECLARED_TYPE).isInterface(); } @Override - protected Type getSuperClass(final Type type) { - if (this.currentClass != null && type.equals(this.currentClass)) { - return this.currentSuperClass; + protected boolean isSubTypeOf(BasicValue value, BasicValue expected) { + Type expectedType = expected.getType(); + Type type = value.getType(); + switch (expectedType.getSort()) { + case Type.INT: + case Type.FLOAT: + case Type.LONG: + case Type.DOUBLE: + return type.equals(expectedType); + case Type.ARRAY: + case Type.OBJECT: + if (type.equals(NULL_TYPE)) { + return true; + } else if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) { + if (isAssignableFrom(expectedType, type)) { + return true; + } + if (expectedType.getSort() == Type.ARRAY) { + if (type.getSort() != Type.ARRAY) { + return false; + } + int dim = expectedType.getDimensions(); + expectedType = expectedType.getElementType(); + if (dim > type.getDimensions() || expectedType.getSort() != Type.OBJECT) { + return false; + } + type = Type.getType(type.getDescriptor().substring(dim)); + } + if (isInterface(expectedType)) { + // The merge of class or interface types can only yield class types (because it is not + // possible in general to find an unambiguous common super interface, due to multiple + // inheritance). Because of this limitation, we need to relax the subtyping check here + // if 'value' is an interface. + return type.getSort() >= Type.ARRAY; + } else { + return false; + } + } else { + return false; + } + default: + throw new AssertionError(); } - ClassInfo c = ClassInfo.forType(type, TypeLookup.ELEMENT_TYPE).getSuperClass(); - return c == null ? null : Type.getType("L" + c.getName() + ";"); } @Override - protected boolean isAssignableFrom(final Type type, final Type other) { - if (type.equals(other)) { - return true; + protected boolean isAssignableFrom(Type type1, Type type2) { + return type1.equals(getCommonSupertype(type1, type2)); + } + + @Override + public BasicValue merge(BasicValue value1, BasicValue value2) { + if (value1.equals(value2)) { + return value1; } - if (this.currentClass != null && type.equals(this.currentClass)) { - if (this.getSuperClass(other) == null) { - return false; - } - if (this.isInterface) { - return other.getSort() == Type.OBJECT || other.getSort() == Type.ARRAY; - } - return this.isAssignableFrom(type, this.getSuperClass(other)); + if (value1.equals(BasicValue.UNINITIALIZED_VALUE) || value2.equals(BasicValue.UNINITIALIZED_VALUE)) { + return BasicValue.UNINITIALIZED_VALUE; } - if (this.currentClass != null && other.equals(this.currentClass)) { - if (this.isAssignableFrom(type, this.currentSuperClass)) { - return true; - } - if (this.currentClassInterfaces != null) { - for (int i = 0; i < this.currentClassInterfaces.size(); ++i) { - Type v = this.currentClassInterfaces.get(i); - if (this.isAssignableFrom(type, v)) { - return true; - } + Type supertype = getCommonSupertype(value1.getType(), value2.getType()); + return newValue(supertype); + } + + private static Type getCommonSupertype(Type type1, Type type2) { + if (type1.equals(type2) || type2.equals(NULL_TYPE)) { + return type1; + } + if (type1.equals(NULL_TYPE)) { + return type2; + } + if (type1.getSort() < Type.ARRAY || type2.getSort() < Type.ARRAY) { + // We know they're not the same, so they must be incompatible. + return null; + } + if (type1.getSort() == Type.ARRAY && type2.getSort() == Type.ARRAY) { + int dim1 = type1.getDimensions(); + Type elem1 = type1.getElementType(); + int dim2 = type2.getDimensions(); + Type elem2 = type2.getElementType(); + if (dim1 == dim2) { + Type commonSupertype; + if (elem1.equals(elem2)) { + commonSupertype = elem1; + } else if (elem1.getSort() == Type.OBJECT && elem2.getSort() == Type.OBJECT) { + commonSupertype = getCommonSupertype(elem1, elem2); + } else { + return arrayType(OBJECT_TYPE, dim1 - 1); } + return arrayType(commonSupertype, dim1); } - return false; + Type smaller; + int shared; + if (dim1 < dim2) { + smaller = elem1; + shared = dim1 - 1; + } else { + smaller = elem2; + shared = dim2 - 1; + } + if (smaller.getSort() == Type.OBJECT) { + shared++; + } + return arrayType(OBJECT_TYPE, shared); } - ClassInfo typeInfo = ClassInfo.forType(type, TypeLookup.ELEMENT_TYPE); - if (typeInfo == null) { - return false; + if (type1.getSort() == Type.ARRAY && type2.getSort() == Type.OBJECT || type2.getSort() == Type.ARRAY && type1.getSort() == Type.OBJECT) { + return OBJECT_TYPE; } - if (typeInfo.isInterface()) { - typeInfo = ClassInfo.forName("java/lang/Object"); + return ClassInfo.getCommonSuperClass(type1, type2).getType(); + } + + private static Type arrayType(final Type type, final int dimensions) { + if (dimensions == 0) { + return type; + } else { + StringBuilder descriptor = new StringBuilder(); + for (int i = 0; i < dimensions; ++i) { + descriptor.append('['); + } + descriptor.append(type.getDescriptor()); + return Type.getType(descriptor.toString()); } - return ClassInfo.forType(other, TypeLookup.ELEMENT_TYPE).hasSuperClass(typeInfo); + } + + @Override + protected Class getClass(Type type) { + throw new UnsupportedOperationException( + String.format( + "Live-loading of %s attempted by MixinVerifier! This should never happen!", + type.getClassName() + ) + ); } } diff --git a/src/modlauncher/java/org/spongepowered/asm/launch/MixinLaunchPluginLegacy.java b/src/modlauncher/java/org/spongepowered/asm/launch/MixinLaunchPluginLegacy.java index 9cbbce1c4..c186846b9 100644 --- a/src/modlauncher/java/org/spongepowered/asm/launch/MixinLaunchPluginLegacy.java +++ b/src/modlauncher/java/org/spongepowered/asm/launch/MixinLaunchPluginLegacy.java @@ -204,11 +204,16 @@ protected void initializeLaunch(ITransformerLoader transformerLoader) { @Override public ClassNode getClassNode(String name) throws ClassNotFoundException, IOException { - return this.getClassNode(name, true); + return this.getClassNode(name, true, 0); } @Override public ClassNode getClassNode(String name, boolean runTransformers) throws ClassNotFoundException, IOException { + return this.getClassNode(name, runTransformers, ClassReader.EXPAND_FRAMES); + } + + @Override + public ClassNode getClassNode(String name, boolean runTransformers, int readerFlags) throws ClassNotFoundException, IOException { if (!runTransformers) { throw new IllegalArgumentException("ModLauncher service does not currently support retrieval of untransformed bytecode"); } @@ -235,7 +240,7 @@ public ClassNode getClassNode(String name, boolean runTransformers) throws Class if (classBytes != null && classBytes.length != 0) { ClassNode classNode = new ClassNode(); ClassReader classReader = new MixinClassReader(classBytes, canonicalName); - classReader.accept(classNode, ClassReader.EXPAND_FRAMES); + classReader.accept(classNode, readerFlags); return classNode; } diff --git a/src/modlauncher/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncher.java b/src/modlauncher/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncher.java index 56774262b..6742587e4 100644 --- a/src/modlauncher/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncher.java +++ b/src/modlauncher/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncher.java @@ -49,6 +49,21 @@ public Resource(String name, Path path) { this.path = path; } + @Override + public String getId() { + String name = this.name; + int lastDotPos = name.lastIndexOf('.'); + if (lastDotPos > 0) { + name = name.substring(0, lastDotPos); + } + return name; + } + + @Override + public String getDescription() { + return this.path.toAbsolutePath().toString(); + } + public String getName() { return this.name; } diff --git a/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/Blackboard.java b/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/Blackboard.java index 042c593c2..0fe7212d5 100644 --- a/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/Blackboard.java +++ b/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/Blackboard.java @@ -85,7 +85,7 @@ public T getProperty(IPropertyKey key) { */ @SuppressWarnings("unchecked") @Override - public void setProperty(IPropertyKey key, Object value) { + public void setProperty(IPropertyKey key, final Object value) { this.blackboard.computeIfAbsent(((Key)key).key, k -> value); } diff --git a/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/MixinServiceModLauncher.java b/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/MixinServiceModLauncher.java index 4f95c01c2..859421fba 100644 --- a/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/MixinServiceModLauncher.java +++ b/src/modlauncher/java/org/spongepowered/asm/service/modlauncher/MixinServiceModLauncher.java @@ -27,6 +27,7 @@ import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.Collection; +import java.util.Optional; import org.spongepowered.asm.launch.IClassProcessor; import org.spongepowered.asm.launch.platform.container.ContainerHandleModLauncher; @@ -46,7 +47,10 @@ import com.google.common.collect.ImmutableList; import cpw.mods.modlauncher.Launcher; +import cpw.mods.modlauncher.api.IEnvironment; import cpw.mods.modlauncher.api.ITransformationService; +import cpw.mods.modlauncher.api.TypesafeMap; +import org.spongepowered.asm.util.VersionNumber; /** * Mixin service for ModLauncher @@ -56,7 +60,7 @@ public class MixinServiceModLauncher extends MixinServiceAbstract { /** * Specification version to check for at startup */ - private static final String MODLAUNCHER_4_SPECIFICATION_VERSION = "4.0"; + private static final VersionNumber MODLAUNCHER_4_SPECIFICATION_VERSION = VersionNumber.parse("4.0"); /** * Specification version for ModLauncher versions >= 9.0.4, yes this is @@ -65,8 +69,8 @@ public class MixinServiceModLauncher extends MixinServiceAbstract { * version 5.0 for example, and ML7 and ML8 both had specification version * 7.0). */ - private static final String MODLAUNCHER_9_SPECIFICATION_VERSION = "8.0"; - + private static final VersionNumber MODLAUNCHER_9_SPECIFICATION_VERSION = VersionNumber.parse("8.0"); + private static final String CONTAINER_PACKAGE = MixinServiceAbstract.LAUNCH_PACKAGE + "platform.container."; private static final String MODLAUNCHER_4_ROOT_CONTAINER_CLASS = MixinServiceModLauncher.CONTAINER_PACKAGE + "ContainerHandleModLauncher"; private static final String MODLAUNCHER_9_ROOT_CONTAINER_CLASS = MixinServiceModLauncher.CONTAINER_PACKAGE + "ContainerHandleModLauncherEx"; @@ -117,15 +121,15 @@ public class MixinServiceModLauncher extends MixinServiceAbstract { private CompatibilityLevel minCompatibilityLevel = CompatibilityLevel.JAVA_8; public MixinServiceModLauncher() { - final Package pkg = ITransformationService.class.getPackage(); - if (pkg.isCompatibleWith(MixinServiceModLauncher.MODLAUNCHER_9_SPECIFICATION_VERSION)) { + VersionNumber apiVersion = MixinServiceModLauncher.getModLauncherApiVersion(); + if (apiVersion.compareTo(MODLAUNCHER_9_SPECIFICATION_VERSION) >= 0) { this.createRootContainer(MixinServiceModLauncher.MODLAUNCHER_9_ROOT_CONTAINER_CLASS); this.minCompatibilityLevel = CompatibilityLevel.JAVA_16; } else { this.createRootContainer(MixinServiceModLauncher.MODLAUNCHER_4_ROOT_CONTAINER_CLASS); } } - + /** * Begin init * @@ -200,9 +204,8 @@ protected ILogger createLogger(String name) { @Override public boolean isValid() { try { - Launcher.INSTANCE.hashCode(); - final Package pkg = ITransformationService.class.getPackage(); - if (!pkg.isCompatibleWith(MixinServiceModLauncher.MODLAUNCHER_4_SPECIFICATION_VERSION)) { + VersionNumber apiVersion = MixinServiceModLauncher.getModLauncherApiVersion(); + if (apiVersion.compareTo(MixinServiceModLauncher.MODLAUNCHER_4_SPECIFICATION_VERSION) < 0) { return false; } } catch (Throwable th) { @@ -311,4 +314,16 @@ public Collection getProcessors() { ); } + private static VersionNumber getModLauncherApiVersion() { + TypesafeMap.Key versionProperty = IEnvironment.Keys.MLSPEC_VERSION.get(); + Optional version = Launcher.INSTANCE.environment().getProperty(versionProperty); + + // Fall back to the package information (this is not present when loaded as a module) + if (!version.isPresent()) { + version = Optional.ofNullable(ITransformationService.class.getPackage().getSpecificationVersion()); + } + + return version.map(VersionNumber::parse).orElse(VersionNumber.NONE); + } + } diff --git a/src/modlauncher9/java/org/spongepowered/asm/launch/MixinTransformationService.java b/src/modlauncher9/java/org/spongepowered/asm/launch/MixinTransformationService.java index caa2cd8b5..4f2e13ec2 100644 --- a/src/modlauncher9/java/org/spongepowered/asm/launch/MixinTransformationService.java +++ b/src/modlauncher9/java/org/spongepowered/asm/launch/MixinTransformationService.java @@ -24,11 +24,79 @@ */ package org.spongepowered.asm.launch; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +import org.spongepowered.asm.mixin.injection.invoke.arg.ArgsClassGenerator; +import org.spongepowered.asm.service.MixinService; +import org.spongepowered.asm.util.Constants; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import cpw.mods.jarhandling.JarMetadata; +import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.SecureJar.Provider; +import cpw.mods.jarhandling.VirtualJar; +import cpw.mods.modlauncher.api.IModuleLayerManager; + /** - * Service for handling transforms mixin under ModLauncher, now just a concrete - * class which extends the abstract base class used for pre-9 versions of - * ModLauncher + * Service for handling transforms mixin under ModLauncher, most of the + * functionality is provided by the abstract base class used for pre-9 versions + * of ModLauncher, though we also handle SecureJarHandler requirements here, for + * modlauncher 10+ */ public class MixinTransformationService extends MixinTransformationServiceAbstract { + + private static final String VIRTUAL_JAR_CLASS = "cpw.mods.jarhandling.VirtualJar"; + + @Override + public List completeScan(final IModuleLayerManager layerManager) { + try { + Path codeSource = Path.of(this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); + + if (this.detectVirtualJar(layerManager)) { + try { + return ImmutableList.of(this.createVirtualJar(codeSource)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + try { + return ImmutableList.of(this.createShim(codeSource)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } catch (Throwable th) { + th.printStackTrace(); + return super.completeScan(layerManager); + } + } + + private boolean detectVirtualJar(final IModuleLayerManager layerManager) { + try { + MixinService.getService().getClassProvider().findClass(MixinTransformationService.VIRTUAL_JAR_CLASS, false); + return true; + } catch (ClassNotFoundException ex) { + // VirtualJar not supported + return false; + } + } + + private Resource createVirtualJar(final Path codeSource) throws URISyntaxException { + VirtualJar jar = new VirtualJar("mixin_synthetic", codeSource, Constants.SYNTHETIC_PACKAGE, ArgsClassGenerator.SYNTHETIC_PACKAGE); + return new Resource(IModuleLayerManager.Layer.GAME, ImmutableList.of(jar)); + } + + @SuppressWarnings("removal") + private Resource createShim(final Path codeSource) throws URISyntaxException { + final Path path = codeSource.resolve("mixin_synthetic"); + final Set packages = ImmutableSet.of(Constants.SYNTHETIC_PACKAGE, ArgsClassGenerator.SYNTHETIC_PACKAGE); + SecureJar jar = SecureJar.from(sj -> JarMetadata.fromFileName(path, packages, ImmutableList.of()), codeSource); + return new Resource(IModuleLayerManager.Layer.GAME, ImmutableList.of(jar)); + } } diff --git a/src/modlauncher9/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncherEx.java b/src/modlauncher9/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncherEx.java index a6553ef5d..5d4a9b45a 100644 --- a/src/modlauncher9/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncherEx.java +++ b/src/modlauncher9/java/org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncherEx.java @@ -45,6 +45,21 @@ public SecureJarResource(SecureJar resource) { this.jar = resource; } + @Override + public String getId() { + String name = this.jar.name(); + int lastDotPos = name.lastIndexOf('.'); + if (lastDotPos > 0) { + name = name.substring(0, lastDotPos); + } + return name; + } + + @Override + public String getDescription() { + return this.jar.getRootPath().toAbsolutePath().toString(); + } + public String getName() { return this.jar.name(); }