Fix classpath calculation

* Refactor MavanProjectAnalyzer and related classes
* Refactor MavenProjectParser parse methods to reuse code between source sets
* ParsingResults are provided to subsequent modules in a multi-module project. The result can be used to build the classpath when parsing subsequent modules.
This commit is contained in:
Fabian Krüger
2024-01-22 15:57:14 +00:00
committed by GitHub
parent 31288e0c1e
commit 13ed15952a
20 changed files with 2434 additions and 1550 deletions

View File

@@ -1,198 +0,0 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.openrewrite.ExecutionContext;
import org.openrewrite.RecipeRun;
import org.openrewrite.SourceFile;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.marker.JavaSourceSet;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.springframework.rewrite.parsers.RewriteExecutionContext;
import org.springframework.rewrite.parsers.RewriteProjectParsingResult;
import org.springframework.rewrite.parsers.SpringRewriteProperties;
import org.springframework.rewrite.parsers.maven.ClasspathDependencies;
import org.springframework.rewrite.support.openrewrite.GenericOpenRewriteRecipe;
import org.springframework.rewrite.test.util.ParserExecutionHelper;
import org.springframework.rewrite.test.util.TestProjectHelper;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class MavenTypeResolutionTest {
@Nested
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class WithSingleModuleMavenProject {
private static RewriteProjectParsingResult parsingResult;
@Test
@Order(1)
@DisplayName("parsed project should have correct classpath")
void parsedProjectShouldHaveCorrectClasspath(@TempDir Path baseDir) {
// given
pepareProject(baseDir);
// when
parsingResult = new ParserExecutionHelper().parseWithRewriteProjectParser(baseDir,
new SpringRewriteProperties());
// then
verifyTypesOnClasspathAndNoTypesInUse(parsingResult);
}
@Test
@Order(2)
@DisplayName("annotating the class adds the annotation to typesInUse")
void annotatingTheClassAddsTheAnnotationToTypesInUse() {
// Given: parsed project
List<SourceFile> sourceFiles = parsingResult.sourceFiles();
assertThat(sourceFiles).isNotEmpty();
// When: Add @SpringBootApplication annotation to class
RecipeRun recipeRun = annotateClass(sourceFiles);
// Then: annotation is in typesInUse
verifyAnnotationIsNowInUsedTypes(recipeRun);
}
private static void verifyAnnotationIsNowInUsedTypes(RecipeRun recipeRun) {
SourceFile after = recipeRun.getChangeset().getAllResults().get(0).getAfter();
assertThat(after).isInstanceOf(J.CompilationUnit.class);
J.CompilationUnit cu = (J.CompilationUnit) after;
List<String> classpath = cu.getMarkers()
.findFirst(JavaSourceSet.class)
.get()
.getClasspath()
.stream()
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(classpath).contains("org.springframework.boot.autoconfigure.SpringBootApplication", "SomeClass");
List<String> typesInUse = cu.getTypesInUse()
.getTypesInUse()
.stream()
.map(JavaType.FullyQualified.class::cast)
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(typesInUse)
.containsExactlyInAnyOrder("org.springframework.boot.autoconfigure.SpringBootApplication");
}
@NotNull
private static RecipeRun annotateClass(List<SourceFile> sourceFiles) {
RecipeRun recipeRun = new GenericOpenRewriteRecipe<>(() -> new JavaIsoVisitor<>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl,
ExecutionContext executionContext) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, executionContext);
if (cd.getSimpleName().equals("SomeClass")) {
ClasspathDependencies classpathDependencies = ((J.CompilationUnit) getCursor()
.dropParentUntil(J.CompilationUnit.class::isInstance)
.getValue()).getMarkers().findFirst(ClasspathDependencies.class).get();
String annotationFqName = "org.springframework.boot.autoconfigure.SpringBootApplication";
cd = JavaTemplate.builder("@SpringBootApplication")
.imports(annotationFqName)
.javaParser(JavaParser.fromJavaVersion()
.classpath(classpathDependencies.getDependencies())
.logCompilationWarningsAndErrors(true))
.build()
.apply(getCursor(), cd.getCoordinates()
.addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)));
maybeAddImport(annotationFqName);
}
return cd;
}
}).run(new InMemoryLargeSourceSet(sourceFiles), new RewriteExecutionContext());
return recipeRun;
}
private static void verifyTypesOnClasspathAndNoTypesInUse(RewriteProjectParsingResult parallelParsingResult) {
J.CompilationUnit cuBefore = parallelParsingResult.sourceFiles()
.stream()
.filter(J.CompilationUnit.class::isInstance)
.map(J.CompilationUnit.class::cast)
.findFirst()
.get();
List<String> typesInUseBefore = cuBefore.getTypesInUse()
.getTypesInUse()
.stream()
.map(JavaType.FullyQualified.class::cast)
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(typesInUseBefore).isEmpty();
List<String> classpathBefore = cuBefore.getMarkers()
.findFirst(JavaSourceSet.class)
.get()
.getClasspath()
.stream()
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(classpathBefore).contains("org.springframework.boot.autoconfigure.SpringBootApplication",
"SomeClass");
}
private static void pepareProject(Path baseDir) {
TestProjectHelper.createTestProject(baseDir)
.addResource("pom.xml",
"""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>artifact</artifactId>
<version>0.1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.1.3</version>
</dependency>
</dependencies>
</project>
""")
.addResource("src/main/java/SomeClass.java",
// @formatter:off
"""
public class SomeClass {}
"""
// @formatter:on
)
.writeToFilesystem();
}
}
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class WithMultiModuleMavenProject {
}
}

View File

@@ -0,0 +1,514 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import org.openrewrite.ExecutionContext;
import org.openrewrite.RecipeRun;
import org.openrewrite.SourceFile;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.marker.JavaSourceSet;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.Statement;
import org.springframework.rewrite.parsers.RewriteExecutionContext;
import org.springframework.rewrite.parsers.RewriteProjectParsingResult;
import org.springframework.rewrite.parsers.SpringRewriteProperties;
import org.springframework.rewrite.parsers.maven.ClasspathDependencies;
import org.springframework.rewrite.support.openrewrite.GenericOpenRewriteRecipe;
import org.springframework.rewrite.test.util.ParserExecutionHelper;
import org.springframework.rewrite.test.util.TestProjectHelper;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class MavenTypeResolutionTests {
@Nested
class ClassesFromMainCanBeResolvedInTest {
@Test
@DisplayName("TestClass should have MainClass in used types")
void testClassShouldHaveMainClassInUsedTypes(@TempDir Path baseDir) {
TestProjectHelper.createTestProject(baseDir)
.addResource("pom.xml",
"""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>artifact</artifactId>
<version>0.1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.1.3</version>
</dependency>
</dependencies>
</project>
""")
.addResource("src/main/java/main/SomeClass.java", """
package main;
public class SomeClass {}
""")
.addResource("src/test/java/test/SomeTest.java", """
package test;
import main.SomeClass;
public class SomeTest {
private SomeClass someClass;
}
""")
.writeToFilesystem();
RewriteProjectParsingResult parsingResult = new ParserExecutionHelper()
.parseWithRewriteProjectParser(baseDir, new SpringRewriteProperties());
SourceFile sourceFile = parsingResult.sourceFiles()
.stream()
.filter(f -> f.getSourcePath().toString().endsWith("SomeTest.java"))
.findFirst()
.get();
assertThat(sourceFile).isNotNull();
J.CompilationUnit cu = (J.CompilationUnit) sourceFile;
cu.getClasses().get(0).getBody().getStatements().get(0);
assertThat(
((JavaType.Class) ((J.VariableDeclarations) cu.getClasses().get(0).getBody().getStatements().get(0))
.getTypeExpression()
.getType()).getFullyQualifiedName())
.isEqualTo("main.SomeClass");
}
}
@Nested
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class WithSingleModuleMavenProject {
private static RewriteProjectParsingResult parsingResult;
@Test
@Order(1)
@DisplayName("parsed project should have correct classpath")
void parsedProjectShouldHaveCorrectClasspath(@TempDir Path baseDir) {
// given
pepareProject(baseDir);
// when
parsingResult = new ParserExecutionHelper().parseWithRewriteProjectParser(baseDir,
new SpringRewriteProperties());
// then
verifyTypesOnClasspathAndNoTypesInUse(parsingResult);
}
@Test
@Order(2)
@DisplayName("annotating the class adds the annotation to typesInUse")
void annotatingTheClassAddsTheAnnotationToTypesInUse() {
// Given: parsed project
List<SourceFile> sourceFiles = parsingResult.sourceFiles();
assertThat(sourceFiles).isNotEmpty();
// When: Add @SpringBootApplication annotation to class
RecipeRun recipeRun = annotateClass(sourceFiles);
// Then: annotation is in typesInUse
verifyAnnotationIsNowInUsedTypes(recipeRun);
}
private static void verifyAnnotationIsNowInUsedTypes(RecipeRun recipeRun) {
SourceFile after = recipeRun.getChangeset().getAllResults().get(0).getAfter();
assertThat(after).isInstanceOf(J.CompilationUnit.class);
J.CompilationUnit cu = (J.CompilationUnit) after;
List<String> classpath = cu.getMarkers()
.findFirst(JavaSourceSet.class)
.get()
.getClasspath()
.stream()
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(classpath).contains("org.springframework.boot.autoconfigure.SpringBootApplication", "SomeClass");
List<String> typesInUse = cu.getTypesInUse()
.getTypesInUse()
.stream()
.map(JavaType.FullyQualified.class::cast)
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(typesInUse)
.containsExactlyInAnyOrder("org.springframework.boot.autoconfigure.SpringBootApplication");
}
@NotNull
private static RecipeRun annotateClass(List<SourceFile> sourceFiles) {
RecipeRun recipeRun = new GenericOpenRewriteRecipe<>(() -> new JavaIsoVisitor<>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl,
ExecutionContext executionContext) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, executionContext);
if (cd.getSimpleName().equals("SomeClass")) {
ClasspathDependencies classpathDependencies = ((J.CompilationUnit) getCursor()
.dropParentUntil(J.CompilationUnit.class::isInstance)
.getValue()).getMarkers().findFirst(ClasspathDependencies.class).get();
String annotationFqName = "org.springframework.boot.autoconfigure.SpringBootApplication";
cd = JavaTemplate.builder("@SpringBootApplication")
.imports(annotationFqName)
.javaParser(JavaParser.fromJavaVersion()
.classpath(classpathDependencies.getDependencies())
.logCompilationWarningsAndErrors(true))
.build()
.apply(getCursor(), cd.getCoordinates()
.addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)));
maybeAddImport(annotationFqName);
}
return cd;
}
}).run(new InMemoryLargeSourceSet(sourceFiles), new RewriteExecutionContext());
return recipeRun;
}
private static void verifyTypesOnClasspathAndNoTypesInUse(RewriteProjectParsingResult parallelParsingResult) {
J.CompilationUnit cuBefore = parallelParsingResult.sourceFiles()
.stream()
.filter(J.CompilationUnit.class::isInstance)
.map(J.CompilationUnit.class::cast)
.findFirst()
.get();
List<String> typesInUseBefore = cuBefore.getTypesInUse()
.getTypesInUse()
.stream()
.map(JavaType.FullyQualified.class::cast)
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(typesInUseBefore).isEmpty();
List<String> classpathBefore = cuBefore.getMarkers()
.findFirst(JavaSourceSet.class)
.get()
.getClasspath()
.stream()
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(classpathBefore).contains("org.springframework.boot.autoconfigure.SpringBootApplication",
"SomeClass");
}
private static void pepareProject(Path baseDir) {
// formatter:off
TestProjectHelper.createTestProject(baseDir)
.addResource("pom.xml",
"""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>artifact</artifactId>
<version>0.1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.1.3</version>
</dependency>
</dependencies>
</project>
""")
.addResource("src/main/java/SomeClass.java", """
public class SomeClass {}
""")
.writeToFilesystem();
// formatter:on
}
}
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Nested
class WithMultiModuleMavenProject {
/**
* formatter:off Multi-module project with two modules module-1 and module-2.
* module-2 has a dependency to spring and a class in main Module2Class and one in
* test Module2Test module-1 has a dependency to module-2 and a class in main
* Module1Class and one in test Module1Test, Module1Class depends on Module2Class
* and MOdule1Test depends on Module2Class and Module2Test. formatter:on
*/
@Test
@DisplayName("Module-1 should have classpath from module-2")
void module1ShouldHaveClasspathFromModule2(@TempDir Path baseDir) {
TestProjectHelper.createTestProject(baseDir)
.addResource("pom.xml", PARENT_POM)
.addResource("module-1/pom.xml", MODULE_1_POM)
.addResource("module-2/pom.xml", MODULE_2_POM)
.addResource("module-1/src/main/java/com/example/module1/main/Module1Class.java", MODULE_1_CLASS)
.addResource("module-1/src/test/java/com/example/module1/test/Module1Test.java", MODULE_1_TEST)
.addResource("module-2/src/main/java/com/example/module2/main/Module2Class.java", MODULE_2_CLASS)
.addResource("module-2/src/test/java/com/example/module2/test/Module2Test.java", MODULE_2_TEST)
.writeToFilesystem();
RewriteProjectParsingResult parsingResult = new ParserExecutionHelper()
.parseWithRewriteProjectParser(baseDir, new SpringRewriteProperties());
// Module2Class
J.CompilationUnit module2class = getCompilationContainginTypeEndingWith("Module2Class.java", parsingResult);
// classpath
List<String> cpModule2 = getClasspath(module2class);
// typesInUse
List<String> module2classTypesInUse = getTypesInUse(module2class);
assertThat(module2classTypesInUse).contains("org.springframework.boot.autoconfigure.SpringBootApplication");
assertThat(cpModule2).contains("org.springframework.boot.autoconfigure.SpringBootApplication");
// Module2Test
J.CompilationUnit module2test = getCompilationContainginTypeEndingWith("Module2Test.java", parsingResult);
// typesInUse
List<String> module2testTypesInUse = getTypesInUse(module2test);
// classpath
List<String> classpathModule2test = getClasspath(module2test);
Statement actual = module2test.getClasses().get(0).getBody().getStatements().get(0);
assertThat(classpathModule2test).contains("com.example.module2.main.Module2Class",
"com.example.module2.test.Module2Test",
"org.springframework.boot.autoconfigure.SpringBootApplication",
"org.springframework.boot.test.context.SpringBootTest");
// Module1Class
J.CompilationUnit module1class = getCompilationContainginTypeEndingWith("Module1Class.java", parsingResult);
// typesInUse
List<String> module1classTypesInUse = getTypesInUse(module1class);
// classpath
List<String> module1classClasspath = getClasspath(module1class);
assertThat(module1classTypesInUse).contains("com.example.module2.main.Module2Class");
assertThat(module1classClasspath).contains("com.example.module1.main.Module1Class",
"com.example.module2.main.Module2Class",
"org.springframework.boot.autoconfigure.SpringBootApplication");
assertThat(module1classClasspath).doesNotContain("com.example.module2.test.Module2Test",
"org.springframework.boot.test.context.SpringBootTest");
// Module1Test
J.CompilationUnit module1test = getCompilationContainginTypeEndingWith("Module1Test.java", parsingResult);
// typesInUse
List<String> module1testTypesInUse = getTypesInUse(module1test);
// classpath
List<String> classpathModule1test = getClasspath(module1test);
assertThat(module1testTypesInUse).contains("com.example.module2.main.Module2Class",
"com.example.module2.test.Module2Test", "org.springframework.boot.test.context.SpringBootTest");
assertThat(classpathModule1test).contains("com.example.module1.main.Module1Class",
"com.example.module1.test.Module1Test",
"org.springframework.boot.autoconfigure.SpringBootApplication",
"org.springframework.boot.test.context.SpringBootTest", "com.example.module2.main.Module2Class",
"com.example.module2.test.Module2Test");
}
// @formatter:off
@Language("xml")
private static final String PARENT_POM =
"""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<modules>
<module>module-1</module>
<module>module-2</module>
</modules>
</project>
""";
@Language("xml")
private static final String MODULE_1_POM =
"""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>module-1</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>module-2</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>3.1.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
""";
private static final String MODULE_1_CLASS =
"""
package com.example.module1.main;
import com.example.module2.main.Module2Class;
public class Module1Class {
private Module2Class module2Class;
}
""";
private static final String MODULE_1_TEST =
"""
package com.example.module1.test;
import com.example.module2.main.Module2Class;
import com.example.module2.test.Module2Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Module1Test extends Module2Test {
private Module1Class module1Class;
private Module2Class module2Class;
}
""";
@Language("xml")
private static final String MODULE_2_POM =
"""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>module-2</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>3.1.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
""";
private static final String MODULE_2_CLASS =
"""
package com.example.module2.main;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Module2Class {
}
""";
private static final String MODULE_2_TEST =
"""
package com.example.module2.test;
import com.example.module2.main.Module2Class;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Module2Test {
Module2Class module2Class;
}
""";
// @formatter:on
@NotNull
private static List<String> getClasspath(J.CompilationUnit module2class) {
return module2class.getMarkers()
.findFirst(JavaSourceSet.class)
.get()
.getClasspath()
.stream()
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
}
@NotNull
private static List<String> getTypesInUse(J.CompilationUnit module2class) {
return module2class.getTypesInUse()
.getTypesInUse()
.stream()
.map(JavaType.FullyQualified.class::cast)
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
}
@NotNull
private static J.CompilationUnit getCompilationContainginTypeEndingWith(String endsWith,
RewriteProjectParsingResult parsingResult) {
return (J.CompilationUnit) parsingResult.sourceFiles()
.stream()
.filter(sf -> sf.getSourcePath().toString().endsWith(endsWith))
.findFirst()
.get();
}
}
}

