From 23d79d1930e291292782194a3fbb8c307ef085b1 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 21 Jun 2026 15:36:36 +0200 Subject: [PATCH] event maching heuristic update 4 --- .../analysis/resolver/ConstantResolver.java | 13 ++ .../service/HeuristicCallGraphEngine.java | 192 +++++++++++------- .../service/BuilderLocalVariableTest.java | 69 +++++++ .../service/ConstructorInvocationTest.java | 137 +++++++++++++ .../service/ConstructorLocalVariableTest.java | 72 +++++++ .../analysis/service/LocalVariableTest.java | 122 +++++++++++ .../analysis/service/ReturnStatementTest.java | 66 ++++++ 7 files changed, 603 insertions(+), 68 deletions(-) create mode 100644 state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/BuilderLocalVariableTest.java create mode 100644 state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorLocalVariableTest.java create mode 100644 state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/LocalVariableTest.java create mode 100644 state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ReturnStatementTest.java diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java index d11135a..739713c 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/ConstantResolver.java @@ -17,6 +17,19 @@ public class ConstantResolver { private String resolveInternal(Expression expr, CodebaseContext context, Set visited) { if (expr == null) return null; + // Unwrap common wrappers + if (expr instanceof CastExpression ce) { + return resolveInternal(ce.getExpression(), context, visited); + } + if (expr instanceof ParenthesizedExpression pe) { + return resolveInternal(pe.getExpression(), context, visited); + } + if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) { + if (!mi.arguments().isEmpty()) { + return resolveInternal((Expression) mi.arguments().get(0), context, visited); + } + } + // 1. Literal? if (expr instanceof StringLiteral sl) { return sl.getLiteralValue(); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java index e954337..8bd76a8 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngine.java @@ -126,7 +126,10 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, currentParamName, localMethodName); if (localSetterExpr != null) { List setterEvents = new ArrayList<>(); - extractConstantsFromExpression(localSetterExpr, setterEvents); + List tracedSetters = traceVariableAll(localSetterExpr); + for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) { + extractConstantsFromExpression(tracedSetter, setterEvents); + } if (!setterEvents.isEmpty()) { return TriggerPoint.builder() .event(tp.getEvent()) @@ -240,7 +243,10 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { org.eclipse.jdt.core.dom.Expression localSetterExpr = traceLocalSetter(entryMethod, varName, methodName); System.out.println("localSetterExpr = " + (localSetterExpr != null ? localSetterExpr.toString() : "null")); if (localSetterExpr != null) { - extractConstantsFromExpression(localSetterExpr, polymorphicEvents); + List tracedSetters = traceVariableAll(localSetterExpr); + for (org.eclipse.jdt.core.dom.Expression tracedSetter : tracedSetters) { + extractConstantsFromExpression(tracedSetter, polymorphicEvents); + } System.out.println("Extracted from setter: " + polymorphicEvents); if (!polymorphicEvents.isEmpty()) { return TriggerPoint.builder() @@ -496,30 +502,33 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } } if (!handled) { - extractConstantsFromExpression(retExpr, constants); - String val = constantResolver.resolve(retExpr, context); - if (val != null) { - if (val.startsWith("ENUM_SET:")) { - for (String eVal : val.substring(9).split(",")) { - String parsed = parseEnumSetElement(eVal); - if (!constants.contains(parsed)) constants.add(parsed); + List tracedRetExprs = traceVariableAll(retExpr); + for (Expression tracedRetExpr : tracedRetExprs) { + extractConstantsFromExpression(tracedRetExpr, constants); + String val = constantResolver.resolve(tracedRetExpr, context); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + for (String eVal : val.substring(9).split(",")) { + String parsed = parseEnumSetElement(eVal); + if (!constants.contains(parsed)) constants.add(parsed); + } + } else { + if (!constants.contains(val)) constants.add(val); + } + } else if (tracedRetExpr instanceof org.eclipse.jdt.core.dom.SimpleName sn) { + List consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited); + if (!consts.isEmpty()) { + constants.addAll(consts); + } else { + constants.add(sn.toString()); + } + } else if (tracedRetExpr instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { + List consts = traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited); + if (!consts.isEmpty()) { + constants.addAll(consts); + } else { + constants.add(fa.getName().getIdentifier()); } - } else { - if (!constants.contains(val)) constants.add(val); - } - } else if (retExpr instanceof org.eclipse.jdt.core.dom.SimpleName sn) { - List consts = traceFieldInConstructors(td, sn.getIdentifier(), context, visited); - if (!consts.isEmpty()) { - constants.addAll(consts); - } else { - constants.add(sn.toString()); - } - } else if (retExpr instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { - List consts = traceFieldInConstructors(td, fa.getName().getIdentifier(), context, visited); - if (!consts.isEmpty()) { - constants.addAll(consts); - } else { - constants.add(fa.getName().getIdentifier()); } } } @@ -542,9 +551,11 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { for (MethodDeclaration md : td.getMethods()) { if (md.getBody() != null) { md.getBody().accept(new ASTVisitor() { - @Override public boolean visit(ReturnStatement node) { - extractConstantsFromExpression(node.getExpression(), resolvedConstants); + List tracedReturns = traceVariableAll(node.getExpression()); + for (org.eclipse.jdt.core.dom.Expression tracedRet : tracedReturns) { + extractConstantsFromExpression(tracedRet, resolvedConstants); + } return super.visit(node); } }); @@ -567,6 +578,8 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { extractConstantsFromExpression(pe.getExpression(), constants); } else if (expr instanceof org.eclipse.jdt.core.dom.CastExpression ce) { extractConstantsFromExpression(ce.getExpression(), constants); + } else if (expr instanceof org.eclipse.jdt.core.dom.Assignment assignment) { + extractConstantsFromExpression(assignment.getRightHandSide(), constants); } else if (expr instanceof org.eclipse.jdt.core.dom.SwitchExpression se) { se.accept(new org.eclipse.jdt.core.dom.ASTVisitor() { @Override @@ -1135,33 +1148,43 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { return (TypeDeclaration) parent; } private Expression traceVariable(Expression expr) { + List all = traceVariableAll(expr); + return all.isEmpty() ? expr : all.get(all.size() - 1); + } + + private List traceVariableAll(Expression expr) { + List results = new ArrayList<>(); + System.out.println("traceVariableAll called with: " + expr); if (expr instanceof SimpleName sn) { String varName = sn.getIdentifier(); MethodDeclaration enclosingMethod = findEnclosingMethod(expr); + System.out.println("traceVariableAll SimpleName=" + varName + " enclosingMethod=" + (enclosingMethod != null ? enclosingMethod.getName() : "null")); if (enclosingMethod != null) { - final Expression[] initializer = new Expression[1]; enclosingMethod.accept(new ASTVisitor() { @Override public boolean visit(VariableDeclarationFragment node) { if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { - initializer[0] = node.getInitializer(); + System.out.println("traceVariableAll found init: " + node.getInitializer()); + results.addAll(traceVariableAll(node.getInitializer())); } return super.visit(node); } @Override public boolean visit(Assignment node) { if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { - initializer[0] = node.getRightHandSide(); + System.out.println("traceVariableAll found assign: " + node.getRightHandSide()); + results.addAll(traceVariableAll(node.getRightHandSide())); } return super.visit(node); } }); - if (initializer[0] != null) { - return traceVariable(initializer[0]); + if (!results.isEmpty()) { + return results; } } } - return expr; + results.add(expr); + return results; } private List traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set visited) { @@ -1173,14 +1196,17 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj; if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) { Expression right = frag.getInitializer(); - if (right instanceof MethodInvocation mi) { - String calledName = mi.getName().getIdentifier(); - String targetClass = context.getFqn(td); - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu)); - } else { - String val = constantResolver.resolve(right, context); - if (val != null) results.add(val); + List tracedRights = traceVariableAll(right); + for (Expression tracedRight : tracedRights) { + if (tracedRight instanceof MethodInvocation mi) { + String calledName = mi.getName().getIdentifier(); + String targetClass = context.getFqn(td); + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu)); + } else { + String val = constantResolver.resolve(tracedRight, context); + if (val != null) results.add(val); + } } } } @@ -1194,26 +1220,29 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) { Expression right = node.getRightHandSide(); - if (right instanceof MethodInvocation mi) { - String calledName = mi.getName().getIdentifier(); - String targetClass = context.getFqn(td); - - if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) { - targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName(); - } else if (mi.getExpression() instanceof SimpleName) { - String exprName = ((SimpleName) mi.getExpression()).getIdentifier(); - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu); - if (targetTd != null) { - targetClass = context.getFqn(targetTd); + List tracedRights = traceVariableAll(right); + for (Expression tracedRight : tracedRights) { + if (tracedRight instanceof MethodInvocation mi) { + String calledName = mi.getName().getIdentifier(); + String targetClass = context.getFqn(td); + + if (mi.getExpression() != null && mi.resolveMethodBinding() != null && mi.resolveMethodBinding().getDeclaringClass() != null) { + targetClass = mi.resolveMethodBinding().getDeclaringClass().getQualifiedName(); + } else if (mi.getExpression() instanceof SimpleName) { + String exprName = ((SimpleName) mi.getExpression()).getIdentifier(); + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + TypeDeclaration targetTd = context.resolveStaticImport(exprName, cu); + if (targetTd != null) { + targetClass = context.getFqn(targetTd); + } } - } - CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; - results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu)); - } else { - String val = constantResolver.resolve(right, context); - if (val != null) results.add(val); + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu)); + } else { + String val = constantResolver.resolve(tracedRight, context); + if (val != null) results.add(val); + } } } return super.visit(node); @@ -1303,10 +1332,11 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { public boolean visit(Assignment asn) { Expression left = asn.getLeftHandSide(); if ((left instanceof SimpleName sn && sn.getIdentifier().equals(fieldName)) || - (left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName))) { + (left instanceof FieldAccess fa && fa.getName().getIdentifier().equals(fieldName)) || + (left instanceof SuperFieldAccess sfa && sfa.getName().getIdentifier().equals(fieldName))) { Expression right = asn.getRightHandSide(); - if (right instanceof SimpleName snRight) { - String rightName = snRight.getIdentifier(); + String rightName = extractVariableName(right); + if (rightName != null) { for (int i = 0; i < constructorMd.parameters().size(); i++) { SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); if (svd.getName().getIdentifier().equals(rightName)) { @@ -1319,15 +1349,22 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } @Override public boolean visit(ConstructorInvocation node) { + org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = node.resolveConstructorBinding(); TypeDeclaration enclosingTd = findEnclosingType(node); if (enclosingTd != null) { for (MethodDeclaration otherMd : enclosingTd.getMethods()) { - if (otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size()) { + boolean matches = false; + if (resolvedBinding != null && otherMd.resolveBinding() != null) { + matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); + } else { + matches = otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size(); + } + if (matches) { int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited); if (targetIdx >= 0 && targetIdx < node.arguments().size()) { Expression arg = (Expression) node.arguments().get(targetIdx); - if (arg instanceof SimpleName snArg) { - String argName = snArg.getIdentifier(); + String argName = extractVariableName(arg); + if (argName != null) { for (int i = 0; i < constructorMd.parameters().size(); i++) { SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); if (svd.getName().getIdentifier().equals(argName)) { @@ -1343,6 +1380,7 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } @Override public boolean visit(SuperConstructorInvocation node) { + org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = node.resolveConstructorBinding(); TypeDeclaration enclosingTd = findEnclosingType(node); if (enclosingTd != null) { String superFqn = context.getSuperclassFqn(enclosingTd); @@ -1350,12 +1388,18 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { TypeDeclaration superTd = context.getTypeDeclaration(superFqn); if (superTd != null) { for (MethodDeclaration otherMd : superTd.getMethods()) { - if (otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size()) { + boolean matches = false; + if (resolvedBinding != null && otherMd.resolveBinding() != null) { + matches = resolvedBinding.isEqualTo(otherMd.resolveBinding()) || resolvedBinding.getKey().equals(otherMd.resolveBinding().getKey()); + } else { + matches = otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size(); + } + if (matches) { int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, visited); if (targetIdx >= 0 && targetIdx < node.arguments().size()) { Expression arg = (Expression) node.arguments().get(targetIdx); - if (arg instanceof SimpleName snArg) { - String argName = snArg.getIdentifier(); + String argName = extractVariableName(arg); + if (argName != null) { for (int i = 0; i < constructorMd.parameters().size(); i++) { SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); if (svd.getName().getIdentifier().equals(argName)) { @@ -1374,4 +1418,16 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { }); return foundIdx[0]; } + + private String extractVariableName(Expression expr) { + if (expr instanceof SimpleName sn) return sn.getIdentifier(); + if (expr instanceof FieldAccess fa) return fa.getName().getIdentifier(); + if (expr instanceof SuperFieldAccess sfa) return sfa.getName().getIdentifier(); + if (expr instanceof CastExpression ce) return extractVariableName(ce.getExpression()); + if (expr instanceof ParenthesizedExpression pe) return extractVariableName(pe.getExpression()); + if (expr instanceof MethodInvocation mi && (mi.getName().getIdentifier().equals("requireNonNull") || mi.getName().getIdentifier().equals("notNull"))) { + if (!mi.arguments().isEmpty()) return extractVariableName((Expression)mi.arguments().get(0)); + } + return null; + } } \ No newline at end of file diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/BuilderLocalVariableTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/BuilderLocalVariableTest.java new file mode 100644 index 0000000..36ad247 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/BuilderLocalVariableTest.java @@ -0,0 +1,69 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +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 static org.assertj.core.api.Assertions.assertThat; + +class BuilderLocalVariableTest { + + @Test + void shouldTraceLocalVariableInBuilderPattern(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + OrderEvents e = OrderEvents.PAY; + RichOrderEvent event = new RichOrderEvent(); + event.setType(e); + service.updateOrderState(event.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + private OrderEvents type; + public void setType(OrderEvents type) { this.type = type; } + public OrderEvents getType() { return type; } + } + + enum OrderEvents { PAY } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + System.out.println("POLY EVENTS FOUND: " + chain.getTriggerPoint().getPolymorphicEvents()); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.PAY"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorInvocationTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorInvocationTest.java index 007e1a4..eeb7c94 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorInvocationTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorInvocationTest.java @@ -266,6 +266,143 @@ class ConstructorInvocationTest { List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .contains("OrderEvents.PAY"); + } + @Test + void shouldTraceThroughWrappedConstructorArguments(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + import java.util.Objects; + + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + private OrderEvents type; + + public RichOrderEvent() { + this(Objects.requireNonNull(OrderEvents.PAY)); + } + + public RichOrderEvent(OrderEvents type) { + this((OrderEvents) type, "DEFAULT_INFO"); + } + + public RichOrderEvent(OrderEvents type, String info) { + this.type = Objects.requireNonNull(type); + } + + public OrderEvents getType() { return type; } + } + + enum OrderEvents { PAY } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .contains("OrderEvents.PAY"); + } + + @Test + void shouldTraceThroughComplexOverloadedConstructors(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + + public class OrderController { + private OrderService service; + public void processOrderEvent(EnterpriseEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class EnterpriseEvent { + private OrderEvents type; + private String info; + + public EnterpriseEvent() { + this(OrderEvents.PAY); + } + + // Matches by parameter count but wrong type! + public EnterpriseEvent(String info) { + this.info = info; + } + + public EnterpriseEvent(OrderEvents type) { + this("info", type); + } + + // Target of previous constructor + public EnterpriseEvent(String info, OrderEvents type) { + this(type, info, null); + } + + // Final destination + public EnterpriseEvent(OrderEvents type, String info, Object extra) { + this.type = type; + this.info = info; + } + + public OrderEvents getType() { return type; } + } + + enum OrderEvents { PAY } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); CallChain chain = chains.get(0); assertThat(chain.getTriggerPoint().getPolymorphicEvents()) diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorLocalVariableTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorLocalVariableTest.java new file mode 100644 index 0000000..d578b2f --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorLocalVariableTest.java @@ -0,0 +1,72 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +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 static org.assertj.core.api.Assertions.assertThat; + +class ConstructorLocalVariableTest { + + @Test + void shouldTraceLocalVariableInConstructor(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + private OrderEvents type; + + public RichOrderEvent() { + OrderEvents e = OrderEvents.PAY; + this.type = e; + } + + public OrderEvents getType() { + return type; + } + } + + enum OrderEvents { PAY } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.PAY"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/LocalVariableTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/LocalVariableTest.java new file mode 100644 index 0000000..bcd20ec --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/LocalVariableTest.java @@ -0,0 +1,122 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +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 static org.assertj.core.api.Assertions.assertThat; + +class LocalVariableTest { + + @Test + void shouldTraceLocalVariableInGetter(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + public OrderEvents getType() { + OrderEvents e = OrderEvents.PAY; + return e; + } + } + + enum OrderEvents { PAY } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .contains("OrderEvents.PAY") + .doesNotContain("e"); // Should not blindly add the variable name + } + + @Test + void shouldTraceMultipleAssignments(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + public OrderEvents getType() { + OrderEvents e = null; + if (System.currentTimeMillis() % 2 == 0) { + e = OrderEvents.PAY; + } else { + e = OrderEvents.CANCEL; + } + return e; + } + } + + enum OrderEvents { PAY, CANCEL } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCEL"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ReturnStatementTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ReturnStatementTest.java new file mode 100644 index 0000000..1ff4bf1 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ReturnStatementTest.java @@ -0,0 +1,66 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +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 static org.assertj.core.api.Assertions.assertThat; + +class ReturnStatementTest { + + @Test + void shouldHandleAssignmentInReturn(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + private OrderEvents type; + public OrderEvents getType() { + return this.type = OrderEvents.PAY; + } + } + + enum OrderEvents { PAY } + """; + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.PAY"); + } +}