Skip to content

Commit

Permalink
Lombok support for java 21 (#4860)
Browse files Browse the repository at this point in the history
* Lombok support for java 21

* Minimize diff with Java 17 to make it easier to spot intentional changes

* Minimize diff some more to add missing `var` handling

---------

Co-authored-by: Tim te Beek <[email protected]>
  • Loading branch information
jevanlingen and timtebeek authored Jan 7, 2025
1 parent b3f17ce commit 6ddcc37
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,6 @@ public J visitCompilationUnit(CompilationUnitTree node, Space fmt) {

Map<Integer, JCAnnotation> annotationPosTable = mapAnnotations(node.getPackageAnnotations(),
new HashMap<>(node.getPackageAnnotations().size()));

List<J.Annotation> packageAnnotations = collectAnnotations(annotationPosTable);

J.Package packageDecl = null;
Expand Down
1 change: 1 addition & 0 deletions rewrite-java-21/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ val javaTck = configurations.create("javaTck") {
dependencies {
api(project(":rewrite-core"))
api(project(":rewrite-java"))
runtimeOnly(project(":rewrite-java-lombok"))

compileOnly("org.slf4j:slf4j-api:1.7.+")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
import com.sun.tools.javac.comp.Modules;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Log;
import com.sun.tools.javac.util.Options;
import lombok.Getter;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.objectweb.asm.ClassReader;
Expand All @@ -33,7 +35,6 @@
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.SourceFile;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaParsingException;
import org.openrewrite.java.internal.JavaTypeCache;
Expand All @@ -43,20 +44,27 @@
import org.openrewrite.tree.ParseError;
import org.openrewrite.tree.ParsingEventListener;
import org.openrewrite.tree.ParsingExecutionContextView;
import org.slf4j.LoggerFactory;

import javax.annotation.processing.Processor;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;
import java.io.*;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;

/**
Expand All @@ -77,14 +85,16 @@ public class ReloadableJava21Parser implements JavaParser {
private final JavaCompiler compiler;
private final ResettableLog compilerLog;
private final Collection<NamedStyles> styles;

private ReloadableJava21Parser(boolean logCompilationWarningsAndErrors,
@Nullable Collection<Path> classpath,
Collection<byte[]> classBytesClasspath,
@Nullable Collection<Input> dependsOn,
Charset charset,
Collection<NamedStyles> styles,
JavaTypeCache typeCache) {
private final List<Processor> annotationProcessors;

private ReloadableJava21Parser(
boolean logCompilationWarningsAndErrors,
@Nullable Collection<Path> classpath,
Collection<byte[]> classBytesClasspath,
@Nullable Collection<Input> dependsOn,
Charset charset,
Collection<NamedStyles> styles,
JavaTypeCache typeCache) {
this.classpath = classpath;
this.dependsOn = dependsOn;
this.styles = styles;
Expand All @@ -106,6 +116,70 @@ private ReloadableJava21Parser(boolean logCompilationWarningsAndErrors,
Options.instance(context).put("-g", "-g");
Options.instance(context).put("-proc", "none");

// Ensure type attribution continues despite errors in individual files or nodes.
// If an error occurs in a single file or node, type attribution should still proceed
// for all other source files and unaffected nodes within the same file.
Options.instance(context).put("should-stop.ifError", "GENERATE");

LOMBOK:
if (System.getenv().getOrDefault("REWRITE_LOMBOK", System.getProperty("rewrite.lombok")) != null &&
classpath != null && classpath.stream().anyMatch(it -> it.toString().contains("lombok"))) {
Processor lombokProcessor = null;
try {
// https://projectlombok.org/contributing/lombok-execution-path
List<String> overrideClasspath = new ArrayList<>();
for (Path part : classpath) {
if (part.toString().contains("lombok")) {
overrideClasspath.add(part.toString());
}
}
// make sure the rewrite-java-lombok dependency comes first
boolean found = false;
for (int i = 0; i < overrideClasspath.size(); i++) {
if (overrideClasspath.get(i).contains("rewrite-java-lombok")) {
overrideClasspath.add(0, overrideClasspath.remove(i));
found = true;
}
}
if (!found) {
// try to find `rewrite-java-lombok` using class loader
URL resource = getClass().getClassLoader().getResource("org/openrewrite/java/lombok/OpenRewriteConfigurationKeysLoader.class");
if (resource != null && resource.getProtocol().equals("jar") && resource.getPath().startsWith("file:")) {
String path = Paths.get(URI.create(resource.getPath().substring(0, resource.getPath().indexOf("!")))).toString();
overrideClasspath.add(0, path);
} else {
break LOMBOK;
}
}
System.setProperty("shadow.override.lombok", String.join(File.pathSeparator, overrideClasspath));

Class<?> shadowLoaderClass = Class.forName("lombok.launch.ShadowClassLoader", true, getClass().getClassLoader());
Constructor<?> shadowLoaderConstructor = shadowLoaderClass.getDeclaredConstructor(
Class.forName("java.lang.ClassLoader"),
Class.forName("java.lang.String"),
Class.forName("java.lang.String"),
Class.forName("java.util.List"),
Class.forName("java.util.List"));
shadowLoaderConstructor.setAccessible(true);

ClassLoader lombokShadowLoader = (ClassLoader) shadowLoaderConstructor.newInstance(
getClass().getClassLoader(),
"lombok",
null,
emptyList(),
singletonList("lombok.patcher.Symbols")
);
lombokProcessor = (Processor) lombokShadowLoader.loadClass("lombok.core.AnnotationProcessor").getDeclaredConstructor().newInstance();
Options.instance(context).put(Option.PROCESSOR, "lombok.launch.AnnotationProcessorHider$AnnotationProcessor");
} catch (ReflectiveOperationException ignore) {
// Lombok was not found or could not be initialized
} finally {
annotationProcessors = lombokProcessor != null ? singletonList(lombokProcessor) : emptyList();
}
} else {
annotationProcessors = emptyList();
}

// MUST be created (registered with the context) after pfm and compilerLog
compiler = new JavaCompiler(context);

Expand All @@ -124,7 +198,7 @@ public void write(char[] cbuf, int off, int len) {
if (logCompilationWarningsAndErrors) {
String log = new String(Arrays.copyOfRange(cbuf, off, len));
if (!log.isBlank()) {
org.slf4j.LoggerFactory.getLogger(ReloadableJava21Parser.class).warn(log);
LoggerFactory.getLogger(ReloadableJava21Parser.class).warn(log);
}
}
}
Expand Down Expand Up @@ -164,6 +238,7 @@ public Stream<SourceFile> parseInputs(Iterable<Input> sourceFiles, @Nullable Pat
);

J.CompilationUnit cu = (J.CompilationUnit) parser.scan(cuByPath.getValue(), Space.EMPTY);
//noinspection DataFlowIssue
cuByPath.setValue(null); // allow memory used by this JCCompilationUnit to be released
parsingListener.parsed(input, cu);
return requirePrintEqualsInput(cu, input, relativeTo, ctx);
Expand All @@ -188,39 +263,46 @@ LinkedHashMap<Input, JCTree.JCCompilationUnit> parseInputsToCompilerAst(Iterable
}

LinkedHashMap<Input, JCTree.JCCompilationUnit> cus = new LinkedHashMap<>();
acceptedInputs(sourceFiles).forEach(input1 -> {
List<ReloadableJava21ParserInputFileObject> inputFileObjects = acceptedInputs(sourceFiles)
.map(input -> new ReloadableJava21ParserInputFileObject(input, ctx))
.toList();
if (!annotationProcessors.isEmpty()) {
compiler.initProcessAnnotations(annotationProcessors, inputFileObjects, emptyList());
}
try {
//noinspection unchecked
com.sun.tools.javac.util.List<JCTree.JCCompilationUnit> jcCompilationUnits = compiler.parseFiles((List<JavaFileObject>) (List<?>) inputFileObjects, true);
for (int i = 0; i < inputFileObjects.size(); i++) {
cus.put(inputFileObjects.get(i).getInput(), jcCompilationUnits.get(i));
}
try {
JCTree.JCCompilationUnit jcCompilationUnit = compiler.parse(new ReloadableJava21ParserInputFileObject(input1, ctx));
cus.put(input1, jcCompilationUnit);
} catch (IllegalStateException e) {
if ("endPosTable already set".equals(e.getMessage())) {
throw new IllegalStateException(
"Call reset() on JavaParser before parsing another set of source files that " +
"have some of the same fully qualified names. Source file [" +
input1.getPath() + "]\n[\n" + StringUtils.readFully(input1.getSource(ctx), getCharset(ctx)) + "\n]", e);
initModules(cus.values());
enterAll(cus.values());

// For some reason this is necessary in JDK 9+, where the internal block counter that
// annotationsBlocked() tests against remains >0 after attribution.
Annotate annotate = Annotate.instance(context);
while (annotate.annotationsBlocked()) {
annotate.unblockAnnotations(); // also flushes once unblocked
}
throw e;
if (!annotationProcessors.isEmpty()) {
compiler.processAnnotations(jcCompilationUnits, emptyList());
}
compiler.attribute(compiler.todo);
} catch (Throwable t) {
// when symbol entering fails on problems like missing types, attribution can often times proceed
// unhindered, but it sometimes cannot (so attribution is always best-effort in the presence of errors)
ctx.getOnError().accept(new JavaParsingException("Failed symbol entering or attribution", t));
}
});

try {
initModules(cus.values());
enterAll(cus.values());

// For some reason this is necessary in JDK 9+, where the internal block counter that
// annotationsBlocked() tests against remains >0 after attribution.
Annotate annotate = Annotate.instance(context);
while (annotate.annotationsBlocked()) {
annotate.unblockAnnotations(); // also flushes once unblocked
} catch (IllegalStateException e) {
if ("endPosTable already set".equals(e.getMessage())) {
throw new IllegalStateException(
"Call reset() on JavaParser before parsing another set of source files that " +
"have some of the same fully qualified names.", e);
}

compiler.attribute(compiler.todo);
} catch (
Throwable t) {
// when symbol entering fails on problems like missing types, attribution can often times proceed
// unhindered, but it sometimes cannot (so attribution is always best-effort in the presence of errors)
ctx.getOnError().accept(new JavaParsingException("Failed symbol entering or attribution", t));
throw e;
}

return cus;
}

Expand Down Expand Up @@ -333,16 +415,17 @@ public Iterable<JavaFileObject> list(Location location, String packageName, Set<
Iterable<JavaFileObject> listed = super.list(location, packageName, kinds, recurse);
return classByteClasspath.isEmpty() ? listed :
Stream.concat(classByteClasspath.stream()
.filter(jfo -> jfo.getPackage().equals(packageName)),
StreamSupport.stream(listed.spliterator(), false)
).collect(toList());
.filter(jfo -> jfo.getPackage().equals(packageName)),
StreamSupport.stream(listed.spliterator(), false)
).collect(toList());
}
return super.list(location, packageName, kinds, recurse);
}
}

private static class PackageAwareJavaFileObject extends SimpleJavaFileObject {
private final String pkg;
@Getter
private final String className;
private final byte[] classBytes;

Expand Down Expand Up @@ -376,10 +459,6 @@ public String getPackage() {
return pkg;
}

public String getClassName() {
return className;
}

@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(classBytes);
Expand Down
Loading

0 comments on commit 6ddcc37

Please sign in to comment.