View File

@@ -15,8 +15,9 @@
*/
package org.springframework.rewrite.parsers;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.*;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.jetbrains.annotations.NotNull;
import org.openrewrite.maven.tree.MavenResolutionResult;
import org.openrewrite.maven.tree.ResolvedDependency;
@@ -29,11 +30,9 @@ import org.springframework.rewrite.utils.LinuxWindowsPathUnifier;
import org.springframework.rewrite.utils.ResourceUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.*;
import java.util.function.Predicate;
/**
@@ -43,14 +42,24 @@ public class MavenProject {
private final Path projectRoot;
private final Resource pomFile;
private final MavenBuildFile buildFile;
// FIXME: 945 temporary method, model should nopt come from Maven
private final Model pomModel;
/**
* All {@link MavenProject}s of this build.
*/
private List<MavenProject> reactorProjects = new ArrayList<>();
private List<MavenProject> collectedProjects = new ArrayList<>();
/**
* List of {@link MavenProject}s that depend on this project.
*/
private final List<MavenProject> dependentProjects = new ArrayList<>();
private Xml.Document sourceFile;
/**
* List of {@link MavenProject}s this project depends on.
*/
private final List<MavenProject> dependencyProjects = new ArrayList<>();
private final List<MavenProject> moduleProjects = new ArrayList<>();
private final MavenArtifactDownloader rewriteMavenArtifactDownloader;
@@ -58,35 +67,58 @@ public class MavenProject {
private ProjectId projectId;
public MavenProject(Path projectRoot, Resource pomFile, Model pomModel,
public MavenProject(Path baseDir, Resource rootPom, MavenArtifactDownloader rewriteMavenArtifactDownloader,
List<Resource> resources) {
this(baseDir, rootPom, List.of(), rewriteMavenArtifactDownloader, resources);
}
public MavenProject(Path baseDir, Resource pomFile, List<MavenProject> dependsOnModels,
MavenArtifactDownloader rewriteMavenArtifactDownloader, List<Resource> resources) {
this.projectRoot = projectRoot;
this.pomFile = pomFile;
this.pomModel = pomModel;
this.projectRoot = baseDir;
this.buildFile = new MavenBuildFile(pomFile);
if (dependsOnModels != null) {
this.dependentProjects.addAll(dependsOnModels);
}
this.rewriteMavenArtifactDownloader = rewriteMavenArtifactDownloader;
this.resources = resources;
projectId = new ProjectId(getGroupId(), getArtifactId());
}
public List<MavenProject> getDependentProjects() {
return dependentProjects;
}
public void setDependencyProjects(List<MavenProject> dependencyProjects) {
this.dependencyProjects.clear();
this.dependencyProjects.addAll(dependencyProjects);
}
public List<MavenProject> getDependencyProjects() {
return dependencyProjects;
}
public File getFile() {
return ResourceUtil.getPath(pomFile).toFile();
return buildFile.getPath().toFile();
}
public MavenBuildFile getBuildFile() {
return buildFile;
}
public Path getBasedir() {
// TODO: 945 Check if this is correct
return pomFile == null ? null : ResourceUtil.getPath(pomFile).getParent();
return buildFile == null ? null : buildFile.getPath().getParent();
}
public void setCollectedProjects(List<MavenProject> collected) {
this.collectedProjects = collected;
public void setReactorProjects(List<MavenProject> collected) {
this.reactorProjects = collected;
}
/**
* @return all {@link MavenProject}s belonging to the same reactor build.
*/
public List<MavenProject> getCollectedProjects() {
return collectedProjects;
}
public Resource getResource() {
return pomFile;
return reactorProjects;
}
/**
@@ -103,29 +135,28 @@ public class MavenProject {
if (getBasedir() == null) {
return null;
}
else if ("pom.xml"
.equals(LinuxWindowsPathUnifier.relativize(projectRoot, ResourceUtil.getPath(pomFile)).toString())) {
else if ("pom.xml".equals(LinuxWindowsPathUnifier.relativize(projectRoot, buildFile.getPath()).toString())) {
return Path.of("");
}
else {
return LinuxWindowsPathUnifier.relativize(projectRoot, ResourceUtil.getPath(pomFile)).getParent();
return LinuxWindowsPathUnifier.relativize(projectRoot, buildFile.getPath()).getParent();
}
}
public String getGroupIdAndArtifactId() {
return this.pomModel.getGroupId() + ":" + pomModel.getArtifactId();
return this.buildFile.getGroupIdAndArtifactId();
}
public Path getPomFilePath() {
return ResourceUtil.getPath(pomFile);
return buildFile.getPath();
}
public Plugin getPlugin(String s) {
return pomModel.getBuild() == null ? null : pomModel.getBuild().getPluginsAsMap().get(s);
return buildFile.getBuild() == null ? null : buildFile.getBuild().getPluginsAsMap().get(s);
}
public Properties getProperties() {
return pomModel.getProperties();
return buildFile.getProperties();
}
public MavenRuntimeInformation getMavenRuntimeInformation() {
@@ -134,15 +165,15 @@ public class MavenProject {
}
public String getName() {
return pomModel.getName();
return buildFile.getName();
}
public String getGroupId() {
return pomModel.getGroupId() == null ? pomModel.getParent().getGroupId() : pomModel.getGroupId();
return buildFile.getGroupId() == null ? buildFile.getParent().getGroupId() : buildFile.getGroupId();
}
public String getArtifactId() {
return pomModel.getArtifactId();
return buildFile.getArtifactId();
}
/**
@@ -150,30 +181,25 @@ public class MavenProject {
* version will be null.
*/
public String getVersion() {
return pomModel.getVersion() == null ? pomModel.getParent().getVersion() : pomModel.getVersion();
return buildFile.getVersion() == null ? buildFile.getParent().getVersion() : buildFile.getVersion();
}
@Override
public String toString() {
String groupId = pomModel.getGroupId() == null ? pomModel.getParent().getGroupId() : pomModel.getGroupId();
return groupId + ":" + pomModel.getArtifactId();
String groupId = buildFile.getGroupId() == null ? buildFile.getParent().getGroupId() : buildFile.getGroupId();
return groupId + ":" + buildFile.getArtifactId();
}
public String getBuildDirectory() {
String s = pomModel.getBuild() != null ? pomModel.getBuild().getDirectory() : null;
return s == null
? ResourceUtil.getPath(pomFile).getParent().resolve("target").toAbsolutePath().normalize().toString()
String s = buildFile.getBuild() != null ? buildFile.getBuild().getDirectory() : null;
return s == null ? buildFile.getPath().getParent().resolve("target").toAbsolutePath().normalize().toString()
: s;
}
public String getSourceDirectory() {
String s = pomModel.getBuild() != null ? pomModel.getBuild().getSourceDirectory() : null;
return s == null ? ResourceUtil.getPath(pomFile)
.getParent()
.resolve("src/main/java")
.toAbsolutePath()
.normalize()
.toString() : s;
String s = buildFile.getBuild() != null ? buildFile.getBuild().getSourceDirectory() : null;
return s == null
? buildFile.getPath().getParent().resolve("src/main/java").toAbsolutePath().normalize().toString() : s;
}
public List<Path> getCompileClasspathElements() {
@@ -187,7 +213,20 @@ public class MavenProject {
@NotNull
private List<Path> getClasspathElements(Scope scope) {
MavenResolutionResult pom = getSourceFile().getMarkers().findFirst(MavenResolutionResult.class).get();
Xml.Document pomSourceFile = getSourceFile();
return getClasspathJars(scope, pomSourceFile);
}
@NotNull
private List<Path> getClasspathJars(Scope scope, Xml.Document pomSourceFile) {
MavenArtifactDownloader downloader = rewriteMavenArtifactDownloader;
return getClasspathJars(scope, pomSourceFile, downloader);
}
@NotNull
public static List<Path> getClasspathJars(Scope scope, Xml.Document pomSourceFile,
MavenArtifactDownloader downloader) {
MavenResolutionResult pom = pomSourceFile.getMarkers().findFirst(MavenResolutionResult.class).get();
List<ResolvedDependency> resolvedDependencies = pom.getDependencies().get(scope);
if (resolvedDependencies != null) {
return resolvedDependencies
@@ -195,7 +234,7 @@ public class MavenProject {
//
.stream()
.filter(rd -> rd.getRepository() != null)
.map(rd -> rewriteMavenArtifactDownloader.downloadArtifact(rd))
.map(rd -> downloader.downloadArtifact(rd))
.filter(Objects::nonNull)
.distinct()
.toList();
@@ -206,17 +245,13 @@ public class MavenProject {
}
public String getTestSourceDirectory() {
String s = pomModel.getBuild() != null ? pomModel.getBuild().getSourceDirectory() : null;
return s == null ? ResourceUtil.getPath(pomFile)
.getParent()
.resolve("src/test/java")
.toAbsolutePath()
.normalize()
.toString() : s;
String s = buildFile.getBuild() != null ? buildFile.getBuild().getSourceDirectory() : null;
return s == null
? buildFile.getPath().getParent().resolve("src/test/java").toAbsolutePath().normalize().toString() : s;
}
public void setSourceFile(Xml.Document sourceFile) {
this.sourceFile = sourceFile;
this.buildFile.setSourceFile(sourceFile);
}
private static List<Resource> listJavaSources(List<Resource> resources, Path sourceDirectory) {
@@ -245,6 +280,9 @@ public class MavenProject {
return this.resources;
}
/**
* @return All {@link Resource}s found under {@code src/main/java} of this project.
*/
public List<Resource> getMainJavaSources() {
Path sourceDir = getProjectRoot().resolve(getModuleDir()).resolve("src/main/java");
return listJavaSources(resources, sourceDir);
@@ -259,31 +297,316 @@ public class MavenProject {
}
public Object getProjectEncoding() {
return getPomModel().getProperties().get("project.build.sourceEncoding");
return buildFile.getProperties().get("project.build.sourceEncoding");
}
public Path getProjectRoot() {
return projectRoot;
}
@Deprecated
public Resource getPomFile() {
return pomFile;
}
public Model getPomModel() {
return pomModel;
return buildFile.getPomFileResource();
}
public Xml.Document getSourceFile() {
return sourceFile;
return buildFile.getSourceFile();
}
public MavenArtifactDownloader getRewriteMavenArtifactDownloader() {
return rewriteMavenArtifactDownloader;
public boolean dependsOn(MavenProject model) {
return dependentProjects.stream()
.anyMatch(
m -> m.getGroupId().equals(model.getGroupId()) && m.getArtifactId().equals(model.getArtifactId()));
}
public void setProjectId(ProjectId projectId) {
this.projectId = projectId;
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
MavenProject that = (MavenProject) o;
return Objects.equals(buildFile, that.buildFile);
}
@Override
public int hashCode() {
return Objects.hash(buildFile);
}
public class MavenBuildFile extends org.apache.maven.model.Model {
private final Resource pomFileResource;
// FIXME: 945 temporary method, model should nopt come from Maven
@Deprecated
private final Resource resource;
private Xml.Document sourceFile;
private final org.apache.maven.model.Model delegate;
private static final MavenXpp3Reader XPP_3_READER = new MavenXpp3Reader();
public MavenBuildFile(Resource pomFileResource) {
this.pomFileResource = pomFileResource;
assertPomFile(pomFileResource);
this.resource = pomFileResource;
try {
this.delegate = XPP_3_READER.read(ResourceUtil.getInputStream(resource));
this.delegate.setPomFile(resource.getFile());
List<Dependency> dependencies = this.delegate.getDependencies();
dependencies.forEach(d -> {
});
}
catch (IOException e) {
throw new RuntimeException(e);
}
catch (XmlPullParserException e) {
throw new RuntimeException(e);
}
}
private void assertPomFile(Resource resource) {
if (!LinuxWindowsPathUnifier.unifiedPathString(resource).endsWith("pom.xml")) {
throw new IllegalArgumentException(
"Provided resource '%s' is not a pom.xml file.".formatted(ResourceUtil.getPath(resource)));
}
}
public void setSourceFile(Xml.Document sourceFile) {
this.sourceFile = sourceFile;
}
public Xml.Document getSourceFile() {
return sourceFile;
}
public String getContent() {
return ResourceUtil.getContent(pomFileResource);
}
public Path getPath() {
return ResourceUtil.getPath(pomFileResource);
}
public Resource getPomFileResource() {
return pomFileResource;
}
// Model methods
@Override
public String toString() {
return (delegate.getGroupId() == null ? delegate.getParent().getGroupId() : delegate.getGroupId()) + ":"
+ delegate.getArtifactId();
}
@Override
public String getArtifactId() {
return delegate.getArtifactId();
}
@Override
public Build getBuild() {
return delegate.getBuild();
}
@Override
public String getChildProjectUrlInheritAppendPath() {
return delegate.getChildProjectUrlInheritAppendPath();
}
@Override
public CiManagement getCiManagement() {
return delegate.getCiManagement();
}
@Override
public List<Contributor> getContributors() {
return delegate.getContributors();
}
@Override
public String getDescription() {
return delegate.getDescription();
}
@Override
public List<Developer> getDevelopers() {
return delegate.getDevelopers();
}
@Override
public String getGroupId() {
return delegate.getGroupId();
}
@Override
public String getInceptionYear() {
return delegate.getInceptionYear();
}
@Override
public IssueManagement getIssueManagement() {
return delegate.getIssueManagement();
}
@Override
public List<License> getLicenses() {
return delegate.getLicenses();
}
@Override
public List<MailingList> getMailingLists() {
return delegate.getMailingLists();
}
@Override
public String getModelEncoding() {
return delegate.getModelEncoding();
}
@Override
public String getModelVersion() {
return delegate.getModelVersion();
}
@Override
public String getName() {
String name = delegate.getName();
if (name == null) {
name = delegate.getArtifactId();
}
return name;
}
@Override
public Organization getOrganization() {
return delegate.getOrganization();
}
@Override
public String getPackaging() {
return delegate.getPackaging();
}
@Override
public Parent getParent() {
return delegate.getParent();
}
@Override
public Prerequisites getPrerequisites() {
return delegate.getPrerequisites();
}
@Override
public List<Profile> getProfiles() {
return delegate.getProfiles();
}
@Override
public Scm getScm() {
return delegate.getScm();
}
@Override
public String getUrl() {
return delegate.getUrl();
}
@Override
public String getVersion() {
return delegate.getVersion();
}
@Override
public File getPomFile() {
return delegate.getPomFile();
}
@Override
public File getProjectDirectory() {
return delegate.getPomFile().toPath().getParent().toFile();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public List<Dependency> getDependencies() {
return delegate.getDependencies();
}
@Override
public DependencyManagement getDependencyManagement() {
return delegate.getDependencyManagement();
}
@Override
public DistributionManagement getDistributionManagement() {
return delegate.getDistributionManagement();
}
@Override
public InputLocation getLocation(Object key) {
return delegate.getLocation(key);
}
@Override
public List<String> getModules() {
return delegate.getModules();
}
@Override
public List<Repository> getPluginRepositories() {
return delegate.getPluginRepositories();
}
@Override
public Properties getProperties() {
return delegate.getProperties();
}
@Override
public Reporting getReporting() {
return delegate.getReporting();
}
@Override
public Object getReports() {
return delegate.getReports();
}
@Override
public List<Repository> getRepositories() {
return delegate.getRepositories();
}
public String getGroupIdAndArtifactId() {
return getGroupId() + ":" + getArtifactId();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
MavenBuildFile that = (MavenBuildFile) o;
Path thisPath = ResourceUtil.getPath(this.pomFileResource);
Path thatPath = ResourceUtil.getPath(that.pomFileResource);
return Objects.equals(thisPath, thatPath);
}
@Override
public int hashCode() {
return Objects.hash(pomFileResource);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite.parsers;
import org.openrewrite.SourceFile;
import java.util.ArrayList;
import java.util.List;
/**
* @author Fabian Krüger
*/
public record ModuleParsingResult(MavenProject currentProject, SourceSetParsingResult mainSourcesParsingResult,
SourceSetParsingResult testSourcesParsingResult, List<SourceFile> resourceFilesList) {
public List<? extends SourceFile> sourceFiles() {
List<SourceFile> allSourceFiles = new ArrayList<>();
allSourceFiles.addAll(mainSourcesParsingResult.sourceFiles());
allSourceFiles.addAll(testSourcesParsingResult.sourceFiles());
allSourceFiles.addAll(resourceFilesList);
return allSourceFiles;
}
}

View File

@@ -102,7 +102,10 @@ public class RewriteParserConfiguration {
@Bean
MavenProjectAnalyzer mavenProjectAnalyzer(MavenArtifactDownloader artifactDownloader) {
return new MavenProjectAnalyzer(artifactDownloader);
MavenProjectGraph mavenProjectGraph = new MavenProjectGraph();
MavenProjectSorter mavenProjectSorter = new MavenProjectSorter(mavenProjectGraph);
MavenProjectFactory mavenProjectFactory = new MavenProjectFactory(artifactDownloader);
return new MavenProjectAnalyzer(mavenProjectSorter, mavenProjectFactory);
}
@Bean

View File

@@ -35,7 +35,6 @@ import org.springframework.rewrite.parsers.maven.MavenProjectAnalyzer;
import org.springframework.rewrite.parsers.maven.ProvenanceMarkerFactory;
import org.springframework.rewrite.recipes.RewriteRecipeDiscovery;
import org.springframework.rewrite.scopes.ScanScope;
import org.springframework.rewrite.utils.LinuxWindowsPathUnifier;
import org.springframework.util.StringUtils;
import java.nio.file.Path;
@@ -140,7 +139,8 @@ public class RewriteProjectParser {
List<NamedStyles> styles = List.of();
// Get the ordered otherSourceFiles of projects
ParserContext parserContext = mavenProjectAnalyzer.createParserContext(baseDir, resources);
List<MavenProject> sortedProjects = mavenProjectAnalyzer.getBuildProjects(baseDir, resources);
ParserContext parserContext = new ParserContext(baseDir, resources, sortedProjects);
// generate provenance
Map<Path, List<Marker>> provenanceMarkers = provenanceMarkerFactory.generateProvenanceMarkers(baseDir,

View File

@@ -46,6 +46,9 @@ public class SourceFileParser {
Set<SourceFile> parsedSourceFiles = new LinkedHashSet<>();
// we use the map to look up previous parsing results when building the classpath
// of a module
Map<MavenProject, ModuleParsingResult> parsingResultsMap = new HashMap<>();
parserContext.getSortedProjects().forEach(currentMavenProject -> {
Xml.Document moduleBuildFile = currentMavenProject.getSourceFile();
List<Marker> markers = provenanceMarkers.get(currentMavenProject.getPomFilePath());
@@ -53,9 +56,30 @@ public class SourceFileParser {
LOGGER.warn("Could not find provenance markers for resource '%s'"
.formatted(parserContext.getMatchingBuildFileResource(currentMavenProject)));
}
List<SourceFile> sourceFiles = moduleParser.parseModuleSourceFiles(resources, currentMavenProject,
moduleBuildFile, markers, styles, executionContext, baseDir);
parsedSourceFiles.addAll(sourceFiles);
ModuleParsingResult result = moduleParser.parseModule(baseDir, resources, currentMavenProject,
moduleBuildFile, markers, styles, executionContext, parsingResultsMap);
parsingResultsMap.put(currentMavenProject, result);
// Maybe..
// Return ModuleParsingResult
// mpr.getMainClasspath()
// mpr.getTestClasspath()
// if(currentMavenProject.dependsOn(mpr.getModule())
// requirements:
// - provide jars that define the classpath
// - provide classes from (transitive) module(s)
// Retrieve and append the shared classpath from previously parsed modules
Set<Path> classpath = new HashSet<>();
Map<MavenProject, Set<Path>> modelClasspathMap = new HashMap<>();
currentMavenProject.getDependentProjects().forEach(m -> {
Set<Path> dependencyPaths = modelClasspathMap.get(m);
classpath.addAll(dependencyPaths);
});
// TODO: provide the classpath to ModuleParser
parsedSourceFiles.addAll(result.sourceFiles());
});
return new ArrayList<>(parsedSourceFiles);

View File

@@ -16,6 +16,7 @@
package org.springframework.rewrite.parsers;
import org.openrewrite.SourceFile;
import org.openrewrite.java.internal.JavaTypeCache;
import org.openrewrite.java.tree.JavaType;
import java.util.List;
@@ -23,5 +24,6 @@ import java.util.List;
/**
* @author Fabian Krüger
*/
public record SourceSetParsingResult(List<SourceFile> sourceFiles, List<JavaType.FullyQualified> classpath) {
public record SourceSetParsingResult(List<SourceFile> sourceFiles, List<JavaType.FullyQualified> classpath,
JavaTypeCache typeCache) {
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite.parsers.maven;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.io.Resource;
import org.springframework.rewrite.utils.ResourceUtil;
import java.util.List;
/**
* @author Fabian Krüger
*/
class MavenBuildFileFilter {
private static final String POM_XML = "pom.xml";
@NotNull
static List<Resource> filterBuildFiles(List<Resource> resources) {
return resources.stream()
.filter(r -> ResourceUtil.getPath(r).getFileName().toString().equals(POM_XML))
.toList();
}
}

View File

@@ -67,9 +67,9 @@ public class MavenModuleParser {
this.springRewriteProperties = springRewriteProperties;
}
public List<SourceFile> parseModuleSourceFiles(List<Resource> resources, MavenProject currentProject,
public ModuleParsingResult parseModule(Path baseDir, List<Resource> resources, MavenProject currentProject,
Xml.Document moduleBuildFile, List<Marker> provenanceMarkers, List<NamedStyles> styles,
ExecutionContext executionContext, Path baseDir) {
ExecutionContext executionContext, Map<MavenProject, ModuleParsingResult> parsingResultsMap) {
List<SourceFile> sourceFiles = new ArrayList<>();
// 146:149: get source encoding from maven
@@ -85,9 +85,10 @@ public class MavenModuleParser {
.setCharset(Charset.forName(mavenSourceEncoding.toString()));
}
boolean logCompilationWarningsAndErrors = springRewriteProperties.isLogCompilationWarningsAndErrors();
JavaParser.Builder<? extends JavaParser, ?> javaParserBuilder = JavaParser.fromJavaVersion()
.styles(styles)
.logCompilationWarningsAndErrors(springRewriteProperties.isLogCompilationWarningsAndErrors());
.logCompilationWarningsAndErrors(logCompilationWarningsAndErrors);
Path buildFilePath = currentProject.getBasedir().resolve(moduleBuildFile.getSourcePath());
LOGGER.info("Parsing module " + buildFilePath);
@@ -102,12 +103,12 @@ public class MavenModuleParser {
alreadyParsed.add(moduleBuildFilePath);
alreadyParsed.addAll(skipResourceScanDirs);
SourceSetParsingResult mainSourcesParsingResult = processMainSources(baseDir, resources, moduleBuildFile,
javaParserBuilder, rp, provenanceMarkers, alreadyParsed, executionContext, currentProject);
SourceSetParsingResult mainSourcesParsingResult = parseMainSourceSet(baseDir, currentProject, javaParserBuilder,
parsingResultsMap, executionContext, alreadyParsed, provenanceMarkers, resources, rp);
SourceSetParsingResult testSourcesParsingResult = processTestSources(baseDir, moduleBuildFile,
javaParserBuilder, rp, provenanceMarkers, alreadyParsed, executionContext, currentProject, resources,
mainSourcesParsingResult.classpath());
SourceSetParsingResult testSourcesParsingResult = parseTestSourceSet(baseDir, currentProject, javaParserBuilder,
parsingResultsMap, executionContext, alreadyParsed, provenanceMarkers, resources, rp,
mainSourcesParsingResult);
// Collect the dirs of modules parsed in previous steps
// parse other project resources
@@ -122,7 +123,199 @@ public class MavenModuleParser {
sourceFiles.addAll(mainAndTestSources);
sourceFiles.addAll(resourceFilesList);
return sourceFiles;
ModuleParsingResult moduleParsingResult = new ModuleParsingResult(currentProject, mainSourcesParsingResult,
testSourcesParsingResult, resourceFilesList);
return moduleParsingResult;
}
/**
* Parse main source set {@code src/main} from current module. The classpath for Java
* sources is created from jars and compilation units of dependency project previously
* parsed. The parsed java sources are collected and provided with the result and can
* be used by subsequent parse to build tha classpath.
*/
SourceSetParsingResult parseMainSourceSet(@Nullable Path baseDir, MavenProject currentProject,
JavaParser.Builder<? extends JavaParser, ?> javaParserBuilder,
Map<MavenProject, ModuleParsingResult> parsingResultsMap, ExecutionContext executionContext,
Set<Path> alreadyParsed, List<Marker> provenanceMarkers, List<Resource> resources,
RewriteResourceParser rp) {
// collect and prepare all types for classpath and TypeCache
// java sources in current source set
List<Resource> javaSourcesInSrc = currentProject.getMainJavaSources();
// jars from dependencies
List<Path> classpathJars = currentProject.getCompileClasspathElements();
LOGGER.debug("Dependencies on main classpath: %s".formatted(classpathJars));
javaParserBuilder.classpath(classpathJars);
// sources from other dependency modules
List<SourceFile> sourceFilesFromOtherModules = currentProject.getDependencyProjects()
.stream()
// get their parsing result
.map(project -> parsingResultsMap.get(project))
.flatMap(result -> result.mainSourcesParsingResult().sourceFiles().stream())
.toList();
String[] dependsOnSources = sourceFilesFromOtherModules.stream()
.map(SourceFile::printAll)
.toArray(String[]::new);
javaParserBuilder.dependsOn(dependsOnSources);
JavaTypeCache typeCache = getJavaTypeCache(currentProject, parsingResultsMap, sourceFilesFromOtherModules);
javaParserBuilder.typeCache(typeCache);
Set<JavaType.FullyQualified> sourceSetClassesCp = new HashSet<>();
// Add test sources from dependency projects to classpath
sourceFilesFromOtherModules.stream()
.filter(J.CompilationUnit.class::isInstance)
.map(J.CompilationUnit.class::cast)
.flatMap(s -> s.getClasses().stream())
.map(J.ClassDeclaration::getType)
.forEach(sourceSetClassesCp::add);
return parseSourceSet(baseDir, currentProject, javaSourcesInSrc, javaParserBuilder, sourceSetClassesCp,
executionContext, alreadyParsed, classpathJars, typeCache, provenanceMarkers, "main", resources, rp,
"src/main");
}
/**
* Parse test source set {@code src/test} from current module. The classpath for Java
* sources is created from jars and compilation units of dependency project previously
* parsed. The parsed java sources are collected and provided with the result and can
* be used by subsequent parse to build tha classpath.
*/
SourceSetParsingResult parseTestSourceSet(@Nullable Path baseDir, MavenProject currentProject,
JavaParser.Builder<? extends JavaParser, ?> javaParserBuilder,
Map<MavenProject, ModuleParsingResult> parsingResultsMap, ExecutionContext executionContext,
Set<Path> alreadyParsed, List<Marker> provenanceMarkers, List<Resource> resources, RewriteResourceParser rp,
SourceSetParsingResult mainSourcesParsingResult) {
// collect and prepare all types for classpath and TypeCache
// java sources in current source set
List<Resource> javaSourcesInSrc = currentProject.getTestJavaSources();
// jars from dependencies
List<Path> classpathJars = currentProject.getTestClasspathElements();
LOGGER.debug("Dependencies on main classpath: %s".formatted(classpathJars));
javaParserBuilder.classpath(classpathJars);
// sources from other dependency modules
List<SourceFile> sourceFilesFromOtherModules = currentProject.getDependencyProjects()
.stream()
// get their parsing result
.map(project -> parsingResultsMap.get(project))
.flatMap(result -> {
return Stream.concat(result.mainSourcesParsingResult().sourceFiles().stream(),
result.testSourcesParsingResult().sourceFiles().stream());
})
.toList();
List<SourceFile> sourceFilesFromMain = mainSourcesParsingResult.sourceFiles();
String[] dependsOnSources = Stream.concat(sourceFilesFromMain.stream(), sourceFilesFromOtherModules.stream())
.map(SourceFile::printAll)
.toArray(String[]::new);
javaParserBuilder.dependsOn(dependsOnSources);
JavaTypeCache typeCache = getJavaTypeCache(currentProject, parsingResultsMap, sourceFilesFromOtherModules);
javaParserBuilder.typeCache(typeCache);
Set<JavaType.FullyQualified> sourceSetClassesCp = new HashSet<>();
// Add test sources from dependency projects to classpath
Stream.concat(sourceFilesFromMain.stream(), sourceFilesFromOtherModules.stream())
.filter(J.CompilationUnit.class::isInstance)
.map(J.CompilationUnit.class::cast)
.flatMap(s -> s.getClasses().stream())
.map(J.ClassDeclaration::getType)
.forEach(sourceSetClassesCp::add);
return parseSourceSet(baseDir, currentProject, javaSourcesInSrc, javaParserBuilder, sourceSetClassesCp,
executionContext, alreadyParsed, classpathJars, typeCache, provenanceMarkers, "test", resources, rp,
"src/test");
}
SourceSetParsingResult parseSourceSet(@Nullable Path baseDir, MavenProject currentProject,
List<Resource> javaSourcesInSrc, JavaParser.Builder<? extends JavaParser, ?> javaParserBuilder,
Set<JavaType.FullyQualified> localClassesCp, ExecutionContext executionContext, Set<Path> alreadyParsed,
List<Path> classpathJars, JavaTypeCache typeCache, List<Marker> provenanceMarkers, String sourceSetName,
List<Resource> resources, RewriteResourceParser rp, String sourceDir) {
// collect source files from module src dir
List<Resource> javaSources = new ArrayList<>();
List<Resource> javaSourcesInTarget = currentProject.getJavaSourcesInTarget();
javaSources.addAll(javaSourcesInTarget);
javaSources.addAll(javaSourcesInSrc);
Iterable<Parser.Input> inputs = javaSources.stream().map(r -> {
FileAttributes fileAttributes = null;
Path path = ResourceUtil.getPath(r);
boolean isSynthetic = Files.exists(path);
Supplier<InputStream> inputStreamSupplier = () -> ResourceUtil.getInputStream(r);
Parser.Input input = new Parser.Input(path, fileAttributes, inputStreamSupplier, isSynthetic);
return input;
}).toList();
// collecting parsed compilation units to the classpath (localClassesCp).
List<? extends SourceFile> cus = javaParserBuilder.build()
.parseInputs(inputs, baseDir, executionContext)
.peek(s -> {
((J.CompilationUnit) s).getClasses()
.stream()
.map(J.ClassDeclaration::getType)
.forEach(localClassesCp::add);
alreadyParsed.add(baseDir.resolve(s.getSourcePath()));
})
.toList();
JavaSourceSet javaSourceSet = sourceSet(sourceSetName, classpathJars, typeCache);
List<Marker> markers = new ArrayList<>(provenanceMarkers);
javaSourceSet = appendToClasspath(localClassesCp, javaSourceSet);
ClasspathDependencies classpathDependencies = new ClasspathDependencies(classpathJars);
markers.add(javaSourceSet);
markers.add(classpathDependencies);
List<Path> parsedJavaPaths = javaSourcesInTarget.stream().map(ResourceUtil::getPath).toList();
Stream<SourceFile> parsedJavaSources = cus.stream().map(addProvenance(baseDir, markers, parsedJavaPaths));
LOGGER.debug("[%s] Scanned %d java source files in main scope.".formatted(currentProject, javaSources.size()));
// Filter out any generated source files from the returned list, as we do not want
// to apply the recipe to the
// generated files.
Path buildDirectory = LinuxWindowsPathUnifier.unifiedPath(Paths.get(currentProject.getBuildDirectory()));
List<SourceFile> filteredJavaSources = filterOutResourcesInDir(parsedJavaSources, buildDirectory);
int sourcesParsedBefore = alreadyParsed.size();
alreadyParsed.addAll(parsedJavaPaths);
List<Resource> resourcesLeft = resources.stream()
.filter(r -> alreadyParsed.stream().noneMatch(path -> LinuxWindowsPathUnifier.pathStartsWith(r, path)))
.toList();
LOGGER.info("Parsing test resources");
Path searchDir = currentProject.getModulePath().resolve(sourceDir).resolve("resources");
List<SourceFile> parsedResourceFiles = rp
.parseSourceFiles(searchDir, resourcesLeft, alreadyParsed, executionContext)
.map(addProvenance(baseDir, markers, null))
.toList();
LOGGER.info("Parsed %d main resources".formatted(parsedResourceFiles.size()));
// TODO: Remove
// List<SourceFile> parsedResourceFiles = rp
// .parse(currentProject.getModulePath().resolve("src/main/resources"), resources,
// alreadyParsed)
// .map(addProvenance(baseDir, mainProjectProvenance, null))
// .toList();
LOGGER.debug("[%s] Scanned %d resource files in main scope.".formatted(currentProject,
(alreadyParsed.size() - sourcesParsedBefore)));
// Any resources parsed from "main/resources" should also have the main source set
// added to them.
filteredJavaSources.addAll(parsedResourceFiles);
return new SourceSetParsingResult(filteredJavaSources, javaSourceSet.getClasspath(), typeCache);
}
/**
@@ -163,134 +356,26 @@ public class MavenModuleParser {
private Set<Path> pathsToOtherMavenProjects(MavenProject mavenProject, Path moduleBuildFile) {
return mavenProject.getCollectedProjects()
.stream()
.filter(p -> !p.getFile().toPath().toString().equals(moduleBuildFile.toString()))
.filter(p -> !LinuxWindowsPathUnifier.pathEquals(p.getBuildFile().getPath(), moduleBuildFile))
.map(p -> p.getFile().toPath().getParent())
.collect(Collectors.toSet());
}
/**
* Parse Java sources and resources under {@code src/main} of current module.
*/
public SourceSetParsingResult processMainSources(Path baseDir, List<Resource> resources,
Xml.Document moduleBuildFile, JavaParser.Builder<? extends JavaParser, ?> javaParserBuilder,
RewriteResourceParser rp, List<Marker> provenanceMarkers, Set<Path> alreadyParsed,
ExecutionContext executionContext, MavenProject currentProject) {
LOGGER.info("Processing main sources in module '%s'".formatted(currentProject.getProjectId()));
// FIXME: 945
// Some annotation processors output generated sources to the /target directory.
// These are added for parsing but
// should be filtered out of the final SourceFile list.
List<Resource> mainJavaSources = new ArrayList<>();
List<Resource> javaSourcesInTarget = currentProject.getJavaSourcesInTarget(); // listJavaSources(resources,
// currentProject.getBasedir().resolve(currentProject.getBuildDirectory()));
List<Resource> javaSourcesInMain = currentProject.getMainJavaSources(); // listJavaSources(resources,
// currentProject.getBasedir().resolve(currentProject.getSourceDirectory()));
mainJavaSources.addAll(javaSourcesInTarget);
mainJavaSources.addAll(javaSourcesInMain);
LOGGER.info("[%s] Parsing main source files".formatted(currentProject));
// FIXME 945 classpath
// - Resolve dependencies to non-reactor projects from Maven repository
// - Resolve dependencies to reactor projects by providing the sources
// javaParserBuilder.classpath(byte[])
// we're processing a module here. The classpath of the module consists of all
// declared dependencies and their transitive dependencies too.
// For dependencies to projects that belong to the current rector...
// They'd either need to be built with Maven before to guarantee that the jars are
// installed to local Maven repo.
// Or, the classpath must be created from the sources of the project.
List<Path> dependencies = currentProject.getCompileClasspathElements();
javaParserBuilder.classpath(dependencies);
LOGGER.info("Dependencies on main classpath: %s".formatted(dependencies));
JavaTypeCache typeCache = new JavaTypeCache();
javaParserBuilder.typeCache(typeCache);
Iterable<Parser.Input> inputs = mainJavaSources.stream().map(r -> {
FileAttributes fileAttributes = null;
Path path = ResourceUtil.getPath(r);
boolean isSynthetic = Files.exists(path);
Supplier<InputStream> inputStreamSupplier = () -> ResourceUtil.getInputStream(r);
Parser.Input input = new Parser.Input(path, fileAttributes, inputStreamSupplier, isSynthetic);
return input;
}).toList();
LOGGER.info("Parsing main Java sources.");
Set<JavaType.FullyQualified> localClassesCp = new HashSet<>();
List<? extends SourceFile> cus = javaParserBuilder.build()
.parseInputs(inputs, baseDir, executionContext)
.peek(s -> {
((J.CompilationUnit) s).getClasses()
.stream()
.map(J.ClassDeclaration::getType)
.forEach(localClassesCp::add);
alreadyParsed.add(baseDir.resolve(s.getSourcePath()));
})
.toList();
LOGGER.info("Parsed %d main Java source files.".formatted(cus.size()));
// TODO: This is a hack:
// Parsed java sources are not themselves on the classpath (here).
// The actual parsing happens when the stream is terminated (toList),
// therefore the toList() must be called before the parsed compilation units can
// be added to the classpath
JavaSourceSet javaSourceSet = sourceSet("main", dependencies, typeCache);
List<Marker> mainProjectProvenance = new ArrayList<>(provenanceMarkers);
javaSourceSet = appendToClasspath(localClassesCp, javaSourceSet);
mainProjectProvenance.add(javaSourceSet);
ClasspathDependencies classpathDependencies = new ClasspathDependencies(dependencies);
mainProjectProvenance.add(classpathDependencies);
List<Path> parsedJavaPaths = javaSourcesInTarget.stream().map(ResourceUtil::getPath).toList();
Stream<SourceFile> parsedMainJava = cus.stream()
.map(addProvenance(baseDir, mainProjectProvenance, parsedJavaPaths));
LOGGER.debug(
"[%s] Scanned %d java source files in main scope.".formatted(currentProject, mainJavaSources.size()));
// Filter out any generated source files from the returned list, as we do not want
// to apply the recipe to the
// generated files.
Path buildDirectory = LinuxWindowsPathUnifier.unifiedPath(Paths.get(currentProject.getBuildDirectory()));
List<SourceFile> filteredMainJava = filterOutResourcesInDir(parsedMainJava, buildDirectory);
int sourcesParsedBefore = alreadyParsed.size();
alreadyParsed.addAll(parsedJavaPaths);
List<Resource> resourcesLeft = resources.stream()
.filter(r -> alreadyParsed.stream().noneMatch(path -> LinuxWindowsPathUnifier.pathStartsWith(r, path)))
.toList();
LOGGER.info("Parsing main resources");
List<SourceFile> parsedResourceFiles = rp
.parseSourceFiles(currentProject.getModulePath().resolve("src/main/resources"), resourcesLeft,
alreadyParsed, executionContext)
.map(addProvenance(baseDir, mainProjectProvenance, null))
.toList();
LOGGER.info("Parsed %d main resources".formatted(parsedResourceFiles.size()));
// TODO: Remove
// List<SourceFile> parsedResourceFiles = rp
// .parse(currentProject.getModulePath().resolve("src/main/resources"), resources,
// alreadyParsed)
// .map(addProvenance(baseDir, mainProjectProvenance, null))
// .toList();
LOGGER.debug("[%s] Scanned %d resource files in main scope.".formatted(currentProject,
(alreadyParsed.size() - sourcesParsedBefore)));
// Any resources parsed from "main/resources" should also have the main source set
// added to them.
filteredMainJava.addAll(parsedResourceFiles);
return new SourceSetParsingResult(filteredMainJava, javaSourceSet.getClasspath());
private static JavaTypeCache getJavaTypeCache(MavenProject currentProject,
Map<MavenProject, ModuleParsingResult> parsingResultsMap, List<SourceFile> sourceFilesFromOtherModules) {
JavaTypeCache typeCache;
if (!sourceFilesFromOtherModules.isEmpty()) {
Optional<JavaTypeCache> optJavaTypeCache = currentProject.getDependencyProjects()
.stream()
.map(mp -> parsingResultsMap.get(mp).mainSourcesParsingResult().typeCache())
.max(Comparator.comparing(JavaTypeCache::size));
typeCache = optJavaTypeCache.orElseThrow(() -> new IllegalStateException(
"No TypeCahche from previous build found for project " + currentProject.getProjectId()));
}
else {
typeCache = new JavaTypeCache();
}
return typeCache;
}
@NotNull
@@ -321,84 +406,4 @@ public class MavenModuleParser {
return JavaSourceSet.build(name, dependencies, typeCache, false);
}
/**
* Parse Java sources and resource files under {@code src/test}.
*/
public SourceSetParsingResult processTestSources(Path baseDir, Xml.Document moduleBuildFile,
JavaParser.Builder<? extends JavaParser, ?> javaParserBuilder, RewriteResourceParser rp,
List<Marker> provenanceMarkers, Set<Path> alreadyParsed, ExecutionContext executionContext,
MavenProject currentProject, List<Resource> resources, List<JavaType.FullyQualified> classpath) {
LOGGER.info("Processing test sources in module '%s'".formatted(currentProject.getProjectId()));
List<Path> testDependencies = currentProject.getTestClasspathElements();
javaParserBuilder.classpath(testDependencies);
JavaTypeCache typeCache = new JavaTypeCache();
javaParserBuilder.typeCache(typeCache);
List<Resource> testJavaSources = currentProject.getTestJavaSources();
// listJavaSources(resources,
// currentProject.getBasedir().resolve(currentProject.getTestSourceDirectory()));
// alreadyParsed.addAll(testJavaSources.stream().map(ResourceUtil::getPath).toList());
Iterable<Parser.Input> inputs = testJavaSources.stream()
.map(r -> new Parser.Input(ResourceUtil.getPath(r), () -> ResourceUtil.getInputStream(r)))
.toList();
final List<JavaType.FullyQualified> localClassesCp = new ArrayList<>();
List<? extends SourceFile> cus = javaParserBuilder.build()
.parseInputs(inputs, baseDir, executionContext)
.peek(s -> {
((J.CompilationUnit) s).getClasses()
.stream()
.map(J.ClassDeclaration::getType)
.forEach(localClassesCp::add);
alreadyParsed.add(baseDir.resolve(s.getSourcePath()));
})
.toList();
List<Marker> markers = new ArrayList<>(provenanceMarkers);
JavaSourceSet javaSourceSet = sourceSet("test", testDependencies, typeCache);
Set<JavaType.FullyQualified> curClasspath = Stream.concat(classpath.stream(), localClassesCp.stream())
.collect(Collectors.toSet());
javaSourceSet = appendToClasspath(curClasspath, javaSourceSet);
markers.add(javaSourceSet);
Stream<SourceFile> parsedJava = cus.stream().map(addProvenance(baseDir, markers, null));
LOGGER.debug(
"[%s] Scanned %d java source files in test scope.".formatted(currentProject, testJavaSources.size()));
Stream<SourceFile> sourceFiles = parsedJava;
// Any resources parsed from "test/resources" should also have the test source set
// added to them.
int sourcesParsedBefore = alreadyParsed.size();
Stream<SourceFile> parsedResourceFiles = rp
.parse(currentProject.getBasedir().resolve("src/test/resources"), resources, alreadyParsed)
.map(addProvenance(baseDir, markers, null));
LOGGER.debug("[%s] Scanned %d resource files in test scope.".formatted(currentProject,
(alreadyParsed.size() - sourcesParsedBefore)));
sourceFiles = Stream.concat(sourceFiles, parsedResourceFiles);
List<SourceFile> result = sourceFiles.toList();
return new SourceSetParsingResult(result, javaSourceSet.getClasspath());
}
// FIXME: 945 take Java sources from resources
private static List<Resource> listJavaSources(List<Resource> resources, Path sourceDirectory) {
return resources.stream()
.filter(whenIn(LinuxWindowsPathUnifier.unifiedPath(sourceDirectory)))
.filter(whenFileNameEndsWithJava())
.toList();
}
@NotNull
private static Predicate<Resource> whenFileNameEndsWithJava() {
return p -> ResourceUtil.getPath(p).getFileName().toString().endsWith(".java");
}
@NotNull
private static Predicate<Resource> whenIn(Path sourceDirectory) {
return r -> ResourceUtil.getPath(r).toString().startsWith(sourceDirectory.toString());
}
}

View File

@@ -15,23 +15,12 @@
*/
package org.springframework.rewrite.parsers.maven;
import org.apache.maven.model.*;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.jetbrains.annotations.NotNull;
import org.openrewrite.maven.utilities.MavenArtifactDownloader;
import org.springframework.core.io.Resource;
import org.springframework.rewrite.parsers.MavenProject;
import org.springframework.rewrite.parsers.ParserContext;
import org.springframework.rewrite.utils.LinuxWindowsPathUnifier;
import org.springframework.rewrite.utils.ResourceUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Implements the ordering of Maven (reactor) build projects. See <a href=
@@ -42,393 +31,37 @@ import java.util.stream.Collectors;
*/
public class MavenProjectAnalyzer {
private static final String POM_XML = "pom.xml";
private final MavenProjectSorter mavenProjectSorter;
private static final MavenXpp3Reader XPP_3_READER = new MavenXpp3Reader();
private final MavenProjectFactory mavenProjectFactory;
private final MavenArtifactDownloader rewriteMavenArtifactDownloader;
public MavenProjectAnalyzer(MavenArtifactDownloader rewriteMavenArtifactDownloader) {
this.rewriteMavenArtifactDownloader = rewriteMavenArtifactDownloader;
public MavenProjectAnalyzer(MavenProjectSorter mavenProjectSorter, MavenProjectFactory mavenProjectFactory) {
this.mavenProjectSorter = mavenProjectSorter;
this.mavenProjectFactory = mavenProjectFactory;
}
public List<MavenProject> getSortedProjects(Path baseDir, List<Resource> resources) {
List<Resource> allPomFiles = resources.stream()
.filter(r -> ResourceUtil.getPath(r).getFileName().toString().equals(POM_XML))
.toList();
if (allPomFiles.isEmpty()) {
throw new IllegalArgumentException("The provided resources did not contain any 'pom.xml' file.");
}
Resource rootPom = allPomFiles.stream()
.filter(r -> LinuxWindowsPathUnifier.pathEquals(r, baseDir.resolve(POM_XML)))
.findFirst()
.orElseThrow(
() -> new IllegalArgumentException("The provided resources do not contain a root 'pom.xml' file."));
Model rootPomModel = new Model(rootPom);
if (isSingleModuleProject(rootPomModel)) {
return List.of(new MavenProject(baseDir, rootPom, rootPomModel, rewriteMavenArtifactDownloader, resources));
}
List<Model> reactorModels = new ArrayList<>();
recursivelyFindReactorModules(baseDir, null, reactorModels, allPomFiles, rootPomModel);
List<Model> sortedModels = sortModels(reactorModels);
return map(baseDir, resources, sortedModels);
public List<MavenProject> getBuildProjects(Path baseDir, List<Resource> resources) {
List<MavenProject> allMavenProjects = mavenProjectFactory.create(baseDir, resources);
List<MavenProject> mavenProjects = mavenProjectSorter.sort(baseDir, allMavenProjects);
return map(baseDir, resources, mavenProjects);
}
private List<MavenProject> map(Path baseDir, List<Resource> resources, List<Model> sortedModels) {
private List<MavenProject> map(Path baseDir, List<Resource> resources, List<MavenProject> sortedModels) {
List<MavenProject> mavenProjects = new ArrayList<>();
sortedModels.stream().filter(Objects::nonNull).forEach(m -> {
sortedModels.stream().filter(Objects::nonNull).forEach(mavenProject -> {
String projectDir = LinuxWindowsPathUnifier
.unifiedPathString(baseDir.resolve(m.getProjectDirectory().toPath()).normalize());
.unifiedPathString(baseDir.resolve(mavenProject.getModuleDir()).normalize());
List<Resource> filteredResources = resources.stream()
.filter(r -> LinuxWindowsPathUnifier.unifiedPathString(r).startsWith(projectDir))
.toList();
MavenProject mavenProject = new MavenProject(baseDir, m.getResource(), m, rewriteMavenArtifactDownloader,
filteredResources);
mavenProjects.add(mavenProject);
});
// set all non parent poms as collected projects for root parent pom
// set all non parent poms as collected projects for root parent p
List<MavenProject> collected = new ArrayList<>(mavenProjects);
collected.remove(0);
mavenProjects.get(0).setCollectedProjects(collected);
mavenProjects.get(0).setReactorProjects(collected);
return mavenProjects;
}
private List<Model> recursivelyFindReactorModules(Path baseDir, String path, List<Model> reactorModels,
List<Resource> allPomFiles, Model pomModel) {
// TODO: verify given module is root pom
if (pomModel != null) {
reactorModels.add(pomModel);
}
else {
throw new IllegalStateException("PomModel was null.");
}
List<String> moduleNames = pomModel.getModules();
moduleNames.stream().forEach(moduleName -> {
String modulePathSegment = path == null ? moduleName : path + "/" + moduleName;
allPomFiles.stream()
.filter(getResourcePredicate(baseDir, modulePathSegment))
.map(Model::new)
.forEach(m -> recursivelyFindReactorModules(baseDir, modulePathSegment, reactorModels, allPomFiles, m)
.stream());
});
return reactorModels;
}
@NotNull
private static Predicate<Resource> getResourcePredicate(Path baseDir, String modulePathSegment) {
return resource -> {
Path pomPath = baseDir.resolve(modulePathSegment).resolve(POM_XML).toAbsolutePath().normalize();
String modulePath = LinuxWindowsPathUnifier.unifiedPathString(pomPath);
Path resourcePath = ResourceUtil.getPath(resource).toAbsolutePath().normalize();
return LinuxWindowsPathUnifier.pathEquals(resourcePath, modulePath);
};
}
public List<Model> sortModels(List<Model> reactorModels) {
List<Model> sortedModels = new ArrayList<>();
Map<String, Model> gaToModelMap = reactorModels.stream().collect(Collectors.toMap(m -> {
return (m.getGroupId() == null ? (m.getParent() == null ? null : m.getParent().getGroupId())
: m.getGroupId()) + ":" + m.getArtifactId();
}, m -> m));
Map<Model, List<Model>> dependsOn = new HashMap<>();
for (Model m : reactorModels) {
addToDependants(m, dependsOn, null);
if (hasParent(m)) {
String parentGa = m.getParent().getGroupId() + ":" + m.getParent().getArtifactId();
Model parentModel = gaToModelMap.get(parentGa);
addToDependants(m, dependsOn, parentModel);
}
for (Dependency d : m.getDependencies()) {
String dependencyGa = getGav(d);
if (gaToModelMap.containsKey(dependencyGa)) {
Model dependencyModel = gaToModelMap.get(dependencyGa);
addToDependants(m, dependsOn, dependencyModel);
}
}
if (m.getBuild() != null && m.getBuild().getPluginsAsMap() != null
&& !m.getBuild().getPluginsAsMap().isEmpty()) {
for (String pluginName : m.getBuild().getPluginsAsMap().keySet()) {
// TODO: find plugin dependencies
}
}
}
ArrayList<Map.Entry<Model, List<Model>>> entries = new ArrayList<>(dependsOn.entrySet());
// sort entries by number of values
entries.stream().sorted((e1, e2) -> {
int compare = Integer.compare(e1.getValue().size(), e2.getValue().size());
if (compare != 0) {
return compare;
}
else {
// with same number of dependencies order by dependant
if (e1.getValue().contains(e2.getKey())) {
return 1;
}
else if (e2.getValue().contains(e1.getKey())) {
return -1;
}
else {
// default is order as given by reactorModels
return Integer.compare(reactorModels.indexOf(e1.getKey()), reactorModels.indexOf(e2.getKey()));
}
}
}).forEach(e -> sortedModels.add(e.getKey()));
return sortedModels;
}
private void addToDependants(Model model, Map<Model, List<Model>> dependsOn, Model dependantModel) {
if (!dependsOn.containsKey(model)) {
dependsOn.put(model, new ArrayList<>());
}
if (dependantModel != null) {
dependsOn.get(model).add(dependantModel);
}
}
private boolean hasParent(Model m) {
return m.getParent() != null;
}
private String getGav(Dependency d) {
return d.getGroupId() + ":" + d.getArtifactId();
}
private static boolean isSingleModuleProject(Model rootPomModel) {
return !rootPomModel.getPackaging().equals("pom");
}
public ParserContext createParserContext(Path baseDir, List<Resource> resources) {
List<MavenProject> sortedProjectsList = getSortedProjects(baseDir, resources);
ParserContext parserContext = new ParserContext(baseDir, resources, sortedProjectsList);
return parserContext;
}
static class Model extends org.apache.maven.model.Model {
private final Resource resource;
private final org.apache.maven.model.Model delegate;
public Model(Resource resource) {
this.resource = resource;
try {
this.delegate = XPP_3_READER.read(ResourceUtil.getInputStream(resource));
this.delegate.setPomFile(resource.getFile());
}
catch (IOException e) {
throw new RuntimeException(e);
}
catch (XmlPullParserException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return (delegate.getGroupId() == null ? delegate.getParent().getGroupId() : delegate.getGroupId()) + ":"
+ delegate.getArtifactId();
}
public Resource getResource() {
return resource;
}
@Override
public String getArtifactId() {
return delegate.getArtifactId();
}
@Override
public Build getBuild() {
return delegate.getBuild();
}
@Override
public String getChildProjectUrlInheritAppendPath() {
return delegate.getChildProjectUrlInheritAppendPath();
}
@Override
public CiManagement getCiManagement() {
return delegate.getCiManagement();
}
@Override
public List<Contributor> getContributors() {
return delegate.getContributors();
}
@Override
public String getDescription() {
return delegate.getDescription();
}
@Override
public List<Developer> getDevelopers() {
return delegate.getDevelopers();
}
@Override
public String getGroupId() {
return delegate.getGroupId();
}
@Override
public String getInceptionYear() {
return delegate.getInceptionYear();
}
@Override
public IssueManagement getIssueManagement() {
return delegate.getIssueManagement();
}
@Override
public List<License> getLicenses() {
return delegate.getLicenses();
}
@Override
public List<MailingList> getMailingLists() {
return delegate.getMailingLists();
}
@Override
public String getModelEncoding() {
return delegate.getModelEncoding();
}
@Override
public String getModelVersion() {
return delegate.getModelVersion();
}
@Override
public String getName() {
String name = delegate.getName();
if (name == null) {
name = delegate.getArtifactId();
}
return name;
}
@Override
public Organization getOrganization() {
return delegate.getOrganization();
}
@Override
public String getPackaging() {
return delegate.getPackaging();
}
@Override
public Parent getParent() {
return delegate.getParent();
}
@Override
public Prerequisites getPrerequisites() {
return delegate.getPrerequisites();
}
@Override
public List<Profile> getProfiles() {
return delegate.getProfiles();
}
@Override
public Scm getScm() {
return delegate.getScm();
}
@Override
public String getUrl() {
return delegate.getUrl();
}
@Override
public String getVersion() {
return delegate.getVersion();
}
@Override
public File getPomFile() {
return delegate.getPomFile();
}
@Override
public File getProjectDirectory() {
return delegate.getPomFile().toPath().getParent().toFile();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public List<Dependency> getDependencies() {
return delegate.getDependencies();
}
@Override
public DependencyManagement getDependencyManagement() {
return delegate.getDependencyManagement();
}
@Override
public DistributionManagement getDistributionManagement() {
return delegate.getDistributionManagement();
}
@Override
public InputLocation getLocation(Object key) {
return delegate.getLocation(key);
}
@Override
public List<String> getModules() {
return delegate.getModules();
}
@Override
public List<Repository> getPluginRepositories() {
return delegate.getPluginRepositories();
}
@Override
public Properties getProperties() {
return delegate.getProperties();
}
@Override
public Reporting getReporting() {
return delegate.getReporting();
}
@Override
public Object getReports() {
return delegate.getReports();
}
@Override
public List<Repository> getRepositories() {
return delegate.getRepositories();
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite.parsers.maven;
import org.jetbrains.annotations.NotNull;
import org.openrewrite.maven.utilities.MavenArtifactDownloader;
import org.springframework.core.io.Resource;
import org.springframework.rewrite.parsers.MavenProject;
import java.nio.file.Path;
import java.util.List;
/**
* @author Fabian Krüger
*/
public class MavenProjectFactory {
private final MavenArtifactDownloader artifactDownloader;
public MavenProjectFactory(MavenArtifactDownloader artifactDownloader) {
this.artifactDownloader = artifactDownloader;
}
public List<MavenProject> create(Path baseDir, List<Resource> projectResources) {
List<Resource> allPomFiles = MavenBuildFileFilter.filterBuildFiles(projectResources);
if (allPomFiles.isEmpty()) {
throw new IllegalArgumentException("The provided resources did not contain any 'pom.xml' file.");
}
return allPomFiles.stream().map(pf -> create(baseDir, pf, projectResources)).toList();
}
@NotNull
public MavenProject create(Path baseDir, Resource pomFile, List<Resource> projectResources) {
return new MavenProject(baseDir, pomFile, artifactDownloader, projectResources);
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite.parsers.maven;
import org.springframework.rewrite.parsers.MavenProject;
import org.springframework.rewrite.parsers.ProjectId;
import org.springframework.rewrite.utils.LinuxWindowsPathUnifier;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
/**
* Create directed acyclic graph from MavenProjects.
*
* @author Fabian Krüger
*/
public class MavenProjectGraph {
private static final String POM_XML = "pom.xml";
private Map<ProjectId, MavenProject> gaToMavenProjectMap = new HashMap<>();
/**
* Create a Maven module dependency graph starting from pom.xml in {@code baseDir}.
* The dependency and dependants for all {@MavenModule}s are added when they are part
* of the build.
*/
public Map<MavenProject, Set<MavenProject>> from(Path baseDir, List<MavenProject> allMavenProjects) {
// build map of projectId -> MavenProject
// this is used to decide if dependencies come from the project
initGaToMavenProjectMap(allMavenProjects);
MavenProject rootProject = findRootProject(baseDir, allMavenProjects);
// the dag with child/parent dependencies
Map<MavenProject, Set<MavenProject>> dag = new HashMap<>();
buildDependencyGraph(rootProject, allMavenProjects, dag);
enrichGraphWithDependencies(dag);
return dag;
}
private void enrichGraphWithDependencies(Map<MavenProject, Set<MavenProject>> dag) {
dag.keySet().stream().forEach(curProject -> {
List<MavenProject> dependencyProjects = new ArrayList<>();
curProject.getBuildFile().getDependencies().forEach(d -> {
ProjectId projectId = new ProjectId(d.getGroupId(), d.getArtifactId());
if (gaToMavenProjectMap.containsKey(projectId)) {
MavenProject dependencyMavenProject = gaToMavenProjectMap.get(projectId);
dependencyProjects.add(dependencyMavenProject);
}
});
curProject.setDependencyProjects(dependencyProjects);
});
dag.keySet().stream().forEach(curProject -> {
Set<MavenProject> dependentProjects = dag.get(curProject);
curProject.getBuildFile().getDependencies().stream().forEach(dependency -> {
ProjectId projectId = new ProjectId(dependency.getGroupId(), dependency.getArtifactId());
if (gaToMavenProjectMap.containsKey(projectId)) {
MavenProject dependantProject = gaToMavenProjectMap.get(projectId);
if (dag.containsKey(dependantProject)) {
dependentProjects.add(dependantProject);
}
}
});
});
}
private void initGaToMavenProjectMap(List<MavenProject> allPomFiles) {
gaToMavenProjectMap = allPomFiles.stream()
.collect(Collectors.toMap(mp -> new ProjectId(mp.getGroupId(), mp.getArtifactId()), mp -> mp));
}
private void buildDependencyGraph(MavenProject currentProject, List<MavenProject> reactorProjects,
Map<MavenProject, Set<MavenProject>> dag) {
if (isSingleModuleProject(reactorProjects)) {
logDependentProject(currentProject, dag, null);
return;
}
if (isMultiModuleProject(currentProject)) {
currentProject.getBuildFile().getModules().stream().map(moduleName -> {
Path modulePath = currentProject.getModulePath().resolve(moduleName).normalize();
MavenProject mavenProject = reactorProjects.stream()
.filter(p -> LinuxWindowsPathUnifier.pathEquals(p.getModulePath(), modulePath))
.findFirst()
.get();
return mavenProject;
}).forEach(childProject -> {
// add dependent project
logDependentProject(childProject, dag, currentProject);
buildDependencyGraph(childProject, reactorProjects, dag);
});
}
dag.computeIfAbsent(currentProject, __ -> new HashSet<>());
}
private static boolean isMultiModuleProject(MavenProject currentProject) {
return hasPomPackaging(currentProject);
}
private static boolean isSingleModuleProject(List<MavenProject> reactorProjects) {
return reactorProjects.size() == 1;
}
private static void logDependentProject(MavenProject dependingProject, Map<MavenProject, Set<MavenProject>> dag,
MavenProject dependantProject) {
Set<MavenProject> mavenProjects = dag.computeIfAbsent(dependingProject, __ -> new HashSet<>());
if (dependantProject != null) {
mavenProjects.add(dependantProject);
}
}
private MavenProject findRootProject(Path baseDir, List<MavenProject> reactorProjects) {
return reactorProjects.stream()
.filter(p -> LinuxWindowsPathUnifier.pathEquals(p.getBuildFile().getPath(), baseDir.resolve(POM_XML)))
.findFirst()
.get();
}
private static boolean hasPomPackaging(MavenProject curMavenProject) {
return "pom".equals(curMavenProject.getBuildFile().getPackaging());
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite.parsers.maven;
import org.springframework.rewrite.parsers.MavenProject;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Fabian Krüger
*/
public class MavenProjectSorter {
private static final String POM_XML = "pom.xml";
private final MavenProjectGraph mavenProjectGraph;
public MavenProjectSorter(MavenProjectGraph mavenProjectGraph) {
this.mavenProjectGraph = mavenProjectGraph;
}
/**
* Filter {@link MavenProject}s that belong to a reactor build. Create ADG of the
* relevant projects and their dependencies to other {@link MavenProject}s. Sort the
* ADG and return the ordered list of {@link MavenProject}s.
*/
public List<MavenProject> sort(Path baseDir, List<MavenProject> allPomFiles) {
Map<MavenProject, Set<MavenProject>> graph = mavenProjectGraph.from(baseDir, allPomFiles);
List<MavenProject> buildOrder = new ArrayList<>();
Map<MavenProject, Set<MavenProject>> dependingProjects = new HashMap<>();
graph.keySet().forEach(mavenProject -> {
graph.entrySet()
.stream()
.peek(e -> dependingProjects.computeIfAbsent(mavenProject, __ -> new HashSet<>()))
.filter(e -> e.getValue().contains(mavenProject))
.forEach(e -> {
dependingProjects.get(mavenProject).add(e.getKey());
});
});
Map<MavenProject, Integer> inDegree = dependingProjects.keySet()
.stream()
.collect(Collectors.toMap(k -> k, __ -> 0));
dependingProjects.entrySet()
.stream()
.flatMap(e -> e.getValue().stream())
.forEach(d -> inDegree.put(d, inDegree.get(d) + 1));
Queue<MavenProject> sources = new PriorityQueue<>(Comparator.comparing(p -> p.getBuildFile().getArtifactId()));
for (Map.Entry<MavenProject, Integer> entry : inDegree.entrySet()) {
if (entry.getValue() == 0) {
sources.add(entry.getKey());
}
}
while (!sources.isEmpty()) {
MavenProject project = sources.poll();
buildOrder.add(project);
List<MavenProject> mavenProjects1 = new ArrayList<>(dependingProjects.get(project));
for (MavenProject child : mavenProjects1) {
inDegree.put(child, inDegree.get(child) - 1);
if (inDegree.get(child) == 0) {
sources.add(child);
}
}
}
if (buildOrder.size() != inDegree.size()) {
throw new RuntimeException("Cycle detected Maven projects");
}
return buildOrder;
}
}

View File

@@ -1,120 +0,0 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.internal.JavaTypeCache;
import org.openrewrite.java.internal.TypesInUse;
import org.openrewrite.java.marker.JavaSourceSet;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Fabian Krüger
*/
public class JavaParserTest {
@Test
@DisplayName("shouldHaveTypeInUse")
@Disabled("Examination the JavaParser")
void shouldHaveTypeInUse() {
String localM2Repo = Path.of(System.getProperty("user.home")).resolve(".m2/repository").toString();
List<Path> classpath = List.of(
Path.of(localM2Repo
+ "/org/springframework/boot/spring-boot-starter/3.1.1/spring-boot-starter-3.1.1.jar"),
Path.of(localM2Repo + "/org/springframework/boot/spring-boot/3.1.1/spring-boot-3.1.1.jar"),
Path.of(localM2Repo + "/org/springframework/spring-context/6.0.10/spring-context-6.0.10.jar"),
Path.of(localM2Repo + "/org/springframework/spring-aop/6.0.10/spring-aop-6.0.10.jar"),
Path.of(localM2Repo + "/org/springframework/spring-beans/6.0.10/spring-beans-6.0.10.jar"),
Path.of(localM2Repo + "/org/springframework/spring-expression/6.0.10/spring-expression-6.0.10.jar"),
Path.of(localM2Repo
+ "/org/springframework/boot/spring-boot-autoconfigure/3.1.1/spring-boot-autoconfigure-3.1.1.jar"),
Path.of(localM2Repo
+ "/org/springframework/boot/spring-boot-starter-logging/3.1.1/spring-boot-starter-logging-3.1.1.jar"),
Path.of(localM2Repo + "/ch/qos/logback/logback-classic/1.4.8/logback-classic-1.4.8.jar"),
Path.of(localM2Repo + "/ch/qos/logback/logback-core/1.4.8/logback-core-1.4.8.jar"),
Path.of(localM2Repo + "/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar"),
Path.of(localM2Repo + "/org/apache/logging/log4j/log4j-to-slf4j/2.20.0/log4j-to-slf4j-2.20.0.jar"),
Path.of(localM2Repo + "/org/apache/logging/log4j/log4j-api/2.20.0/log4j-api-2.20.0.jar"),
Path.of(localM2Repo + "/org/slf4j/jul-to-slf4j/2.0.7/jul-to-slf4j-2.0.7.jar"),
Path.of(localM2Repo
+ "/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar"),
Path.of(localM2Repo + "/org/springframework/spring-core/6.0.10/spring-core-6.0.10.jar"),
Path.of(localM2Repo + "/org/springframework/spring-jcl/6.0.10/spring-jcl-6.0.10.jar"),
Path.of(localM2Repo + "/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar"));
JavaTypeCache javaTypeCache = new JavaTypeCache();
SourceFile sourceFile = JavaParser.fromJavaVersion()
.classpath(classpath)
.typeCache(javaTypeCache)
.build()
.parse("""
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyMain {
public static void main(String[] args){
SpringApplication.run(MyMain.class, args);
}
}
""")
.toList()
.get(0);
J.CompilationUnit compilationUnit = (J.CompilationUnit) sourceFile;
List<String> typesInUse = compilationUnit.getTypesInUse()
.getTypesInUse()
.stream()
.map(s -> s.toString())
.toList();
assertThat(typesInUse).contains("org.springframework.boot.SpringApplication",
"org.springframework.boot.SpringApplication", "com.example.MyMain");
JavaSourceSet main = JavaSourceSet.build("main", classpath, javaTypeCache, true);
List<String> typesOnClasspath = main.getClasspath()
.stream()
.map(JavaType.FullyQualified::getFullyQualifiedName)
.toList();
assertThat(typesOnClasspath).doesNotContain("com.example.MyMain"); // By design
javaTypeCache.put("com.example.MyMain", sourceFile);
main = JavaSourceSet.build("main", classpath, javaTypeCache, true);
List<JavaType.FullyQualified> mainCp = main.getClasspath();
TypesInUse typesInUseBefore = compilationUnit.getTypesInUse();
typesInUse = typesInUseBefore.getTypesInUse().stream().map(s -> s.toString()).toList();
assertThat(typesInUse).contains("org.springframework.boot.SpringApplication",
"org.springframework.boot.SpringApplication", "com.example.MyMain");
compilationUnit.getClasses().stream().map(c -> c.getType()).forEach(mainCp::add);
main = main.withClasspath(mainCp);
assertThat(main.getClasspath().stream().map(JavaType.FullyQualified::getFullyQualifiedName).toList())
.contains("org.springframework.boot.SpringApplication");
assertThat(main.getClasspath().stream().map(JavaType.FullyQualified::getFullyQualifiedName).toList())
.contains("com.example.MyMain");
}
}

View File

@@ -101,7 +101,7 @@ public class CalculateClasspathTest {
}
""";
Path baseDir = LinuxWindowsPathUnifier.unifiedPath(tmpDir.resolve("/example-1").toAbsolutePath().normalize());
Path baseDir = LinuxWindowsPathUnifier.unifiedPath(tmpDir.resolve("example-1").toAbsolutePath().normalize());
List<Resource> resources = List.of(new DummyResource(baseDir.resolve("pom.xml"), pom),
new DummyResource(baseDir.resolve("src/main/java/com/example/MainClass.java"), mainClass),
new DummyResource(baseDir.resolve("src/test/java/com/example/TestClass.java"), testClass));

View File

@@ -25,7 +25,6 @@ import org.openrewrite.Parser;
import org.openrewrite.SourceFile;
import org.openrewrite.tree.ParsingEventListener;
import org.openrewrite.tree.ParsingExecutionContextView;
import org.sonatype.plexus.components.cipher.PlexusCipherException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.DefaultResourceLoader;
@@ -88,7 +87,7 @@ class RewriteProjectParserTest {
@Test
@DisplayName("Parse simple Maven project")
void parseSimpleMavenProject(@TempDir Path tempDir) throws PlexusCipherException {
void parseSimpleMavenProject(@TempDir Path tempDir) {
Path basePath = tempDir;
SpringRewriteProperties springRewriteProperties = new SpringRewriteProperties();
ExecutionContext executionContext = new InMemoryExecutionContext(t -> {
@@ -98,13 +97,16 @@ class RewriteProjectParserTest {
ProjectMetadata projectMetadata = new ProjectMetadata();
MavenSettingsInitializer mavenSettingsInitializer = new MavenSettingsInitializer(executionContext,
projectMetadata);
RewriteMavenArtifactDownloader artifactDownloader = mock(RewriteMavenArtifactDownloader.class);
MavenProjectGraph projectCollector = new MavenProjectGraph();
MavenProjectFactory mavenProjectFactory = new MavenProjectFactory(artifactDownloader);
RewriteProjectParser projectParser = new RewriteProjectParser(
new ProvenanceMarkerFactory(new MavenProvenanceMarkerFactory()),
new BuildFileParser(mavenSettingsInitializer), new SourceFileParser(mavenModuleParser),
new StyleDetector(), springRewriteProperties, mock(ParsingEventListener.class),
mock(ApplicationEventPublisher.class), new ScanScope(), mock(ConfigurableListableBeanFactory.class),
new ProjectScanner(new DefaultResourceLoader(), springRewriteProperties), executionContext,
new MavenProjectAnalyzer(mock(RewriteMavenArtifactDownloader.class)));
new MavenProjectAnalyzer(new MavenProjectSorter(projectCollector), mavenProjectFactory));
List<String> parsedFiles = new ArrayList<>();
ParsingExecutionContextView.view(executionContext).setParsingListener(new ParsingEventListener() {

View File

@@ -0,0 +1,346 @@
/*
* Copyright 2021 - 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.rewrite.parsers.maven;
import com.google.common.base.Supplier;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.SoftAssertions;
import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.openrewrite.maven.cache.LocalMavenArtifactCache;
import org.openrewrite.maven.utilities.MavenArtifactDownloader;
import org.springframework.core.io.Resource;
import org.springframework.rewrite.parsers.MavenProject;
import org.springframework.rewrite.test.util.DummyResource;
import java.nio.file.Path;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.rewrite.parsers.maven.MavenProjectGraphTest.MavenProjectBuilder.none;
/**
* @author Fabian Krüger
*/
class MavenProjectGraphTest {
private MavenProjectGraph sut = new MavenProjectGraph();
private static MavenArtifactDownloader artifactDownloader = new RewriteMavenArtifactDownloader(
new LocalMavenArtifactCache(Path.of(System.getProperty("user.home")).resolve(".m2/repository")), null,
e -> {
throw new RuntimeException(e);
});
private static MavenProjectFactory projectFactory = new MavenProjectFactory(artifactDownloader);
@Test
@DisplayName("single module")
void singleModule() {
@Language("xml")
String pomCode = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme</groupId>
<version>0.1.0-SNAPSHOT</version>
<artifactId>example</artifactId>
</project>
""";
Path baseDir = Path.of("./target").toAbsolutePath().normalize();
Resource pomResource = new DummyResource(baseDir.resolve("pom.xml"), pomCode);
MavenProject mavenProject = projectFactory.create(baseDir, pomResource, List.of(pomResource));
List<MavenProject> allMavenProjects = List.of(mavenProject);
Map<MavenProject, Set<MavenProject>> mavenProjectSetMap = sut.from(baseDir, allMavenProjects);
assertThat(mavenProjectSetMap).hasSize(1);
}
@Test
@DisplayName("Dangling pom should will be collected")
void danglingPomShouldBeIgnored() {
@Language("xml")
String parentPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>example</module>
</modules>
</project>
""";
@Language("xml")
String modulePom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.acme</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>example</artifactId>
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>dangling</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
""";
@Language("xml")
String danglingPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme</groupId>
<artifactId>dangling</artifactId>
<version>0.1.0-SNAPSHOT</version>
</project>
""";
Path baseDir = Path.of(".").toAbsolutePath();
MavenProjectBuilder.buildMavenProject(baseDir)
.withResource("pom.xml", parentPom)
.withResource("dangling/pom.xml", danglingPom)
.withResource("example/pom.xml", modulePom)
.afterSort()
.assertDependencies(parentPom, none())
.assertDependencies(modulePom, parentPom)
.assertRemoved(danglingPom)
.verify();
}
@Test
@DisplayName("Multi module with dependant projects")
void multiModuleWithDependantProjects() {
// Modules declared in order a,b,c
@Language("xml")
String parentPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>module-a</module>
<module>module-b</module>
<module>module-c</module>
</modules>
</project>
""";
// Module A depends on C, so C must be built first effectively changing the order
// in <modules>
@Language("xml")
String moduleAPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.acme</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>module-a</artifactId>
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>module-c</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
""";
@Language("xml")
String moduleBPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.acme</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>module-b</artifactId>
</project>
""";
// C depends on B
@Language("xml")
String moduleCPom = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.acme</groupId>
<artifactId>parent</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>module-c</artifactId>
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>module-b</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
""";
Path baseDir = Path.of(".").toAbsolutePath();
MavenProjectBuilder.buildMavenProject(baseDir)
.withResource("pom.xml", parentPom)
.withResource("module-a/pom.xml", moduleAPom)
.withResource("module-b/pom.xml", moduleBPom)
.withResource("module-c/pom.xml", moduleCPom)
.afterSort()
.assertDependencies(parentPom, none())
.assertDependencies(moduleAPom, parentPom, moduleCPom)
.assertDependencies(moduleBPom, parentPom)
.assertDependencies(moduleCPom, parentPom, moduleBPom)
.verify();
}
static class MavenProjectBuilder {
private final MavenProjectGraph sut = new MavenProjectGraph();
private static MavenArtifactDownloader artifactDownloader = new RewriteMavenArtifactDownloader(
new LocalMavenArtifactCache(Path.of(System.getProperty("user.home")).resolve(".m2/repository")), null,
e -> {
throw new RuntimeException(e);
});
;
private static MavenProjectFactory projectFactory = new MavenProjectFactory(artifactDownloader);
;
private final Path baseDir;
private List<Resource> resources = new ArrayList<>();
private List<MavenProject> mavenProjects = new ArrayList<>();
private List<AbstractAssert> assertions = new ArrayList<>();
private Map<MavenProject, Set<MavenProject>> dependencyGraph;
private List<Supplier<Executable>> listAssertSupplier;
private SoftAssertions softAssertions = new SoftAssertions();
private Set<MavenProject> assertedProjects = new HashSet<>();
private Set<MavenProject> removedProjects = new HashSet<>();
private Set<MavenProject> selectedProjects = new HashSet<>();
public MavenProjectBuilder(Path baseDir) {
this.baseDir = baseDir;
}
public static MavenProjectBuilder buildMavenProject(Path baseDir) {
return new MavenProjectBuilder(baseDir);
}
public static List<String> none() {
return List.of();
}
public MavenProjectBuilder withResource(String path, String content) {
Resource r = new DummyResource(baseDir.resolve(path), content);
this.resources.add(r);
return this;
}
public MavenProjectBuilder afterSort() {
mavenProjects = MavenProjectGraphTest.projectFactory.create(baseDir, resources);
dependencyGraph = sut.from(baseDir, mavenProjects);
return this;
}
public MavenProjectBuilder assertDependencies(String pomContent, String... dependantPomContents) {
MavenProject moduleProject = findMavenProject(pomContent);
this.assertedProjects.add(moduleProject);
List<MavenProject> dependantProjects = getDependantProjects(dependantPomContents);
// softAssertions.
assertThat(dependencyGraph).containsKey(moduleProject);
MavenProject[] dependantProjectsArray = dependantProjects.toArray(MavenProject[]::new);
Set<MavenProject> actualDependantProjects = dependencyGraph.get(moduleProject);
// softAssertions.
assertThat(actualDependantProjects).containsExactlyInAnyOrder(dependantProjectsArray);
return this;
}
private List<MavenProject> getDependantProjects(String... dependantPomContents) {
List<String> contents = Arrays.asList(dependantPomContents);
return mavenProjects.stream().filter(p -> contents.contains(p.getBuildFile().getContent())).toList();
}
private MavenProject findMavenProject(String pomContent) {
return mavenProjects.stream()
.filter(p -> pomContent.equals(p.getBuildFile().getContent()))
.findFirst()
.get();
}
public MavenProjectBuilder assertDependencies(String parentPom, List<String> empty) {
assertDependencies(parentPom);
return this;
}
public void verify() {
softAssertions.assertThat(assertedProjects)
.containsExactlyInAnyOrder(dependencyGraph.keySet().toArray(MavenProject[]::new));
softAssertions.assertAll();
}
public MavenProjectBuilder assertRemoved(String danglingPom) {
MavenProject mavenProject = findMavenProject(danglingPom);
this.removedProjects.add(mavenProject);
this.selectedProjects.remove(mavenProject);
softAssertions.assertThat(selectedProjects).doesNotContain(mavenProject);
return this;
}
}
}

View File

@@ -33,6 +33,7 @@ import org.openrewrite.maven.tree.Scope;
import org.openrewrite.style.Style;
import org.springframework.rewrite.parsers.SpringRewriteProperties;
import org.springframework.rewrite.parsers.RewriteProjectParsingResult;
import org.springframework.rewrite.parsers.maven.ClasspathDependencies;
import java.io.File;
import java.net.URI;
@@ -236,7 +237,9 @@ public class ParserParityTestHelper {
static void verifyEqualSourceFileMarkers(SourceFile curExpectedSourceFile, SourceFile curGivenSourceFile) {
Markers expectedMarkers = curExpectedSourceFile.getMarkers();
List<Marker> expectedMarkersList = expectedMarkers.getMarkers();
Markers givenMarkers = curGivenSourceFile.getMarkers();
// Remove custom marker that only exists here
Markers givenMarkers = curGivenSourceFile.getMarkers().removeByType(ClasspathDependencies.class);
List<Marker> actualMarkersList = givenMarkers.getMarkers();
assertThat(actualMarkersList.stream().map(m -> m.getClass().getSimpleName()).toList())