heuristics classpath
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class DynamicClasspathResolver {
|
||||
|
||||
public List<String> resolveClasspath(Path projectRoot) {
|
||||
log.info("Attempting dynamic classpath resolution for project at {}", projectRoot);
|
||||
if (projectRoot == null || !Files.exists(projectRoot)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (Files.exists(projectRoot.resolve("pom.xml"))) {
|
||||
return resolveMaven(projectRoot);
|
||||
} else if (Files.exists(projectRoot.resolve("build.gradle")) || Files.exists(projectRoot.resolve("build.gradle.kts"))) {
|
||||
return resolveGradle(projectRoot);
|
||||
}
|
||||
|
||||
log.info("No Maven or Gradle configuration found at {}. Proceeding without dynamic classpath.", projectRoot);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected List<String> resolveMaven(Path projectRoot) {
|
||||
log.info("Maven project detected. Resolving dependencies...");
|
||||
Path cpFile = null;
|
||||
try {
|
||||
cpFile = Files.createTempFile("state_machine_exporter_cp", ".txt");
|
||||
String mvnCmd = getCommand(projectRoot, "mvnw", "mvn");
|
||||
if (mvnCmd == null) {
|
||||
log.warn("Maven executable not found.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
mvnCmd,
|
||||
"-q",
|
||||
"dependency:build-classpath",
|
||||
"-DincludeScope=compile",
|
||||
"-Dmdep.outputFile=" + cpFile.toAbsolutePath().toString()
|
||||
);
|
||||
pb.directory(projectRoot.toFile());
|
||||
|
||||
runProcess(pb);
|
||||
|
||||
if (Files.exists(cpFile)) {
|
||||
String cpString = Files.readString(cpFile).trim();
|
||||
if (!cpString.isEmpty()) {
|
||||
return Arrays.asList(cpString.split(File.pathSeparator));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to dynamically resolve Maven classpath", e);
|
||||
} finally {
|
||||
if (cpFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(cpFile);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected List<String> resolveGradle(Path projectRoot) {
|
||||
log.info("Gradle project detected. Resolving dependencies...");
|
||||
Path initScript = null;
|
||||
try {
|
||||
initScript = Files.createTempFile("state_machine_exporter_init", ".gradle");
|
||||
String gradleCmd = getCommand(projectRoot, "gradlew", "gradle");
|
||||
if (gradleCmd == null) {
|
||||
log.warn("Gradle executable not found.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String scriptContent = """
|
||||
allprojects {
|
||||
task smePrintClasspath {
|
||||
doLast {
|
||||
def cp = []
|
||||
if (configurations.findByName("compileClasspath")) {
|
||||
cp = configurations.compileClasspath.files
|
||||
}
|
||||
println "SME_CLASSPATH_MARKER:" + cp.join(File.pathSeparator)
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
Files.writeString(initScript, scriptContent);
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
gradleCmd,
|
||||
"-q",
|
||||
"-I", initScript.toAbsolutePath().toString(),
|
||||
"smePrintClasspath"
|
||||
);
|
||||
pb.directory(projectRoot.toFile());
|
||||
|
||||
String output = runProcessAndGetOutput(pb);
|
||||
for (String line : output.split("\\r?\\n")) {
|
||||
if (line.startsWith("SME_CLASSPATH_MARKER:")) {
|
||||
String cpString = line.substring("SME_CLASSPATH_MARKER:".length()).trim();
|
||||
if (!cpString.isEmpty()) {
|
||||
return Arrays.asList(cpString.split(File.pathSeparator));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to dynamically resolve Gradle classpath", e);
|
||||
} finally {
|
||||
if (initScript != null) {
|
||||
try {
|
||||
Files.deleteIfExists(initScript);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private String getCommand(Path projectRoot, String wrapper, String systemBinary) {
|
||||
Path wrapperUnix = projectRoot.resolve(wrapper);
|
||||
Path wrapperWin = projectRoot.resolve(wrapper + ".bat");
|
||||
|
||||
boolean isWin = System.getProperty("os.name").toLowerCase().contains("win");
|
||||
|
||||
if (isWin && Files.exists(wrapperWin)) return wrapperWin.toAbsolutePath().toString();
|
||||
if (!isWin && Files.exists(wrapperUnix)) {
|
||||
// Need absolute path like ./gradlew
|
||||
return wrapperUnix.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
return systemBinary;
|
||||
}
|
||||
|
||||
protected void runProcess(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||
Process p = pb.start();
|
||||
p.waitFor();
|
||||
}
|
||||
|
||||
protected String runProcessAndGetOutput(ProcessBuilder pb) throws IOException, InterruptedException {
|
||||
Process p = pb.start();
|
||||
StringBuilder out = new StringBuilder();
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
out.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
p.waitFor();
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
@@ -172,40 +172,97 @@ public class GenericEventDetector {
|
||||
}
|
||||
|
||||
private String extractSourceState(ASTNode node) {
|
||||
ASTNode current = node.getParent();
|
||||
ASTNode current = node;
|
||||
while (current != null && !(current instanceof MethodDeclaration)) {
|
||||
if (current instanceof IfStatement ifStmt) {
|
||||
Expression expr = ifStmt.getExpression();
|
||||
String state = extractStateFromExpression(expr);
|
||||
if (state != null) return state;
|
||||
} else if (current instanceof SwitchCase switchCase) {
|
||||
if (!switchCase.expressions().isEmpty()) {
|
||||
return getSimpleNameString((Expression) switchCase.expressions().get(0));
|
||||
ASTNode parent = current.getParent();
|
||||
|
||||
// 1. Direct parent is IfStatement wrapper (e.g., if (state == X) { sm.sendEvent() })
|
||||
if (parent instanceof IfStatement ifStmt) {
|
||||
// Only consider if our current node is in the 'then' or 'else' part, not the condition
|
||||
if (ifStmt.getExpression() != current) {
|
||||
Expression expr = ifStmt.getExpression();
|
||||
String state = extractStateFromExpression(expr);
|
||||
if (state != null) return state;
|
||||
}
|
||||
} else if (current instanceof SwitchStatement switchStmt) {
|
||||
// If it's a switch block but we haven't hit a SwitchCase directly (AST hierarchy usually has SwitchCase as siblings of block statements)
|
||||
// Actually, walking up from the node, we will hit the SwitchStatement, but we need the SwitchCase right before us in the block.
|
||||
ASTNode parentBlock = current;
|
||||
// It's complex to walk siblings. A simpler heuristic is to just use IfStatement and SwitchCase (if wrapped correctly).
|
||||
}
|
||||
current = current.getParent();
|
||||
|
||||
// 2. Parent is a Block or SwitchStatement: perform sibling traversal
|
||||
if (parent instanceof Block block) {
|
||||
String state = extractStateFromSiblings(current, block.statements());
|
||||
if (state != null) return state;
|
||||
} else if (parent instanceof SwitchStatement switchStmt) {
|
||||
String state = extractStateFromSiblings(current, switchStmt.statements());
|
||||
if (state != null) return state;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractStateFromSiblings(ASTNode currentNode, List<?> statements) {
|
||||
int index = statements.indexOf(currentNode);
|
||||
if (index <= 0) return null; // No previous siblings
|
||||
|
||||
// Walk backwards through siblings
|
||||
for (int i = index - 1; i >= 0; i--) {
|
||||
Object stmtObj = statements.get(i);
|
||||
|
||||
if (stmtObj instanceof SwitchCase switchCase) {
|
||||
if (!switchCase.expressions().isEmpty()) {
|
||||
return getSimpleNameString((Expression) switchCase.expressions().get(0));
|
||||
}
|
||||
} else if (stmtObj instanceof IfStatement ifStmt) {
|
||||
// Guard clause detection: check if the 'then' block has a return/throw
|
||||
if (isGuardClause(ifStmt.getThenStatement())) {
|
||||
Expression expr = ifStmt.getExpression();
|
||||
// If it's a guard clause, it usually checks `state != EXPECTED`, so the true state *is* EXPECTED
|
||||
String state = extractStateFromExpression(expr);
|
||||
if (state != null) return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isGuardClause(Statement thenStatement) {
|
||||
if (thenStatement instanceof ReturnStatement || thenStatement instanceof ThrowStatement) {
|
||||
return true;
|
||||
}
|
||||
if (thenStatement instanceof Block block) {
|
||||
for (Object obj : block.statements()) {
|
||||
if (obj instanceof ReturnStatement || obj instanceof ThrowStatement) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String extractStateFromExpression(Expression expr) {
|
||||
if (expr instanceof InfixExpression infix) {
|
||||
if (infix.getOperator() == InfixExpression.Operator.EQUALS) {
|
||||
if (infix.getOperator() == InfixExpression.Operator.EQUALS || infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) {
|
||||
Expression left = infix.getLeftOperand();
|
||||
Expression right = infix.getRightOperand();
|
||||
// Usually one is a method call like getState(), the other is an enum
|
||||
if (left instanceof QualifiedName || left instanceof SimpleName && !(right instanceof SimpleName)) {
|
||||
|
||||
// Ignore null checks entirely
|
||||
if (left instanceof NullLiteral || right instanceof NullLiteral) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Usually one is a method call like getState() or a variable like `state`
|
||||
// and the other is the constant enum like `OrderState.PENDING` or `"PENDING"`
|
||||
|
||||
// If one is a QualifiedName (enum constant) or StringLiteral, it's likely the state
|
||||
if (left instanceof QualifiedName || left instanceof StringLiteral || left instanceof FieldAccess) {
|
||||
return getSimpleNameString(left);
|
||||
}
|
||||
if (right instanceof QualifiedName || right instanceof SimpleName && !(left instanceof SimpleName)) {
|
||||
if (right instanceof QualifiedName || right instanceof StringLiteral || right instanceof FieldAccess) {
|
||||
return getSimpleNameString(right);
|
||||
}
|
||||
return getSimpleNameString(right); // Fallback
|
||||
|
||||
// Fallback
|
||||
return getSimpleNameString(right);
|
||||
}
|
||||
} else if (expr instanceof MethodInvocation mi) {
|
||||
if ("equals".equals(mi.getName().getIdentifier()) && !mi.arguments().isEmpty()) {
|
||||
|
||||
@@ -57,6 +57,9 @@ public class ExporterCommand implements Callable<Integer> {
|
||||
@Option(names = {"--debug"}, description = "Enable diagnostic mode to show unresolved call chains and detailed analysis logging.", defaultValue = "false")
|
||||
private boolean debug;
|
||||
|
||||
@Option(names = {"--resolve-classpath"}, description = "Automatically run Maven/Gradle to resolve full classpath dependencies for perfect AST bindings.", defaultValue = "false")
|
||||
private boolean resolveClasspath;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
var out = spec.commandLine().getOut();
|
||||
@@ -87,7 +90,7 @@ public class ExporterCommand implements Callable<Integer> {
|
||||
if (jsonFile != null) {
|
||||
exportService.runJsonExporter(jsonFile, outputDir, selectedFormats, profiles, eventFormat, stateFormat);
|
||||
} else {
|
||||
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, eventFormat, stateFormat);
|
||||
exportService.runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, profiles, null, null, eventFormat, stateFormat, resolveClasspath);
|
||||
}
|
||||
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ All state machines have been exported to " + outputDir.toAbsolutePath()));
|
||||
return 0;
|
||||
|
||||
@@ -53,22 +53,22 @@ public class ExportService {
|
||||
public static final Set<String> STATE_MACHINE_RETURN_TYPES = Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory");
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds) throws IOException {
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, Collections.emptyList(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||
}
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles) throws IOException {
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||
}
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter) throws IOException {
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, flowsOverride, machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, false);
|
||||
}
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat);
|
||||
runExporter(inputDir, outputDir, selectedFormats, renderChoicesAsDiamonds, activeProfiles, null, null, eventFormat, stateFormat, false);
|
||||
}
|
||||
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
||||
public void runExporter(Path inputDir, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<String> activeProfiles, Path flowsOverride, String machineFilter, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat, boolean resolveClasspath) throws IOException {
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setActiveProfiles(activeProfiles);
|
||||
|
||||
@@ -94,6 +94,18 @@ public class ExportService {
|
||||
.collect(Collectors.toList());
|
||||
log.info("Setting JDT sourcepath: {}", sourcepaths);
|
||||
context.setSourcepath(sourcepaths);
|
||||
|
||||
if (resolveClasspath) {
|
||||
click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver classpathResolver = new click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver();
|
||||
List<String> dynamicClasspath = classpathResolver.resolveClasspath(projectRoot);
|
||||
if (!dynamicClasspath.isEmpty()) {
|
||||
context.setClasspath(dynamicClasspath);
|
||||
log.info("Injected {} external JARs into JDT ASTParser", dynamicClasspath.size());
|
||||
}
|
||||
} else {
|
||||
log.info("Dynamic classpath resolution disabled. Use --resolve-classpath to enable.");
|
||||
}
|
||||
|
||||
context.setResolveBindings(true);
|
||||
|
||||
Path hintsFile = inputDir.resolve("hints.json");
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
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
|
||||
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 = Path.of(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"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveGradleClasspathDynamically(@TempDir Path tempDir) throws IOException {
|
||||
// Create dummy build.gradle
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListIfNoBuildFileFound(@TempDir Path tempDir) {
|
||||
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
|
||||
List<String> classpath = resolver.resolveClasspath(tempDir);
|
||||
assertThat(classpath).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
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.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Tag("control_flow_sibling_avoidance")
|
||||
class GenericEventDetectorControlFlowTest {
|
||||
|
||||
@Test
|
||||
void shouldDetectStateFromDirectIfWrapper(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderService {
|
||||
private StateMachine sm;
|
||||
public void processOrder(OrderState state) {
|
||||
if (state == OrderState.PENDING) {
|
||||
sm.sendEvent(OrderEvent.PAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
public void sendEvent(OrderEvent e) {}
|
||||
}
|
||||
|
||||
enum OrderState { PENDING }
|
||||
enum OrderEvent { PAY }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
|
||||
|
||||
assertThat(triggers).hasSize(1);
|
||||
TriggerPoint tp = triggers.get(0);
|
||||
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
|
||||
assertThat(tp.getSourceState()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectStateFromGuardClauseSibling(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderService {
|
||||
private StateMachine sm;
|
||||
public void processOrder(OrderState state) {
|
||||
if (state != OrderState.PENDING) {
|
||||
return;
|
||||
}
|
||||
|
||||
sm.sendEvent(OrderEvent.PAY);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
public void sendEvent(OrderEvent e) {}
|
||||
}
|
||||
|
||||
enum OrderState { PENDING }
|
||||
enum OrderEvent { PAY }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
|
||||
|
||||
assertThat(triggers).hasSize(1);
|
||||
TriggerPoint tp = triggers.get(0);
|
||||
assertThat(tp.getEvent()).isEqualTo("OrderEvent.PAY");
|
||||
assertThat(tp.getSourceState()).isEqualTo("PENDING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectStateFromSwitchCaseSibling(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderService {
|
||||
private StateMachine sm;
|
||||
public void processOrder(OrderState state) {
|
||||
switch (state) {
|
||||
case PENDING:
|
||||
sm.sendEvent(OrderEvent.PAY);
|
||||
break;
|
||||
case PAID:
|
||||
sm.sendEvent(OrderEvent.SHIP);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
public void sendEvent(OrderEvent e) {}
|
||||
}
|
||||
|
||||
enum OrderState { PENDING, PAID }
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("OrderService.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderService").getRoot());
|
||||
|
||||
assertThat(triggers).hasSize(2);
|
||||
|
||||
TriggerPoint payTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.PAY")).findFirst().get();
|
||||
assertThat(payTrigger.getSourceState()).isEqualTo("PENDING");
|
||||
|
||||
TriggerPoint shipTrigger = triggers.stream().filter(t -> t.getEvent().equals("OrderEvent.SHIP")).findFirst().get();
|
||||
assertThat(shipTrigger.getSourceState()).isEqualTo("PAID");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user