Analyze cloned repos from source without running Maven or Gradle on the target project.

Parse build files statically for module wiring, use the exporter's bundled Spring classpath for type bindings, optionally include pre-built generated main sources, and exclude test output and test-scoped dependencies from analysis.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 10:35:52 +02:00
parent c447641800
commit 0628710106
18 changed files with 1669 additions and 467 deletions

View File

@@ -161,7 +161,7 @@ public class RegressionTest {
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
if (tempDir == null) System.out.println("tempDir is NULL");
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
// Find the generated directory (it might be named with FQN)
List<Path> generatedDirs;

View File

@@ -0,0 +1,173 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class GeneratedSourceDiscoveryTest {
private final GeneratedSourceDiscovery discovery = new GeneratedSourceDiscovery();
@Nested
class MavenTarget {
@Test
void shouldDiscoverMapStructLikeSourcesUnderTarget(@TempDir Path tempDir) throws IOException {
Path module = tempDir.resolve("service");
Path generated = module.resolve("target/generated-sources/annotations/com/acme/mapper");
Files.createDirectories(generated);
Files.writeString(generated.resolve("OrderMapperImpl.java"), """
package com.acme.mapper;
public class OrderMapperImpl implements OrderMapper {}
""");
assertThat(discovery.discoverSourceRoots(module))
.containsExactly(module.resolve("target").normalize());
}
@Test
void shouldIgnoreCompiledClassesUnderTarget(@TempDir Path tempDir) throws IOException {
Path module = tempDir.resolve("service");
Path classes = module.resolve("target/classes/com/acme");
Files.createDirectories(classes);
Files.writeString(classes.resolve("Accidental.java"), "class Accidental {}");
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
assertThat(GeneratedSourceDiscovery.isExcludedBuildPath(module, classes.resolve("Accidental.java")))
.isTrue();
}
}
@Nested
class GradleBuild {
@Test
void shouldDiscoverAnnotationProcessorOutput(@TempDir Path tempDir) throws IOException {
Path module = tempDir.resolve("app");
Path generated = module.resolve(
"build/generated/sources/annotationProcessor/java/main/com/acme/config");
Files.createDirectories(generated);
Files.writeString(generated.resolve("ApplicationConfigImpl.java"), """
package com.acme.config;
public class ApplicationConfigImpl {}
""");
assertThat(discovery.discoverSourceRoots(module))
.containsExactly(module.resolve("build").normalize());
}
@Test
void shouldIgnoreGradleBuildClasses(@TempDir Path tempDir) throws IOException {
Path module = tempDir.resolve("app");
Path classes = module.resolve("build/classes/java/main/com/acme");
Files.createDirectories(classes);
Files.writeString(classes.resolve("ShouldSkip.java"), "class ShouldSkip {}");
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
}
@Test
void shouldIgnoreGradleTestAnnotationProcessorOutput(@TempDir Path tempDir) throws IOException {
Path module = tempDir.resolve("app");
Path generated = module.resolve(
"build/generated/sources/annotationProcessor/java/test/com/acme/testsupport");
Files.createDirectories(generated);
Files.writeString(generated.resolve("TestSupportImpl.java"), "class TestSupportImpl {}");
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
}
}
@Nested
class TestOutputExclusion {
@Test
void shouldIgnoreMavenGeneratedTestSources(@TempDir Path tempDir) throws IOException {
Path module = tempDir.resolve("service");
Path generated = module.resolve("target/generated-test-sources/test-annotations/com/acme");
Files.createDirectories(generated);
Files.writeString(generated.resolve("TestMapperImpl.java"), "class TestMapperImpl {}");
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
}
@Test
void shouldIgnoreTestClassesUnderTarget(@TempDir Path tempDir) throws IOException {
Path module = tempDir.resolve("service");
Path testClasses = module.resolve("target/test-classes/com/acme");
Files.createDirectories(testClasses);
Files.writeString(testClasses.resolve("OrderServiceTest.java"), "class OrderServiceTest {}");
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
}
}
@Nested
class Integration {
@Test
void shouldAddGeneratedRootsToProjectScanPaths(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Files.createDirectories(root.resolve(".git"));
Files.writeString(root.resolve("settings.gradle"), "include 'api'");
Path api = root.resolve("api");
Files.createDirectories(api);
Files.writeString(api.resolve("build.gradle"), "");
Path generated = api.resolve("target/generated-sources/annotations/com/example");
Files.createDirectories(generated);
Files.writeString(generated.resolve("GeneratedBean.java"), """
package com.example;
public class GeneratedBean {}
""");
StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
ProjectModuleGraph graph = analyzer.analyze(root);
assertThat(graph.resolveScanPaths(root)).contains(api.resolve("target").normalize());
}
@Test
void shouldSkipGeneratedRootsWhenDisabled(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Files.createDirectories(root.resolve(".git"));
Files.writeString(root.resolve("settings.gradle"), "include 'api'");
Path api = root.resolve("api");
Files.createDirectories(api);
Files.writeString(api.resolve("build.gradle"), "");
Path generated = api.resolve("target/generated-sources/annotations/com/example");
Files.createDirectories(generated);
Files.writeString(generated.resolve("GeneratedBean.java"), "package com.example; class GeneratedBean {}");
ProjectModuleGraph graph = new StaticBuildFileAnalyzer().analyze(root);
assertThat(graph.resolveScanPaths(root, false)).doesNotContain(api.resolve("target").normalize());
assertThat(graph.resolveScanPaths(root, true)).contains(api.resolve("target").normalize());
}
@Test
void shouldIndexGeneratedTypesWhenScanningModule() throws IOException {
Path module = Files.createTempDirectory("sme-gen-src").resolve("service");
Path generated = module.resolve("target/generated-sources/annotations/com/acme");
Files.createDirectories(generated);
Files.writeString(generated.resolve("GeneratedEvent.java"), """
package com.acme;
public enum GeneratedEvent { PAY, SHIP }
""");
CodebaseContext context = new CodebaseContext();
context.scan(Set.of(module), Set.of());
assertThat(context.getCompilationUnits()).hasSize(1);
assertThat(context.getEnumValues("GeneratedEvent"))
.contains("com.acme.GeneratedEvent.PAY", "com.acme.GeneratedEvent.SHIP");
}
}
}

View File

@@ -0,0 +1,510 @@
package click.kamil.springstatemachineexporter.analysis.resolver;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
class StaticBuildFileAnalyzerTest {
private final StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
@Nested
class GradleIncludes {
@Test
void shouldParseSettingsGradleIncludeList(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Files.createDirectories(root);
Files.writeString(root.resolve("settings.gradle"), """
rootProject.name = 'monorepo'
include 'module-a', 'module-b', 'module-c'
""");
createGradleModule(root, "module-a");
createGradleModule(root, "module-b");
createGradleModule(root, "module-c");
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
assertThat(includes).hasSize(3);
assertThat(includes.get("module-a")).isEqualTo(root.resolve("module-a").normalize());
assertThat(includes.get("module-b")).isEqualTo(root.resolve("module-b").normalize());
assertThat(includes.get("module-c")).isEqualTo(root.resolve("module-c").normalize());
}
@Test
void shouldParseSettingsGradleKtsIncludeCall(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Files.createDirectories(root);
Files.writeString(root.resolve("settings.gradle.kts"), """
include(":api", ":core")
""");
createGradleModule(root, "api");
createGradleModule(root, "core");
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
assertThat(includes).containsKeys("api", "core");
assertThat(includes.get("api")).isEqualTo(root.resolve("api").normalize());
}
@Test
void shouldResolveNestedIncludePaths(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Path nested = root.resolve("state_machines/multi/api");
Files.createDirectories(nested);
Files.writeString(root.resolve("settings.gradle"), """
include ':state_machines:multi:api', ':state_machines:multi:core'
""");
createGradleModule(root, "state_machines/multi/api");
createGradleModule(root, "state_machines/multi/core");
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
assertThat(includes.get("state_machines:multi:api"))
.isEqualTo(root.resolve("state_machines/multi/api").normalize());
assertThat(includes.get("state_machines:multi:core"))
.isEqualTo(root.resolve("state_machines/multi/core").normalize());
}
@Test
void shouldHonorCustomProjectDirMapping(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Path custom = root.resolve("custom-location/service");
Files.createDirectories(custom);
Files.writeString(root.resolve("settings.gradle"), """
include ':service'
project(':service').projectDir = file('custom-location/service')
""");
createGradleModule(custom, ".");
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
assertThat(includes.get("service")).isEqualTo(custom.normalize());
}
@Test
void shouldDiscoverAllIncludedModulesWhenScanningFromRoot(@TempDir Path tempDir) throws IOException {
Path root = writeGradleMonorepo(tempDir);
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
Set<Path> scanPaths = graph.resolveScanPaths(root);
assertThat(scanPaths).containsExactlyInAnyOrder(
root.normalize(),
root.resolve("module-a").normalize(),
root.resolve("module-b").normalize(),
root.resolve("module-c").normalize());
}
}
@Nested
class GradleDependencyGraph {
@Test
void shouldBuildTransitiveProjectDependencyGraph(@TempDir Path tempDir) throws IOException {
Path root = writeGradleMonorepo(tempDir);
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
.filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
.toList();
assertThat(localDeps).anyMatch(dep ->
dep.fromModuleId().contains("module-a") && dep.toModuleId().contains("module-b"));
assertThat(localDeps).anyMatch(dep ->
dep.fromModuleId().contains("module-b") && dep.toModuleId().contains("module-c"));
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root.resolve("module-a"));
assertThat(siblings).containsExactlyInAnyOrder(
root.resolve("module-b").normalize(),
root.resolve("module-c").normalize());
}
@Test
void shouldParseBuildGradleKtsProjectDependencies(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Files.createDirectories(root);
Files.createDirectories(root.resolve(".git"));
Files.writeString(root.resolve("settings.gradle.kts"), "include(\":app\", \":lib\")");
Files.createDirectories(root.resolve("app"));
Files.writeString(root.resolve("app/build.gradle.kts"), """
dependencies {
implementation(project(":lib"))
}
""");
Files.createDirectories(root.resolve("lib"));
Files.writeString(root.resolve("lib/build.gradle.kts"), "");
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
boolean hasLibDep = graph.dependencies().stream()
.anyMatch(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT
&& dep.fromModuleId().contains("app")
&& dep.toModuleId().contains("lib"));
assertThat(hasLibDep).isTrue();
}
@Test
void shouldIgnoreTestImplementationProjectDependencies(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("monorepo");
Files.createDirectories(root.resolve(".git"));
Files.writeString(root.resolve("settings.gradle"), "include 'app', 'lib', 'test-lib'");
createGradleModule(root, "app");
createGradleModule(root, "lib");
createGradleModule(root, "test-lib");
Files.writeString(root.resolve("app/build.gradle"), """
dependencies {
implementation project(':lib')
testImplementation project(':test-lib')
}
""");
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
.filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
.toList();
assertThat(localDeps).anyMatch(dep -> dep.toModuleId().contains("lib"));
assertThat(localDeps).noneMatch(dep -> dep.toModuleId().contains("test-lib"));
}
}
@Nested
class MavenCoordinates {
@Test
void shouldIndexModulesByGroupIdAndArtifactId(@TempDir Path tempDir) throws IOException {
Path root = writeMavenMultiModule(tempDir);
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
assertThat(graph.modulesByCoordinates()).containsKeys(
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-a"),
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-b"),
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-c"));
}
@Test
void shouldDistinguishSameArtifactIdUnderDifferentGroups(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("repo");
Files.createDirectories(root.resolve(".git"));
writePom(root.resolve("pom.xml"), """
<project>
<groupId>com.repo</groupId>
<artifactId>parent</artifactId>
<packaging>pom</packaging>
<modules>
<module>team-a/api</module>
<module>team-b/api</module>
<module>consumer</module>
</modules>
</project>
""");
writePom(root.resolve("team-a/api/pom.xml"), """
<project>
<groupId>com.team.a</groupId>
<artifactId>api</artifactId>
</project>
""");
writePom(root.resolve("team-b/api/pom.xml"), """
<project>
<groupId>com.team.b</groupId>
<artifactId>api</artifactId>
</project>
""");
writePom(root.resolve("consumer/pom.xml"), """
<project>
<groupId>com.consumer</groupId>
<artifactId>consumer</artifactId>
<dependencies>
<dependency>
<groupId>com.team.a</groupId>
<artifactId>api</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
""");
ProjectModuleGraph graph = analyzer.analyze(root.resolve("consumer"));
List<ProjectModuleGraph.ModuleDependency> deps = graph.localDependenciesOf(
graph.modulesByCoordinates()
.get(new ProjectModuleGraph.MavenCoordinates("com.consumer", "consumer"))
.id());
assertThat(deps).hasSize(1);
assertThat(deps.get(0).toModuleId()).isEqualTo(
graph.modulesByCoordinates().get(new ProjectModuleGraph.MavenCoordinates("com.team.a", "api")).id());
}
@Test
void shouldMarkExternalLibraryWhenCoordinatesAreNotInRepo(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("repo");
Files.createDirectories(root.resolve(".git"));
writePom(root.resolve("service/pom.xml"), """
<project>
<groupId>com.acme</groupId>
<artifactId>service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
</project>
""");
ProjectModuleGraph graph = analyzer.analyze(root.resolve("service"));
String serviceId = graph.modulesByCoordinates()
.get(new ProjectModuleGraph.MavenCoordinates("com.acme", "service")).id();
assertThat(graph.dependencies()).anyMatch(dep ->
dep.fromModuleId().equals(serviceId)
&& dep.kind() == ProjectModuleGraph.DependencyKind.EXTERNAL_LIBRARY
&& dep.externalCoordinates().key().equals("org.springframework.boot:spring-boot-starter"));
}
@Test
void shouldIgnoreTestScopedMavenDependencies(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("repo");
Files.createDirectories(root.resolve(".git"));
writePom(root.resolve("pom.xml"), """
<project>
<groupId>com.repo</groupId>
<artifactId>parent</artifactId>
<packaging>pom</packaging>
<modules><module>app</module><module>test-support</module></modules>
</project>
""");
writePom(root.resolve("app/pom.xml"), """
<project>
<groupId>com.repo</groupId>
<artifactId>app</artifactId>
<dependencies>
<dependency>
<groupId>com.repo</groupId>
<artifactId>test-support</artifactId>
<version>1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
""");
writePom(root.resolve("test-support/pom.xml"), """
<project><groupId>com.repo</groupId><artifactId>test-support</artifactId></project>
""");
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
String appId = graph.modulesByCoordinates()
.get(new ProjectModuleGraph.MavenCoordinates("com.repo", "app")).id();
assertThat(graph.localDependenciesOf(appId)).isEmpty();
}
}
@Nested
class MavenModules {
@Test
void shouldDiscoverAggregatorModules(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("aggregator");
Files.createDirectories(root.resolve(".git"));
writePom(root.resolve("pom.xml"), """
<project>
<groupId>com.acme</groupId>
<artifactId>root</artifactId>
<packaging>pom</packaging>
<modules>
<module>service-x</module>
<module>service-y</module>
</modules>
</project>
""");
writePom(root.resolve("service-x/pom.xml"), """
<project><groupId>com.acme</groupId><artifactId>service-x</artifactId></project>
""");
writePom(root.resolve("service-y/pom.xml"), """
<project><groupId>com.acme</groupId><artifactId>service-y</artifactId></project>
""");
ProjectModuleGraph graph = analyzer.analyze(root);
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root);
assertThat(siblings).containsExactlyInAnyOrder(
root.resolve("service-x").normalize(),
root.resolve("service-y").normalize());
}
@Test
void shouldResolveTransitiveMavenModuleDependencies(@TempDir Path tempDir) throws IOException {
Path root = writeMavenMultiModule(tempDir);
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(root.resolve("module-a"));
assertThat(siblings).containsExactlyInAnyOrder(
root.resolve("module-b").normalize(),
root.resolve("module-c").normalize(),
root.normalize());
}
@Test
void shouldResolveParentViaRelativePath(@TempDir Path tempDir) throws IOException {
Path root = tempDir.resolve("root");
Path child = tempDir.resolve("child");
Files.createDirectories(root);
Files.createDirectories(child);
writePom(root.resolve("pom.xml"), """
<project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
""");
writePom(child.resolve("pom.xml"), """
<project>
<parent>
<relativePath>../root/pom.xml</relativePath>
</parent>
<artifactId>child</artifactId>
</project>
""");
Set<Path> scanPaths = analyzer.analyze(child).resolveScanPaths(child);
assertThat(scanPaths).contains(root.toAbsolutePath().normalize());
}
@Test
void shouldPreferNestedMavenAggregatorOverOuterGradleRoot(@TempDir Path tempDir) throws IOException {
Path outerGradle = tempDir.resolve("outer");
Path nestedMaven = outerGradle.resolve("nested-maven");
Files.createDirectories(outerGradle.resolve(".git"));
Files.writeString(outerGradle.resolve("settings.gradle"), "rootProject.name = 'outer'");
writePom(nestedMaven.resolve("pom.xml"), """
<project>
<groupId>com.nested</groupId>
<artifactId>parent</artifactId>
<packaging>pom</packaging>
<modules><module>api</module></modules>
</project>
""");
writePom(nestedMaven.resolve("api/pom.xml"), """
<project><groupId>com.nested</groupId><artifactId>api</artifactId></project>
""");
ProjectModuleGraph graph = analyzer.analyze(nestedMaven.resolve("api"));
assertThat(graph.workspaceRoot()).isEqualTo(nestedMaven.normalize());
assertThat(graph.allModuleDirectories()).containsExactlyInAnyOrder(
nestedMaven.resolve("api").normalize(),
nestedMaven.normalize());
}
}
@Nested
class ExtractHelpers {
@Test
void shouldExtractGradleIncludeTokensFromMixedSyntax() {
String settings = """
pluginManagement {}
include ':api'
include(":core", ":infra")
include 'web', 'worker'
""";
assertThat(StaticBuildFileAnalyzer.extractGradleIncludePaths(settings))
.containsExactlyInAnyOrder("api", "core", "infra", "web", "worker");
}
@Test
void shouldExtractOwnMavenCoordinatesSkippingParentSection() {
String pom = """
<project>
<parent>
<groupId>com.parent</groupId>
<artifactId>parent</artifactId>
</parent>
<artifactId>child</artifactId>
<groupId>com.child</groupId>
</project>
""";
assertThat(StaticBuildFileAnalyzer.extractOwnMavenCoordinates(pom))
.contains(new ProjectModuleGraph.MavenCoordinates("com.child", "child"));
}
}
private static Path writeGradleMonorepo(Path tempDir) throws IOException {
Path root = tempDir.resolve("my-root-project");
Files.createDirectories(root);
Files.createDirectories(root.resolve(".git"));
Files.writeString(root.resolve("settings.gradle"), "include 'module-a', 'module-b', 'module-c'");
createGradleModule(root, "module-a");
createGradleModule(root, "module-b");
createGradleModule(root, "module-c");
Files.writeString(root.resolve("module-a/build.gradle"), "dependencies { implementation project(':module-b') }");
Files.writeString(root.resolve("module-b/build.gradle"), "dependencies { implementation project(':module-c') }");
return root;
}
private static Path writeMavenMultiModule(Path tempDir) throws IOException {
Path root = tempDir.resolve("my-maven-root");
Files.createDirectories(root.resolve(".git"));
writePom(root.resolve("pom.xml"), """
<project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
""");
writePom(root.resolve("module-a/pom.xml"), """
<project>
<groupId>com.acme</groupId>
<artifactId>module-a</artifactId>
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>module-b</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
""");
writePom(root.resolve("module-b/pom.xml"), """
<project>
<groupId>com.acme</groupId>
<artifactId>module-b</artifactId>
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>module-c</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
""");
writePom(root.resolve("module-c/pom.xml"), """
<project><groupId>com.acme</groupId><artifactId>module-c</artifactId></project>
""");
return root;
}
private static void createGradleModule(Path root, String modulePath) throws IOException {
Path moduleDir = ".".equals(modulePath) ? root : root.resolve(modulePath);
Files.createDirectories(moduleDir);
Path buildFile = moduleDir.resolve("build.gradle");
if (!Files.exists(buildFile)) {
Files.writeString(buildFile, "");
}
}
private static void writePom(Path pomFile, String content) throws IOException {
Files.createDirectories(pomFile.getParent());
Files.writeString(pomFile, content);
}
}

