Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
902b7a7
Added implementation for classpathFromResource
JohannisK May 2, 2025
bf447cf
Join `jarNames` without `Stream.map(..)`
timtebeek May 4, 2025
fc56e32
Update expectations
timtebeek Aug 14, 2025
b44be87
Do not use `classpathFromResources` for TemplateProcessor just yet
timtebeek Aug 14, 2025
026d86d
Merge branch 'main' into 1136-use-classpathfromresources-instead-of-r…
timtebeek Aug 15, 2025
979e22c
Update expectations for NoGuavaRefasterRecipes.java
timtebeek Aug 15, 2025
06e4ab1
Add an option to generate classpathFromResources
timtebeek Aug 15, 2025
9d7ca4d
Adapt Semantics, PatternBuilder and TemplateProcessor
timtebeek Aug 15, 2025
013c36d
Apply formatter
timtebeek Aug 15, 2025
979aedc
Determine classpathFromResources from first argument
timtebeek Aug 15, 2025
ff581ba
Correctly concatenate more than one jar name
timtebeek Aug 15, 2025
77f2aa6
Add transitive jars to classpathFromResources too
timtebeek Aug 15, 2025
867a659
Run CI for pull request targeted at any branch
timtebeek Aug 15, 2025
436f996
Merge branch 'main' into transitive-jarnames
timtebeek Aug 15, 2025
f561654
Update ci.yml
timtebeek Aug 15, 2025
73e8f11
Merge branch 'main' into transitive-jarnames
timtebeek Aug 15, 2025
cbc503f
Expand to detect more types
timtebeek Aug 15, 2025
854d798
Annotated types come with a classpath dependency
timtebeek Aug 16, 2025
c2526b4
Annotated types come with a classpath dependency
timtebeek Aug 16, 2025
8d76c92
Merge branch 'main' into transitive-jarnames
timtebeek Aug 16, 2025
f4b16f6
Add ClasspathJarNameDetectorTest
timtebeek Aug 16, 2025
93d8d9a
Reuse `ImportDetector.imports`
timtebeek Aug 16, 2025
053233f
Organize imports
timtebeek Aug 16, 2025
ee55b6b
Improve formatting in ClasspathJarNameDetectorTest
timtebeek Aug 16, 2025
8c30c79
Accept suggestions to make the tests pass
timtebeek Aug 16, 2025
e508f29
Prune dead code branch according to test coverage
timtebeek Aug 16, 2025
694c92d
Make better use of visitor instead of instanceof checks in scan
timtebeek Aug 16, 2025
d32e934
Remove unnecessary method and nullability warnings
timtebeek Aug 16, 2025
fdc494e
Minimize what we actually need for current tests
timtebeek Aug 16, 2025
b28c470
Trim empty lines
timtebeek Aug 16, 2025
c19f3f4
Avoid nullability issue in `detectJUnitAndOpenTest4JFromStatement`
timtebeek Aug 16, 2025
17850a4
Swap test order
timtebeek Aug 16, 2025
ad52cd1
Merge branch 'main' into transitive-jarnames
timtebeek Aug 16, 2025
d328715
Merge branch 'main' into transitive-jarnames
timtebeek Aug 16, 2025
eff6676
Merge branch 'main' into transitive-jarnames
timtebeek Aug 16, 2025
f49b773
Adopt text blocks for ClasspathJarNameDetectorTest
timtebeek Aug 16, 2025
304d80a
Apply suggestions from code review
timtebeek Aug 16, 2025
b784da7
Add exports for ClasspathJarNameDetectorTest compilation
timtebeek Aug 17, 2025
1acb5c4
Slight polish
timtebeek Aug 17, 2025
fc7b9ba
Add a test showing we need transitive dependencies too
timtebeek Aug 17, 2025
a241c1b
Also look at super class and interfaces
timtebeek Aug 17, 2025
d938e9d
Fix execution in IntelliJ
timtebeek Aug 17, 2025
693fb9c
Add another test for extraction based on first statement
timtebeek Aug 17, 2025
e013a4a
Show extraction works even without imports
timtebeek Aug 17, 2025
b5c2e7c
Start anew
timtebeek Aug 17, 2025
bc02ba4
Apply formatter
timtebeek Aug 17, 2025
3da3a53
Use a private static class to minimize nullable warnings
timtebeek Aug 17, 2025
84fe2ec
Move where ImportsDetector is invoked
timtebeek Aug 17, 2025
e5ce73a
Remove the need for ImportDetector in ClasspathJarNameDetector
timtebeek Aug 17, 2025
e702d41
Organize imports
timtebeek Aug 17, 2025
50dd501
Collapse loops and visit in logical order
timtebeek Aug 17, 2025
a5c394b
Have ClasspathJarNameDetector directly extend TreeScanner
timtebeek Aug 17, 2025
3071974
Inline the static `jarNameFor`
timtebeek Aug 18, 2025
34b646f
Add major version to classpath entries to avoid conflicts
timtebeek Aug 18, 2025
6bfcdb8
Also suffix JSpecify versions
timtebeek Aug 18, 2025
d670df4
Use `replaceFirst` as suggested
timtebeek Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ On newer Java version you'd perhaps want to pass in the following option:
-Arewrite.generatedAnnotation=jakarta.annotation.Generated
```

### Use JavaParser.Builder `.classpathFromResources(ctx, "guava")`
### Use JavaParser.Builder `.classpathFromResources(ctx, "guava-31")`
By default, the annotation processor will use `JavaParser.runtimeClasspath()` to resolve the classpath for newly generated Java code snippets.
If you want to use `TypeTables` from `src/main/resources/META-INF/rewrite/classpath.tsv.gz` instead, pass in the following option:
```
Expand Down
26 changes: 25 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,38 @@ tasks.named<JavaCompile>("compileTestJava") {
})
sourceCompatibility = JavaVersion.VERSION_21.toString()
targetCompatibility = JavaVersion.VERSION_21.toString()

