├── jitpack.yml ├── .github ├── FUNDING.yml ├── workflows │ ├── ci.yml │ └── release.yml └── dependabot.yml ├── settings.gradle ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── publishing.gradle ├── jabel-javac-plugin ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── com.sun.source.util.Plugin │ │ └── java │ │ └── com │ │ └── github │ │ └── bsideup │ │ └── jabel │ │ ├── Desugar.java │ │ ├── JabelCompilerPlugin.java │ │ └── RecordsRetrofittingTaskListener.java └── build.gradle ├── docs └── images │ └── idea-setting-language-level-inspection.png ├── .gitignore ├── example ├── src │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ ├── JDKCheck.java │ │ │ ├── JabelExampleTest.java │ │ │ └── RecordExampleTest.java │ └── main │ │ └── java │ │ └── com │ │ └── example │ │ ├── RecordExample.java │ │ └── JabelExample.java └── build.gradle ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk12 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: bsideup 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jabel' 2 | 3 | include 'jabel-javac-plugin' 4 | include 'example' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsideup/jabel/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /jabel-javac-plugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin: -------------------------------------------------------------------------------- 1 | com.github.bsideup.jabel.JabelCompilerPlugin -------------------------------------------------------------------------------- /docs/images/idea-setting-language-level-inspection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsideup/jabel/HEAD/docs/images/idea-setting-language-level-inspection.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .gradle 4 | 5 | build/ 6 | target/ 7 | 8 | .idea/ 9 | out/ 10 | *.iml 11 | *.ipr 12 | *.iws 13 | 14 | 15 | .project 16 | .classpath 17 | .settings/ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/JDKCheck.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.Test; 4 | 5 | public class JDKCheck { 6 | 7 | @Test(expected = ClassNotFoundException.class) 8 | public void isJava8() throws Exception { 9 | Class.forName("java.lang.StackWalker"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Set up JDK 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 16 15 | - name: Build with Gradle 16 | run: ./gradlew build 17 | -------------------------------------------------------------------------------- /jabel-javac-plugin/src/main/java/com/github/bsideup/jabel/Desugar.java: -------------------------------------------------------------------------------- 1 | package com.github.bsideup.jabel; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import static java.lang.annotation.ElementType.TYPE; 9 | 10 | @Documented 11 | @Retention(RetentionPolicy.SOURCE) 12 | @Target(value=TYPE) 13 | public @interface Desugar { 14 | } 15 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/JabelExampleTest.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class JabelExampleTest { 8 | 9 | @Test 10 | public void shouldWork() { 11 | JabelExample jabelExample = new JabelExample(); 12 | 13 | String result = jabelExample.run(new String[0]); 14 | 15 | assertTrue("'" + result + "' should start with 'idk '", result.startsWith("idk ")); 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /example/src/main/java/com/example/RecordExample.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import com.github.bsideup.jabel.Desugar; 4 | 5 | @Desugar 6 | public record RecordExample(int i, String s, long l, float f, double d, String[] arr, boolean b) { 7 | 8 | public static RecordExample DUMMY = new RecordExample(0, null, 0, 0, 0, null, true); 9 | 10 | public RecordExample { 11 | if (i > 1_000_000) { 12 | throw new IllegalArgumentException("'i' is too big"); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "07:00" 8 | timezone: Europe/Berlin 9 | open-pull-requests-limit: 99 10 | ignore: 11 | - dependency-name: net.bytebuddy:byte-buddy-agent 12 | versions: 13 | - 1.10.19 14 | - 1.10.20 15 | - dependency-name: net.bytebuddy:byte-buddy 16 | versions: 17 | - 1.10.19 18 | - 1.10.20 19 | - dependency-name: junit:junit 20 | versions: 21 | - 4.13.1 22 | rebase-strategy: disabled 23 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | } 4 | 5 | configure([tasks.compileJava]) { 6 | sourceCompatibility = 16 7 | options.release = 8 8 | 9 | javaCompiler = javaToolchains.compilerFor { 10 | languageVersion = JavaLanguageVersion.of(21) 11 | } 12 | } 13 | 14 | compileTestJava { 15 | sourceCompatibility = targetCompatibility = 8 16 | } 17 | 18 | test { 19 | javaLauncher = javaToolchains.launcherFor { 20 | languageVersion = JavaLanguageVersion.of(8) 21 | } 22 | } 23 | 24 | dependencies { 25 | annotationProcessor project(":jabel-javac-plugin") 26 | compileOnly project(":jabel-javac-plugin") 27 | 28 | testImplementation 'junit:junit:4.13.2' 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - prereleased 7 | - released 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Set up JDK 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: 14 18 | - name: Deploy with Gradle 19 | env: 20 | GRADLE_PUBLISH_REPO_URL: ${{ secrets.GRADLE_PUBLISH_REPO_URL }} 21 | GRADLE_PUBLISH_MAVEN_USER: ${{ secrets.GRADLE_PUBLISH_MAVEN_USER }} 22 | GRADLE_PUBLISH_MAVEN_PASSWORD: ${{ secrets.GRADLE_PUBLISH_MAVEN_PASSWORD }} 23 | run: ./gradlew --no-daemon -Pversion=$(git tag --points-at HEAD) publish -------------------------------------------------------------------------------- /jabel-javac-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "maven-publish" 4 | } 5 | 6 | sourceCompatibility = targetCompatibility = 8 7 | 8 | dependencies { 9 | implementation platform('net.bytebuddy:byte-buddy-parent:1.14.9') 10 | implementation 'net.bytebuddy:byte-buddy' 11 | implementation 'net.bytebuddy:byte-buddy-agent' 12 | implementation 'net.java.dev.jna:jna:5.13.0' 13 | } 14 | 15 | 16 | task sourcesJar(type: Jar) { 17 | classifier 'sources' 18 | from sourceSets.main.allJava 19 | } 20 | 21 | javadoc { 22 | options.source = "8" 23 | } 24 | 25 | task javadocJar(type: Jar) { 26 | from javadoc 27 | classifier = 'javadoc' 28 | } 29 | 30 | publishing { 31 | publications { 32 | mavenJava(MavenPublication) { publication -> 33 | from components.java 34 | artifact sourcesJar 35 | artifact javadocJar 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gradle/publishing.gradle: -------------------------------------------------------------------------------- 1 | plugins.withType(MavenPublishPlugin) { 2 | project.publishing { 3 | publications { 4 | mavenJava(MavenPublication) { publication -> 5 | pom { 6 | description = 'Jabel - use modern Java 9-14 syntax when targeting Java 8.' 7 | name = project.description ?: description 8 | url = 'https://github.com/bsideup/jabel' 9 | licenses { 10 | license { 11 | name = 'Apache License, Version 2.0' 12 | url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' 13 | distribution = 'repo' 14 | } 15 | } 16 | scm { 17 | url = 'https://github.com/bsideup/jabel/' 18 | connection = 'scm:git:git://github.com/bsideup/jabel.git' 19 | developerConnection = 'scm:git:ssh://git@github.com/bsideup/jabel.git' 20 | } 21 | developers { 22 | developer { 23 | id = 'bsideup' 24 | name = 'Sergei Egorov' 25 | email = 'bsideup@gmail.com' 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /example/src/main/java/com/example/JabelExample.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.Callable; 5 | import java.util.function.Function; 6 | 7 | public class JabelExample { 8 | 9 | public static void main(String[] args) { 10 | System.out.println(new JabelExample().run(args)); 11 | } 12 | 13 | public String run(String[] args) { 14 | // Switch Expressions 15 | // https://openjdk.java.net/jeps/325 16 | var result = switch (args.length) { 17 | case 1 -> { 18 | yield """ 19 | one... 20 | yet pretty long! 21 | """; 22 | } 23 | case 2, 3 -> "two or three"; 24 | default -> new JabelExample().new Inner().innerPublic(); 25 | }; 26 | return result; 27 | } 28 | 29 | // Project Coin: Allow @SafeVarargs on private methods 30 | // https://bugs.openjdk.java.net/browse/JDK-7196160 31 | @SafeVarargs 32 | private String outerPrivate(List... args) { 33 | // Look, Ma! No explicit diamond parameter 34 | var callable = new Callable<>() { 35 | 36 | @Override 37 | public String call() { 38 | // Var in lambda parameter 39 | Function function = (var prefix) -> { 40 | // Pattern Matching in instanceof 41 | // https://openjdk.java.net/jeps/305 42 | if (prefix instanceof String s) { 43 | return s + Integer.toString(0); 44 | } else { 45 | throw new IllegalArgumentException("Expected string!"); 46 | } 47 | }; 48 | // Test indy strings 49 | return function.apply("idk "); 50 | } 51 | }; 52 | 53 | var closeable = new AutoCloseable() { 54 | 55 | @Override 56 | public void close() { 57 | 58 | } 59 | }; 60 | 61 | // Project Coin: Allow final or effectively final variables to be used as resources in try-with-resources 62 | // https://bugs.openjdk.java.net/browse/JDK-7196163 63 | try (closeable) { 64 | return callable.call(); 65 | } 66 | } 67 | 68 | class Inner { 69 | 70 | static class WhyNot { 71 | 72 | } 73 | 74 | public static void staticMethodInsideInner() { 75 | enum LocalEnum { } 76 | System.out.println(LocalEnum.values()); 77 | } 78 | 79 | public String innerPublic() { 80 | // Test nest-mate 81 | return outerPrivate(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /example/src/test/java/com/example/RecordExampleTest.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Arrays; 6 | import java.util.Objects; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertNotEquals; 10 | 11 | public class RecordExampleTest { 12 | 13 | @Test(expected = IllegalArgumentException.class) 14 | public void testCtor() { 15 | int i = 1_000_000 + 1; 16 | new RecordExample(i, "yeah", 100500, 0.5f, 5d, new String[]{"Hello", "World!"}, true); 17 | } 18 | 19 | @Test 20 | public void testToString() { 21 | RecordExample r = new RecordExample(42, "yeah", 100500, 0.5f, 5d, new String[]{"Hello", "World!"}, true); 22 | 23 | assertEquals( 24 | "RecordExample[i=42,s=yeah,l=100500,f=0.5,d=5.0,arr=[Ljava.lang.String;@hash,b=true]", 25 | Objects.toString(r).replaceAll(";@[a-f0-9]+", ";@hash") 26 | ); 27 | } 28 | 29 | @Test 30 | public void testToStringWithNulls() { 31 | RecordExample r = new RecordExample(42, null, 100500, 0.5f, 5d, null, true); 32 | 33 | assertEquals( 34 | "RecordExample[i=42,s=null,l=100500,f=0.5,d=5.0,arr=null,b=true]", 35 | Objects.toString(r) 36 | ); 37 | } 38 | 39 | @Test 40 | public void testEqualsSame() { 41 | RecordExample r = new RecordExample(42, null, 100500, 0.5f, 5d, null, true); 42 | assertEquals(r, r); 43 | } 44 | 45 | @Test 46 | public void testEqualsSameValues() { 47 | String s = "So cool!"; 48 | String[] arr = new String[] { "Hello", "World!"}; 49 | assertEquals( 50 | new RecordExample(42, s, 100500, 0.5f, 5d, arr, true), 51 | new RecordExample(42, s, 100500, 0.5f, 5d, arr, true) 52 | ); 53 | } 54 | 55 | @Test 56 | public void testNotEquals() { 57 | assertNotEquals( 58 | new RecordExample(42, "l", 100500, 0.5f, 5d, new String[] { "Hello", "World!"}, true), 59 | new RecordExample(42, "l", 100500, 0.5f, 5d, new String[] { "Hello", "World!"}, true) 60 | ); 61 | } 62 | 63 | @Test 64 | public void testHashCodeWithDefaults() { 65 | assertEquals( 66 | intellijStyleHashCode(RecordExample.DUMMY), 67 | Objects.hashCode(RecordExample.DUMMY) 68 | ); 69 | } 70 | 71 | @Test 72 | public void testHashCode() { 73 | RecordExample r = new RecordExample(42, "Hi", 100500, 0.5f, 5d, new String[]{"Hello", "World!"}, true); 74 | assertEquals( 75 | intellijStyleHashCode(r), 76 | Objects.hashCode(r) 77 | ); 78 | } 79 | 80 | static int intellijStyleHashCode(RecordExample r) { 81 | int result = r.i(); 82 | result = 31 * result + (r.s() != null ? r.s().hashCode() : 0); 83 | result = 31 * result + (int) (r.l() ^ (r.l() >>> 32)); 84 | result = 31 * result + (r.f() != +0.0f ? Float.floatToIntBits(r.f()) : 0); 85 | 86 | long temp = Double.doubleToLongBits(r.d()); 87 | result = 31 * result + (int) (temp ^ (temp >>> 32)); 88 | result = 31 * result + Arrays.hashCode(r.arr()); 89 | result = 31 * result + (r.b() ? 1 : 0); 90 | return result; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jabel - use modern Java 9-14 syntax when targeting Java 8 2 | 3 | > Because life is too short to wait for your users to upgrade their Java! 4 | 5 | 6 | ## Motivation 7 | 8 | While Java is evolving and introduces new language features, the majority of OSS libraries 9 | are still using Java 8 as their target because it still dominates. 10 | 11 | But, since most of features after Java 8 did not require a change in the bytecode, 12 | `javac` could emit Java 8 bytecode even when compiling Java 12 sources. 13 | 14 | ## How Jabel works 15 | 16 | Although Jabel is a javac compiler plugin, it does not run any processing, 17 | but instruments the java compiler classes and makes it treat some new Java 9+ languages features 18 | as they were supported in Java 8. 19 | 20 | The result is a valid Java 8 bytecode for your switch expressions, `var` declarations, 21 | and other features unavailable in Java 8. 22 | 23 | ## Why it works 24 | 25 | The JVM has evolved a lot for the past years. However, most language features 26 | that were added are simply a syntatic sugar. 27 | They do not require new bytecode, hence can be compiled to the Java 8. 28 | 29 | But, since the Java language was always bound to the JVM development, new language features 30 | require the same target as the JVM because they get released altogether. 31 | 32 | As was previously described, Jabel makes the compiler think that certain features were developed 33 | for Java 8, and removes the checks that otherwise will report them as invalid for the target. 34 | 35 | It is important to understand that it will use the same desugaring code as for Java 9+ but won't change 36 | the result's classfile version, because the compilation phase will be done with Java 8 target. 37 | 38 | ## How to use 39 | 40 | ### Maven 41 | Jabel has to be enabled as a Javac plugin in your maven-compiler-plugin: 42 | ```xml 43 | 44 | 45 | intellij-idea-only 46 | 47 | 48 | idea.maven.embedder.version 49 | 50 | 51 | 52 | 53 | 54 | org.apache.maven.plugins 55 | maven-compiler-plugin 56 | 57 | 14 58 | 59 | --enable-preview 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-compiler-plugin 73 | 3.8.1 74 | 75 | 76 | 8 77 | 14 78 | 14 79 | 80 | 81 | -Xplugin:jabel 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | com.github.bsideup.jabel 91 | jabel-javac-plugin 92 | 0.4.1 93 | provided 94 | 95 | 96 | ``` 97 | 98 | Compile your project and verify that Jabel is installed and successfully reports: 99 | ``` 100 | [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ tester.thirteen --- 101 | [INFO] Changes detected - recompiling the module! 102 | Jabel: initialized. 103 | ``` 104 | 105 | ### Gradle 6 or older 106 | Use the following snippet to add Jabel to your Gradle build: 107 | ```gradle 108 | dependencies { 109 | annotationProcessor 'com.github.bsideup.jabel:jabel-javac-plugin:0.4.2' 110 | } 111 | 112 | // Add more tasks if needed, such as compileTestJava 113 | configure([tasks.compileJava]) { 114 | sourceCompatibility = 14 // for the IDE support 115 | 116 | options.compilerArgs = [ 117 | "--release", "8", 118 | '--enable-preview', 119 | ] 120 | 121 | doFirst { 122 | // Can be omitted on Java 14 and higher 123 | options.compilerArgs << '-Xplugin:jabel' 124 | 125 | options.compilerArgs = options.compilerArgs.findAll { 126 | it != '--enable-preview' 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | Compile your project and verify that the result is still a valid Java 8 bytecode (52.0): 133 | ```shell script 134 | $ ./gradlew --no-daemon clean :example:test 135 | 136 | > Task :example:compileJava 137 | Jabel: initialized. 138 | 139 | 140 | BUILD SUCCESSFUL in 6s 141 | 8 actionable tasks: 8 executed 142 | 143 | $ javap -v example/build/classes/java/main/com/example/JabelExample.class 144 | Classfile /Users/bsideup/Work/bsideup/jabel/example/build/classes/java/main/com/example/JabelExample.class 145 | Last modified 31 Aug 2019; size 1463 bytes 146 | MD5 checksum d98fb6c3bc1b4046fe745983340b7295 147 | Compiled from "JabelExample.java" 148 | public class com.example.JabelExample 149 | minor version: 0 150 | major version: 52 151 | ``` 152 | 153 | ### Gradle 7 and newer 154 | Gradle 7 supports toolchains and makes it extremely easy to configure everything: 155 | ```gradle 156 | dependencies { 157 | annotationProcessor 'com.github.bsideup.jabel:jabel-javac-plugin:0.4.2' 158 | compileOnly 'com.github.bsideup.jabel:jabel-javac-plugin:0.4.2' 159 | } 160 | 161 | configure([tasks.compileJava]) { 162 | sourceCompatibility = 16 // for the IDE support 163 | options.release = 8 164 | 165 | javaCompiler = javaToolchains.compilerFor { 166 | languageVersion = JavaLanguageVersion.of(16) 167 | } 168 | } 169 | ``` 170 | (Java 16 does not require the preview flag for any language feature supported by Jabel) 171 | 172 | You can also force your tests to run with Java 8: 173 | ```gradle 174 | compileTestJava { 175 | sourceCompatibility = targetCompatibility = 8 176 | } 177 | 178 | test { 179 | javaLauncher = javaToolchains.launcherFor { 180 | languageVersion = JavaLanguageVersion.of(8) 181 | } 182 | } 183 | ``` 184 | 185 | ## IDE support 186 | 187 | ### IntelliJ IDEA 188 | #### How to avoid using Java 9+ APIs in IntelliJ IDEA 189 | If you set `--release=8` flag, the compiler will report usages of APIs that were not in Java 8 (e.g. `StackWalker`). But if you wish to see such usages while editing the code, you can make IDEA highlight them for you: 190 | 191 | * On the bottom right click on the head with the hat 192 | * Click on "Configure inspections" 193 | * Find "Usages of API which isn't available at the configured language level" 194 | * Click "Higher than", and select "8 - Lambdas, type annotations etc." from dropdown 195 | 196 | ![IntelliJ IDEA Language Level Inspection](docs/images/idea-setting-language-level-inspection.png) 197 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /jabel-javac-plugin/src/main/java/com/github/bsideup/jabel/JabelCompilerPlugin.java: -------------------------------------------------------------------------------- 1 | package com.github.bsideup.jabel; 2 | 3 | import com.sun.source.util.JavacTask; 4 | import com.sun.source.util.Plugin; 5 | import com.sun.tools.javac.api.BasicJavacTask; 6 | import com.sun.tools.javac.code.Source; 7 | import com.sun.tools.javac.util.Context; 8 | import com.sun.tools.javac.util.JavacMessages; 9 | import net.bytebuddy.ByteBuddy; 10 | import net.bytebuddy.agent.ByteBuddyAgent; 11 | import net.bytebuddy.asm.Advice; 12 | import net.bytebuddy.asm.AsmVisitorWrapper; 13 | import net.bytebuddy.description.field.FieldDescription; 14 | import net.bytebuddy.description.field.FieldList; 15 | import net.bytebuddy.description.method.MethodList; 16 | import net.bytebuddy.description.type.TypeDescription; 17 | import net.bytebuddy.dynamic.ClassFileLocator; 18 | import net.bytebuddy.dynamic.loading.ClassInjector; 19 | import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; 20 | import net.bytebuddy.dynamic.scaffold.MethodGraph; 21 | import net.bytebuddy.implementation.Implementation; 22 | import net.bytebuddy.jar.asm.ClassVisitor; 23 | import net.bytebuddy.jar.asm.MethodVisitor; 24 | import net.bytebuddy.jar.asm.Opcodes; 25 | import net.bytebuddy.pool.TypePool; 26 | import net.bytebuddy.utility.JavaModule; 27 | 28 | import java.util.*; 29 | 30 | import static net.bytebuddy.matcher.ElementMatchers.*; 31 | 32 | public class JabelCompilerPlugin implements Plugin { 33 | static { 34 | Map visitors = new HashMap() {{ 35 | // Disable the preview feature check 36 | AsmVisitorWrapper checkSourceLevelAdvice = Advice.to(CheckSourceLevelAdvice.class) 37 | .on(named("checkSourceLevel").and(takesArguments(2))); 38 | 39 | // Allow features that were introduced together with Records (local enums, static inner members, ...) 40 | AsmVisitorWrapper allowRecordsEraFeaturesAdvice = new FieldAccessStub("allowRecords", true); 41 | 42 | put("com.sun.tools.javac.parser.JavacParser", 43 | new AsmVisitorWrapper.Compound( 44 | checkSourceLevelAdvice, 45 | allowRecordsEraFeaturesAdvice 46 | ) 47 | ); 48 | put("com.sun.tools.javac.parser.JavaTokenizer", checkSourceLevelAdvice); 49 | 50 | put("com.sun.tools.javac.comp.Check", allowRecordsEraFeaturesAdvice); 51 | put("com.sun.tools.javac.comp.Attr", allowRecordsEraFeaturesAdvice); 52 | put("com.sun.tools.javac.comp.Resolve", allowRecordsEraFeaturesAdvice); 53 | 54 | // Lower the source requirement for supported features 55 | put( 56 | "com.sun.tools.javac.code.Source$Feature", 57 | Advice.to(AllowedInSourceAdvice.class) 58 | .on(named("allowedInSource").and(takesArguments(1))) 59 | ); 60 | }}; 61 | 62 | try { 63 | ByteBuddyAgent.install(); 64 | } catch (Exception e) { 65 | ByteBuddyAgent.install( 66 | new ByteBuddyAgent.AttachmentProvider.Compound( 67 | ByteBuddyAgent.AttachmentProvider.ForJ9Vm.INSTANCE, 68 | ByteBuddyAgent.AttachmentProvider.ForStandardToolsJarVm.JVM_ROOT, 69 | ByteBuddyAgent.AttachmentProvider.ForStandardToolsJarVm.JDK_ROOT, 70 | ByteBuddyAgent.AttachmentProvider.ForStandardToolsJarVm.MACINTOSH, 71 | ByteBuddyAgent.AttachmentProvider.ForUserDefinedToolsJar.INSTANCE, 72 | ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE 73 | ) 74 | ); 75 | } 76 | 77 | ByteBuddy byteBuddy = new ByteBuddy() 78 | .with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE); 79 | 80 | ClassLoader classLoader = JavacTask.class.getClassLoader(); 81 | ClassFileLocator classFileLocator = ClassFileLocator.ForClassLoader.of(classLoader); 82 | TypePool typePool = TypePool.ClassLoading.of(classLoader); 83 | 84 | visitors.forEach((className, visitor) -> { 85 | byteBuddy 86 | .decorate( 87 | typePool.describe(className).resolve(), 88 | classFileLocator 89 | ) 90 | .visit(visitor) 91 | .make() 92 | .load(classLoader, ClassReloadingStrategy.fromInstalledAgent()); 93 | }); 94 | 95 | JavaModule jabelModule = JavaModule.ofType(JabelCompilerPlugin.class); 96 | ClassInjector.UsingInstrumentation.redefineModule( 97 | ByteBuddyAgent.getInstrumentation(), 98 | JavaModule.ofType(JavacTask.class), 99 | Collections.emptySet(), 100 | Collections.emptyMap(), 101 | new HashMap>() {{ 102 | put("com.sun.tools.javac.api", Collections.singleton(jabelModule)); 103 | put("com.sun.tools.javac.tree", Collections.singleton(jabelModule)); 104 | put("com.sun.tools.javac.code", Collections.singleton(jabelModule)); 105 | put("com.sun.tools.javac.util", Collections.singleton(jabelModule)); 106 | }}, 107 | Collections.emptySet(), 108 | Collections.emptyMap() 109 | ); 110 | } 111 | 112 | @Override 113 | public void init(JavacTask task, String... args) { 114 | Context context = ((BasicJavacTask) task).getContext(); 115 | JavacMessages.instance(context).add(locale -> new ResourceBundle() { 116 | @Override 117 | protected Object handleGetObject(String key) { 118 | return "{0}"; 119 | } 120 | 121 | @Override 122 | public Enumeration getKeys() { 123 | return Collections.enumeration(Arrays.asList("missing.desugar.on.record")); 124 | } 125 | }); 126 | 127 | task.addTaskListener(new RecordsRetrofittingTaskListener(context)); 128 | 129 | System.out.println("Jabel: initialized"); 130 | } 131 | 132 | @Override 133 | public String getName() { 134 | return "jabel"; 135 | } 136 | 137 | // Make it auto start on Java 14+ 138 | public boolean autoStart() { 139 | return true; 140 | } 141 | 142 | static class AllowedInSourceAdvice { 143 | 144 | @Advice.OnMethodEnter 145 | static void allowedInSource( 146 | @Advice.This Source.Feature feature, 147 | @Advice.Argument(value = 0, readOnly = false) Source source 148 | ) { 149 | switch (feature.name()) { 150 | case "PRIVATE_SAFE_VARARGS": 151 | case "SWITCH_EXPRESSION": 152 | case "SWITCH_RULE": 153 | case "SWITCH_MULTIPLE_CASE_LABELS": 154 | case "LOCAL_VARIABLE_TYPE_INFERENCE": 155 | case "VAR_SYNTAX_IMPLICIT_LAMBDAS": 156 | case "DIAMOND_WITH_ANONYMOUS_CLASS_CREATION": 157 | case "EFFECTIVELY_FINAL_VARIABLES_IN_TRY_WITH_RESOURCES": 158 | case "TEXT_BLOCKS": 159 | case "PATTERN_MATCHING_IN_INSTANCEOF": 160 | case "REIFIABLE_TYPES_INSTANCEOF": 161 | case "RECORDS": 162 | //noinspection UnusedAssignment 163 | source = Source.DEFAULT; 164 | break; 165 | } 166 | } 167 | } 168 | 169 | static class CheckSourceLevelAdvice { 170 | 171 | @Advice.OnMethodEnter 172 | static void checkSourceLevel( 173 | @Advice.Argument(value = 1, readOnly = false) Source.Feature feature 174 | ) { 175 | if (feature.allowedInSource(Source.JDK8)) { 176 | // This must be one of the cases from "AllowedInSourceAdvice" 177 | //noinspection UnusedAssignment 178 | feature = Source.Feature.PRIVATE_SAFE_VARARGS; 179 | } 180 | } 181 | } 182 | 183 | private static class FieldAccessStub extends AsmVisitorWrapper.AbstractBase { 184 | 185 | final String fieldName; 186 | 187 | final Object value; 188 | 189 | public FieldAccessStub(String fieldName, Object value) { 190 | this.fieldName = fieldName; 191 | this.value = value; 192 | } 193 | 194 | @Override 195 | public ClassVisitor wrap(TypeDescription instrumentedType, ClassVisitor classVisitor, Implementation.Context implementationContext, TypePool typePool, FieldList fields, MethodList methods, int writerFlags, int readerFlags) { 196 | return new ClassVisitor(Opcodes.ASM9, classVisitor) { 197 | @Override 198 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 199 | MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); 200 | return new MethodVisitor(Opcodes.ASM9, methodVisitor) { 201 | @Override 202 | public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { 203 | if (opcode == Opcodes.GETFIELD && fieldName.equalsIgnoreCase(name)) { 204 | super.visitInsn(Opcodes.POP); 205 | super.visitLdcInsn(value); 206 | } else { 207 | super.visitFieldInsn(opcode, owner, name, descriptor); 208 | } 209 | } 210 | }; 211 | } 212 | }; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /jabel-javac-plugin/src/main/java/com/github/bsideup/jabel/RecordsRetrofittingTaskListener.java: -------------------------------------------------------------------------------- 1 | package com.github.bsideup.jabel; 2 | 3 | import com.sun.source.tree.ClassTree; 4 | import com.sun.source.tree.CompilationUnitTree; 5 | import com.sun.source.util.TaskEvent; 6 | import com.sun.source.util.TaskListener; 7 | import com.sun.source.util.TreeScanner; 8 | import com.sun.tools.javac.code.*; 9 | import com.sun.tools.javac.tree.JCTree; 10 | import com.sun.tools.javac.tree.TreeMaker; 11 | import com.sun.tools.javac.util.*; 12 | 13 | import javax.lang.model.element.Modifier; 14 | import javax.tools.JavaFileObject; 15 | import java.util.Iterator; 16 | import java.util.stream.Stream; 17 | 18 | class RecordsRetrofittingTaskListener implements TaskListener { 19 | 20 | final TreeMaker make; 21 | 22 | final Symtab syms; 23 | 24 | final Names names; 25 | 26 | final Log log; 27 | 28 | TreeScanner recordsScanner = new TreeScanner() { 29 | @Override 30 | public Void visitClass(ClassTree node, Void aVoid) { 31 | if (!"RECORD".equals(node.getKind().toString())) { 32 | return super.visitClass(node, aVoid); 33 | } 34 | 35 | JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) node; 36 | 37 | if (classDecl.extending == null) { 38 | // Prevent implicit "extends java.lang.Record" 39 | classDecl.extending = make.Type(syms.objectType); 40 | } 41 | 42 | { 43 | Name methodName = names.toString; 44 | List argTypes = List.nil(); 45 | if (!containsMethod(classDecl, methodName)) { 46 | JCTree.JCMethodDecl methodDecl = make.MethodDef( 47 | new Symbol.MethodSymbol( 48 | Flags.PUBLIC, 49 | methodName, 50 | new Type.MethodType( 51 | argTypes, 52 | syms.stringType, 53 | List.nil(), 54 | syms.methodClass 55 | ), 56 | syms.objectType.tsym 57 | ), 58 | make.Block(0, generateToString(classDecl)) 59 | ); 60 | classDecl.defs = classDecl.defs.append(methodDecl); 61 | } 62 | } 63 | 64 | { 65 | Name methodName = names.hashCode; 66 | List argTypes = List.nil(); 67 | if (!containsMethod(classDecl, methodName)) { 68 | classDecl.defs = classDecl.defs.append(make.MethodDef( 69 | new Symbol.MethodSymbol( 70 | Flags.PUBLIC, 71 | methodName, 72 | new Type.MethodType( 73 | argTypes, 74 | syms.intType, 75 | List.nil(), 76 | syms.methodClass 77 | ), 78 | syms.objectType.tsym 79 | ), 80 | make.Block(0, generateHashCode(classDecl)) 81 | )); 82 | } 83 | } 84 | 85 | { 86 | Name methodName = names.equals; 87 | List argTypes = List.of(syms.objectType); 88 | if (!containsMethod(classDecl, methodName)) { 89 | Symbol.MethodSymbol methodSymbol = new Symbol.MethodSymbol( 90 | Flags.PUBLIC | Flags.FINAL, 91 | methodName, 92 | new Type.MethodType( 93 | argTypes, 94 | syms.booleanType, 95 | List.nil(), 96 | syms.methodClass 97 | ), 98 | syms.objectType.tsym 99 | ); 100 | Symbol.VarSymbol firstParameter = methodSymbol.params().head; 101 | 102 | JCTree.JCMethodDecl methodDecl = make.MethodDef( 103 | methodSymbol, 104 | make.Block(0, generateEquals(classDecl, firstParameter.name)) 105 | ); 106 | // THIS ONE IS IMPORTANT! Otherwise, Flow.AssignAnalyzer#visitVarDef will have track=false 107 | methodDecl.params.head.pos = classDecl.pos; 108 | classDecl.defs = classDecl.defs.append(methodDecl); 109 | } 110 | } 111 | return super.visitClass(node, aVoid); 112 | } 113 | 114 | private boolean containsMethod(JCTree.JCClassDecl classDecl, Name name) { 115 | return classDecl.defs.stream() 116 | .filter(JCTree.JCMethodDecl.class::isInstance) 117 | .map(JCTree.JCMethodDecl.class::cast) 118 | .anyMatch(def -> { 119 | if (def.getName() != name) { 120 | return false; 121 | } 122 | 123 | if (name == names.equals) { 124 | if (def.params.size() != 1) { 125 | return false; 126 | } 127 | 128 | // TODO find a better way? 129 | JCTree.JCVariableDecl param = def.params.get(0); 130 | switch (param.getType().toString()) { 131 | case "java.lang.Object": 132 | case "Object": 133 | return true; 134 | default: 135 | return false; 136 | } 137 | } 138 | 139 | return true; 140 | }); 141 | } 142 | }; 143 | 144 | public RecordsRetrofittingTaskListener(Context context) { 145 | make = TreeMaker.instance(context); 146 | syms = Symtab.instance(context); 147 | names = Names.instance(context); 148 | log = Log.instance(context); 149 | } 150 | 151 | @Override 152 | public void started(TaskEvent e) { 153 | switch (e.getKind()) { 154 | case ENTER: 155 | recordsScanner.scan(e.getCompilationUnit(), null); 156 | new TreeScanner() { 157 | @Override 158 | public Void visitClass(ClassTree node, Void aVoid) { 159 | if ("RECORD".equals(node.getKind().toString())) { 160 | JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) node; 161 | 162 | if (classDecl.extending == null) { 163 | // Prevent implicit "extends java.lang.Record" 164 | classDecl.extending = make.Type(syms.objectType); 165 | } 166 | } 167 | return super.visitClass(node, aVoid); 168 | } 169 | }.scan(e.getCompilationUnit(), null); 170 | break; 171 | case ANALYZE: 172 | new MandatoryDesugarAnnotationTreeScanner(log, e.getCompilationUnit()).scan(e.getCompilationUnit(), null); 173 | } 174 | } 175 | 176 | @Override 177 | public void finished(TaskEvent e) { 178 | } 179 | 180 | private Stream getRecordComponents(JCTree.JCClassDecl classDecl) { 181 | return classDecl.getMembers().stream() 182 | .filter(JCTree.JCVariableDecl.class::isInstance) 183 | .map(JCTree.JCVariableDecl.class::cast) 184 | .filter(it -> !it.getModifiers().getFlags().contains(Modifier.STATIC)); 185 | } 186 | 187 | private List generateToString(JCTree.JCClassDecl classDecl) { 188 | JCTree.JCExpression stringBuilder = make.NewClass( 189 | null, 190 | null, 191 | make.QualIdent(syms.stringBuilderType.tsym), 192 | List.of(make.Literal(classDecl.name + "[")), 193 | null 194 | ); 195 | 196 | for ( 197 | Iterator iterator = getRecordComponents(classDecl).iterator(); 198 | iterator.hasNext(); 199 | ) { 200 | JCTree.JCVariableDecl fieldDecl = iterator.next(); 201 | Name fieldName = fieldDecl.name; 202 | 203 | stringBuilder = make.App( 204 | make.Select(stringBuilder, names.append).setType(syms.stringBuilderType), 205 | List.of(make.Literal(fieldName + "=")) 206 | ); 207 | 208 | stringBuilder = make.App( 209 | make.Select(stringBuilder, names.append).setType(syms.stringBuilderType), 210 | List.of( 211 | make.Select( 212 | make.This(Type.noType), 213 | fieldName 214 | ) 215 | ) 216 | ); 217 | 218 | if (iterator.hasNext()) { 219 | stringBuilder = make.App( 220 | make.Select(stringBuilder, names.append).setType(syms.stringBuilderType), 221 | List.of(make.Literal(",")) 222 | ); 223 | } 224 | } 225 | 226 | stringBuilder = make.App( 227 | make.Select(stringBuilder, names.append).setType(syms.stringBuilderType), 228 | List.of(make.Literal("]")) 229 | ); 230 | 231 | return List.of(make.Return( 232 | make.App( 233 | make.Select(stringBuilder, names.toString).setType(syms.stringType) 234 | ) 235 | )); 236 | } 237 | 238 | private List generateEquals(JCTree.JCClassDecl classDecl, Name otherName) { 239 | ListBuffer statements = new ListBuffer<>(); 240 | 241 | // if (o == this) return true; 242 | { 243 | statements.add(make.If( 244 | make.Binary( 245 | JCTree.Tag.EQ, 246 | make.This(Type.noType), 247 | make.Ident(otherName) 248 | ), 249 | make.Return(make.Literal(true)), 250 | null 251 | )); 252 | } 253 | 254 | // if (o == null) return false; 255 | { 256 | statements.add(make.If( 257 | make.Binary( 258 | JCTree.Tag.EQ, 259 | make.Ident(otherName), 260 | make.Literal(TypeTag.BOT, null) 261 | ), 262 | make.Return(make.Literal(false)), 263 | null 264 | )); 265 | } 266 | 267 | // if (o.getClass() != getClass()) return false; 268 | { 269 | statements.add(make.If( 270 | make.Binary( 271 | JCTree.Tag.EQ, 272 | make.App(make.Select(make.Ident(otherName), names.getClass).setType(syms.classType)), 273 | make.App(make.Select(make.This(Type.noType), names.getClass).setType(syms.classType)) 274 | ), 275 | make.Block(0, List.nil()), 276 | make.Return(make.Literal(false)) 277 | )); 278 | } 279 | 280 | // fields 281 | { 282 | for ( 283 | Iterator iterator = getRecordComponents(classDecl).iterator(); 284 | iterator.hasNext(); 285 | ) { 286 | JCTree.JCVariableDecl fieldDecl = iterator.next(); 287 | 288 | JCTree.JCExpression myFieldAccess = make.Select(make.This(Type.noType), fieldDecl.name); 289 | JCTree.JCExpression otherFieldAccess = make.Select( 290 | make.TypeCast(make.Ident(classDecl.name), make.Ident(otherName)), 291 | fieldDecl.name 292 | ); 293 | 294 | final JCTree.JCExpression condition; 295 | if (fieldDecl.getType() instanceof JCTree.JCPrimitiveTypeTree) { 296 | condition = make.Binary(JCTree.Tag.EQ, otherFieldAccess, myFieldAccess); 297 | } else { 298 | condition = make.App( 299 | // call Objects.equals 300 | make.Select( 301 | make.QualIdent(syms.objectsType.tsym), 302 | names.equals 303 | ).setType(syms.objectsType), 304 | List.of(otherFieldAccess, myFieldAccess) 305 | ); 306 | } 307 | statements.add(make.If( 308 | condition, 309 | make.Block(0, List.nil()), 310 | make.Return(make.Literal(false)) 311 | )); 312 | } 313 | } 314 | 315 | statements.add(make.Return(make.Literal(true))); 316 | return statements.toList(); 317 | } 318 | 319 | private List generateHashCode(JCTree.JCClassDecl classDecl) { 320 | ListBuffer expressions = new ListBuffer<>(); 321 | 322 | for ( 323 | Iterator iterator = getRecordComponents(classDecl).iterator(); 324 | iterator.hasNext(); 325 | ) { 326 | JCTree.JCVariableDecl fieldDecl = iterator.next(); 327 | 328 | JCTree fType = fieldDecl.getType(); 329 | 330 | JCTree.JCExpression myFieldAccess = make.Select(make.This(Type.noType), fieldDecl.name); 331 | 332 | if (fType instanceof JCTree.JCPrimitiveTypeTree) { 333 | switch (((JCTree.JCPrimitiveTypeTree) fType).getPrimitiveTypeKind()) { 334 | case BOOLEAN: 335 | /* this.fieldName ? 1 : 0 */ 336 | expressions.append( 337 | make.Conditional( 338 | myFieldAccess, 339 | make.Literal(TypeTag.INT, 1), 340 | make.Literal(TypeTag.INT, 0) 341 | ) 342 | ); 343 | break; 344 | case LONG: 345 | expressions.append(longToIntForHashCode(myFieldAccess)); 346 | break; 347 | case FLOAT: 348 | /* this.fieldName != 0f ? Float.floatToIntBits(this.fieldName) : 0 */ 349 | expressions.append( 350 | make.Conditional( 351 | make.Binary(JCTree.Tag.NE, myFieldAccess, make.Literal(0f)), 352 | make.App( 353 | make.Select( 354 | make.Ident(names.fromString("Float")), 355 | names.fromString("floatToIntBits")).setType(syms.intType), 356 | List.of(myFieldAccess) 357 | ), 358 | make.Literal(TypeTag.INT, 0) 359 | ) 360 | ); 361 | break; 362 | case DOUBLE: 363 | /* longToIntForHashCode(Double.doubleToLongBits(this.fieldName)) */ 364 | expressions.append( 365 | longToIntForHashCode( 366 | make.App( 367 | make.Select( 368 | make.Ident(names.fromString("Double")), 369 | names.fromString("doubleToLongBits")).setType(syms.intType), 370 | List.of(myFieldAccess) 371 | ) 372 | ) 373 | ); 374 | break; 375 | default: 376 | case BYTE: 377 | case SHORT: 378 | case INT: 379 | case CHAR: 380 | /* just the field */ 381 | expressions.append(myFieldAccess); 382 | break; 383 | } 384 | } else if (fType instanceof JCTree.JCArrayTypeTree) { 385 | expressions.append( 386 | make.App( 387 | make.Select( 388 | make.Select( 389 | make.Select( 390 | make.Ident(names.fromString("java")), 391 | names.fromString("util") 392 | ), 393 | names.fromString("Arrays") 394 | ), 395 | names.fromString("hashCode") 396 | ).setType(syms.intType), 397 | List.of(myFieldAccess) 398 | ) 399 | ); 400 | } else { 401 | /* (this.fieldName != null ? this.fieldName.hashCode() : 0) */ 402 | expressions.append( 403 | make.Conditional( 404 | make.Binary(JCTree.Tag.NE, myFieldAccess, make.Literal(TypeTag.BOT, null)), 405 | make.App(make.Select(myFieldAccess, names.hashCode).setType(syms.intType)), 406 | make.Literal(0) 407 | ) 408 | ); 409 | } 410 | } 411 | 412 | ListBuffer statements = new ListBuffer<>(); 413 | 414 | Name resultName = names.fromString("result"); 415 | 416 | statements.append( 417 | make.VarDef( 418 | make.Modifiers(0L), 419 | resultName, 420 | make.TypeIdent(syms.intType.getTag()), 421 | make.Literal(0) 422 | ) 423 | ); 424 | for (JCTree.JCExpression expression : expressions) { 425 | // result = 31 * result + ${expr} 426 | statements.append(make.Exec( 427 | make.Assign( 428 | make.Ident(resultName), 429 | make.Binary( 430 | JCTree.Tag.PLUS, 431 | make.Binary(JCTree.Tag.MUL, make.Literal(TypeTag.INT, 31), make.Ident(resultName)), 432 | expression 433 | ) 434 | ) 435 | )); 436 | } 437 | 438 | statements.append(make.Return(make.Ident(resultName))); 439 | return statements.toList(); 440 | } 441 | 442 | public JCTree.JCExpression longToIntForHashCode(JCTree.JCExpression ref) { 443 | /* (int) (ref ^ ref >>> 32) */ 444 | return make.TypeCast( 445 | make.TypeIdent(syms.intType.getTag()), 446 | make.Parens( 447 | make.Binary( 448 | JCTree.Tag.BITXOR, 449 | ref, 450 | make.Parens(make.Binary(JCTree.Tag.USR, ref, make.Literal(32))) 451 | ) 452 | ) 453 | ); 454 | } 455 | 456 | private static class MandatoryDesugarAnnotationTreeScanner extends TreeScanner { 457 | 458 | private final Log log; 459 | 460 | private final CompilationUnitTree compilationUnit; 461 | 462 | public MandatoryDesugarAnnotationTreeScanner(Log log, CompilationUnitTree compilationUnit) { 463 | this.log = log; 464 | this.compilationUnit = compilationUnit; 465 | } 466 | 467 | @Override 468 | public Void visitClass(ClassTree node, Void aVoid) { 469 | if ("RECORD".equals(node.getKind().toString())) { 470 | if ( 471 | node.getModifiers().getAnnotations().stream() 472 | .noneMatch(annotation -> { 473 | Type type = ((JCTree.JCAnnotation) annotation).type; 474 | return Desugar.class.getName().equals(type.toString()); 475 | }) 476 | ) { 477 | JavaFileObject oldSource = log.useSource(compilationUnit.getSourceFile()); 478 | try { 479 | log.error( 480 | (JCTree.JCClassDecl) node, 481 | new JCDiagnostic.Error( 482 | "jabel", 483 | "missing.desugar.on.record", 484 | "Must be annotated with @Desugar" 485 | ) 486 | ); 487 | } finally { 488 | log.useSource(oldSource); 489 | } 490 | } 491 | } 492 | return super.visitClass(node, aVoid); 493 | } 494 | } 495 | } 496 | --------------------------------------------------------------------------------