View File

@@ -1,78 +1,31 @@
package click.kamil.springstatemachineexporter.analysis.service;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("dynamic_classpath_resolver")
class DynamicClasspathResolverTest {
@Test
void shouldResolveMavenClasspathDynamically(@TempDir Path tempDir) throws IOException {
// Create dummy pom.xml
void shouldNeverResolveExternalJarsEvenWhenMavenProject(@TempDir Path tempDir) throws Exception {
Files.createFile(tempDir.resolve("pom.xml"));
// Mock the resolver so it doesn't actually run maven, but writes the expected output file
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
@Override
protected void runProcess(ProcessBuilder pb) throws IOException {
// Pretend maven executed and generated the cp.txt
String cp = "/fake/m2/repo/spring-core.jar" + File.pathSeparator + "/fake/m2/repo/spring-context.jar";
String outputFileArg = pb.command().stream()
.filter(arg -> arg.startsWith("-Dmdep.outputFile="))
.findFirst()
.orElseThrow();
Path outputFile = pb.directory().toPath().resolve(outputFileArg.substring("-Dmdep.outputFile=".length()));
Files.writeString(outputFile, cp);
}
};
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).containsExactly(
"/fake/m2/repo/spring-core.jar",
"/fake/m2/repo/spring-context.jar"
);
assertThat(new DynamicClasspathResolver().resolveClasspath(tempDir)).isEmpty();
}
@Test
void shouldResolveGradleClasspathDynamically(@TempDir Path tempDir) throws IOException {
// Create dummy build.gradle
void shouldNeverResolveExternalJarsEvenWhenGradleProject(@TempDir Path tempDir) throws Exception {
Files.createFile(tempDir.resolve("build.gradle"));
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
@Override
protected String runProcessAndGetOutput(ProcessBuilder pb) {
// Pretend gradle executed and printed the marker
String cp = "/fake/gradle/caches/spring-core.jar" + File.pathSeparator + "/fake/gradle/caches/spring-context.jar";
return "Some noisy gradle output\n" +
"SME_CLASSPATH_MARKER:" + cp + "\n" +
"BUILD SUCCESSFUL\n";
}
};
List<String> classpath = resolver.resolveClasspath(tempDir);
assertThat(classpath).containsExactly(
"/fake/gradle/caches/spring-core.jar",
"/fake/gradle/caches/spring-context.jar"
);
assertThat(new DynamicClasspathResolver().resolveClasspath(tempDir)).isEmpty();
}
@Test
void shouldReturnEmptyListIfNoBuildFileFound(@TempDir Path tempDir) {
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> classpath = resolver.resolveClasspath(tempDir);
List<String> classpath = new DynamicClasspathResolver().resolveClasspath(tempDir);
assertThat(classpath).isEmpty();
}
}