options.compilerArgs.addAll(
listOf(
"--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
)
)
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
})
jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED")
jvmArgs(
"--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
)
}

tasks.withType<Javadoc>().configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,34 @@
package org.openrewrite.java.template.internal;

import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.TreeScanner;
import org.jspecify.annotations.Nullable;

import javax.tools.JavaFileObject;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ClasspathJarNameDetector {
public class ClasspathJarNameDetector extends TreeScanner {
private final Set<String> jarNames = new LinkedHashSet<>();

/**
* Locate types that are directly referred to by name in the
* given tree and therefore need an import in the template.
*
* @return The list of imports to add.
*/
public static Set<String> classpathFor(JCTree input, Collection<Symbol> imports) {
Set<String> jarNames = new LinkedHashSet<String>() {
@Override
public boolean add(@Nullable String s) {
return s != null && super.add(s);
}
};

for (Symbol anImport : imports) {
jarNames.add(jarNameFor(anImport));
}

new TreeScanner() {
@Override
public void scan(JCTree tree) {
// Detect fully qualified classes
if (tree instanceof JCFieldAccess &&
((JCFieldAccess) tree).sym instanceof Symbol.ClassSymbol &&
Character.isUpperCase(((JCFieldAccess) tree).getIdentifier().toString().charAt(0))) {
jarNames.add(jarNameFor(((JCFieldAccess) tree).sym));
}
super.scan(tree);
}
}.scan(input);

public Set<String> classpathFor(JCTree input) {
scan(input);
return jarNames;
}


private static @Nullable String jarNameFor(Symbol anImport) {
Symbol.ClassSymbol enclClass = anImport instanceof Symbol.ClassSymbol ? (Symbol.ClassSymbol) anImport : anImport.enclClass();
private void addJarNameFor(Symbol owner) {
Symbol.ClassSymbol enclClass = owner instanceof Symbol.ClassSymbol ? (Symbol.ClassSymbol) owner : owner.enclClass();
while (enclClass.enclClass() != null && enclClass.enclClass() != enclClass) {
enclClass = enclClass.enclClass();
}
Expand All @@ -75,10 +52,94 @@ public void scan(JCTree tree) {
String uriStr = classfile.toUri().toString();
Matcher matcher = Pattern.compile("([^/]*)?\\.jar!/").matcher(uriStr);
if (matcher.find()) {
String jarName = matcher.group(1);
return jarName.replaceAll("-\\d.*$", "");
String jarName = matcher.group(1)
// Retain major version number, to avoid `log4j` conflict between `log4j-1` and `log4j2-1`
.replaceFirst("(-\\d+).*?$", "$1");
jarNames.add(jarName);
}
}
return null;
}

@Override
public void scan(JCTree tree) {
// Detect fully qualified classes
if (tree instanceof JCFieldAccess &&
((JCFieldAccess) tree).sym instanceof Symbol.ClassSymbol &&
Character.isUpperCase(((JCFieldAccess) tree).getIdentifier().toString().charAt(0))) {
addJarNameFor(((JCFieldAccess) tree).sym);
}

// Detect method invocations and their types
if (tree instanceof JCTree.JCMethodInvocation) {
JCTree.JCMethodInvocation invocation = (JCTree.JCMethodInvocation) tree;
Symbol.MethodSymbol methodSym = null;

if (invocation.meth instanceof JCFieldAccess) {
JCFieldAccess methodAccess = (JCFieldAccess) invocation.meth;
if (methodAccess.sym instanceof Symbol.MethodSymbol) {
methodSym = (Symbol.MethodSymbol) methodAccess.sym;
}
} else if (invocation.meth instanceof JCTree.JCIdent) {
// Handle unqualified method calls (e.g., from static imports)
JCTree.JCIdent methodIdent = (JCTree.JCIdent) invocation.meth;
if (methodIdent.sym instanceof Symbol.MethodSymbol) {
methodSym = (Symbol.MethodSymbol) methodIdent.sym;
}
}

if (methodSym != null) {
// Add jar for the method's owner class
addJarNameFor(methodSym.owner);

// Add jar for the return type
if (methodSym.getReturnType() != null) {
addTypeAndTransitiveDependencies(methodSym.getReturnType());
}

// Add jars for exception types
for (Type thrownType : methodSym.getThrownTypes()) {
addTypeAndTransitiveDependencies(thrownType);
}
}
}

// Detect identifiers that reference classes
if (tree instanceof JCTree.JCIdent) {
JCTree.JCIdent ident = (JCTree.JCIdent) tree;
if (ident.sym instanceof Symbol.ClassSymbol) {
Symbol.ClassSymbol classSym = (Symbol.ClassSymbol) ident.sym;
addJarNameFor(classSym);

// Add transitive dependencies through inheritance
addTypeAndTransitiveDependencies(classSym.type);
}
}

super.scan(tree);
}

private void addTypeAndTransitiveDependencies(@Nullable Type type) {
if (type == null) {
return;
}

if (type.tsym instanceof Symbol.ClassSymbol) {
Symbol.ClassSymbol classSym = (Symbol.ClassSymbol) type.tsym;
addJarNameFor(classSym);

// Check superclass
Type superType = classSym.getSuperclass();
if (superType != null && superType.tsym != null) {
addJarNameFor(superType.tsym);
}

// Check interfaces
for (Type iface : classSym.getInterfaces()) {
if (iface.tsym != null) {
addJarNameFor(iface.tsym);
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,10 @@ public static Collection<Symbol> imports(JCTree.JCMethodDecl methodDecl) {

public static Collection<Symbol> imports(JCTree.JCMethodDecl methodDecl, Predicate<JCTree> scopePredicate) {
ImportScanner importScanner = new ImportScanner(scopePredicate);
methodDecl.getParameters().forEach(importScanner::scan);
methodDecl.getTypeParameters().forEach(importScanner::scan);
importScanner.scan(methodDecl.getBody());
importScanner.scan(methodDecl.getReturnType());
for (JCTree.JCVariableDecl param : methodDecl.getParameters()) {
importScanner.scan(param);
}
for (JCTree.JCTypeParameter param : methodDecl.getTypeParameters()) {
importScanner.scan(param);
}
return importScanner.imports;
}

/**
* Locate types that are directly referred to by name in the
* given tree and therefore need an import in the template.
*
* @return The list of imports to add.
*/
public static Collection<Symbol> imports(JCTree tree) {
ImportScanner importScanner = new ImportScanner(t -> true);
importScanner.scan(tree);
return importScanner.imports;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,9 @@ public static <T extends JCTree> String process(
if (!printer.staticImports.isEmpty()) {
builder.append("\n .staticImports(").append(printer.staticImports.stream().map(i -> '"' + i + '"').collect(joining(", "))).append(")");
}
Set<String> jarNames = ClasspathJarNameDetector.classpathFor(tree, ImportDetector.imports(tree));
for (JCTree.JCVariableDecl parameter : parameters) {
jarNames.addAll(ClasspathJarNameDetector.classpathFor(parameter, ImportDetector.imports(parameter)));
}
ClasspathJarNameDetector classpathJarNameDetector = new ClasspathJarNameDetector();
parameters.forEach(classpathJarNameDetector::classpathFor);
Set<String> jarNames = classpathJarNameDetector.classpathFor(tree);
if (!jarNames.isEmpty()) {
builder.append("\n .javaParser(JavaParser.fromJavaVersion()");
if (classpathFromResources) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class RefasterTemplateProcessorTest {
@ValueSource(strings = {
"Arrays",
"CharacterEscapeAnnotation",
"ClasspathFromResourcesTransitive",
"ComplexGenerics",
"FindListAdd",
"MatchOrder",
Expand Down Expand Up @@ -174,8 +175,10 @@ static Compilation compile(JavaFileObject javaFileObject, TypeAwareProcessor pro
fileForClass(BeforeTemplate.class),
fileForClass(AfterTemplate.class),
fileForClass(com.google.common.collect.ImmutableMap.class),
fileForClass(org.junit.jupiter.api.Assertions.class),
fileForClass(org.openrewrite.Recipe.class),
fileForClass(org.openrewrite.java.JavaTemplate.class),
fileForClass(org.opentest4j.MultipleFailuresError.class),
fileForClass(org.slf4j.Logger.class),
fileForClass(Primitive.class),
fileForClass(NullMarked.class),
Expand Down
Loading