View File

@@ -7,8 +7,8 @@ import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnal
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
import click.kamil.springstatemachineexporter.analysis.service.ToolingClasspath;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -33,12 +33,7 @@ class JdtCallGraphEngineIntegrationTest {
context = new CodebaseContext();
context.setProjectRoot(projectRoot);
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> cp = resolver.resolveClasspath(projectRoot);
if (!cp.isEmpty()) {
context.setClasspath(cp);
}
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
context.setResolveBindings(true);
context.scan(Set.of(projectRoot), Collections.emptySet());
@@ -290,12 +285,7 @@ class JdtCallGraphEngineIntegrationTest {
CodebaseContext ctx = new CodebaseContext();
ctx.setProjectRoot(projectRoot);
ctx.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> cp = resolver.resolveClasspath(projectRoot);
if (!cp.isEmpty()) {
ctx.setClasspath(cp);
}
ctx.setClasspath(ToolingClasspath.currentJvmJarEntries());
ctx.setResolveBindings(true);
ctx.scan(Set.of(projectRoot), Collections.emptySet());

View File

@@ -34,7 +34,7 @@ class LayeredDispatcherSampleTest {
ExportService exportService = new ExportService(List.of(new JsonExporter()));
exportService.runExporter(sampleRoot, tempDir, List.of("json"), true, List.of(), null, null,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
ObjectMapper mapper = new ObjectMapper();
JsonNode orderMachine = readMachineJson(tempDir, "StandardOrderStateMachineConfiguration", mapper);

View File

@@ -1,7 +1,7 @@
package click.kamil.springstatemachineexporter.analysis.spring;
import click.kamil.springstatemachineexporter.analysis.service.ToolingClasspath;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -25,12 +25,7 @@ class SpringContextScannerIntegrationTest {
context = new CodebaseContext();
context.setProjectRoot(projectRoot);
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
List<String> cp = resolver.resolveClasspath(projectRoot);
if (!cp.isEmpty()) {
context.setClasspath(cp);
}
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
context.setResolveBindings(true);
context.scan(Set.of(projectRoot), Collections.emptySet());