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 07e40af..ec5fb4f 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 @@ -461,6 +461,25 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); if (targetTd == null) targetTd = context.getTypeDeclaration(cName); + if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { + List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); + constants.addAll(delegationResult); + handled = true; + } + } + } + } else if (retExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi) { + String superFqn = context.getSuperclassFqn(td); + if (superFqn != null) { + String called = superFqn + "." + smi.getName().getIdentifier(); + if (visited.contains(called)) { + handled = true; + } else { + String cName = superFqn; + String mName = smi.getName().getIdentifier(); + TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); + if (targetTd == null) targetTd = context.getTypeDeclaration(cName); + if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); constants.addAll(delegationResult); @@ -488,6 +507,13 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } 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()); + } } } } @@ -1168,35 +1194,18 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { } @Override public boolean visit(ConstructorInvocation node) { - for (Object argObj : node.arguments()) { - Expression arg = (Expression) argObj; - String val = constantResolver.resolve(arg, context); - if (val != null) { - if (val.startsWith("ENUM_SET:")) { - for (String eVal : val.substring(9).split(",")) { - results.add(eVal.substring(eVal.lastIndexOf('.') + 1)); - } - } else { - results.add(val); - } - } - } + processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited); return super.visit(node); } @Override public boolean visit(SuperConstructorInvocation node) { - for (Object argObj : node.arguments()) { - Expression arg = (Expression) argObj; - String val = constantResolver.resolve(arg, context); - if (val != null) { - if (val.startsWith("ENUM_SET:")) { - for (String eVal : val.substring(9).split(",")) { - results.add(eVal.substring(eVal.lastIndexOf('.') + 1)); - } - } else { - results.add(val); - } + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + String superFqn = context.getSuperclassFqn(enclosingTd); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited); } } return super.visit(node); @@ -1220,4 +1229,123 @@ public class HeuristicCallGraphEngine implements CallGraphEngine { return results; } + private void processConstructorInvocationArgs(List arguments, TypeDeclaration targetTd, ASTNode callNode, List results, String fieldName, CodebaseContext context, Set visited) { + if (targetTd == null) return; + MethodDeclaration callerMd = findEnclosingMethod(callNode); + + org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = null; + if (callNode instanceof ConstructorInvocation ci) { + resolvedBinding = ci.resolveConstructorBinding(); + } else if (callNode instanceof SuperConstructorInvocation sci) { + resolvedBinding = sci.resolveConstructorBinding(); + } + + for (MethodDeclaration otherMd : targetTd.getMethods()) { + 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() == arguments.size() && otherMd != callerMd; + } + + if (matches) { + int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new java.util.HashSet<>()); + if (targetIdx >= 0 && targetIdx < arguments.size()) { + Expression arg = (Expression) arguments.get(targetIdx); + String val = constantResolver.resolve(arg, context); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + for (String eVal : val.substring(9).split(",")) { + results.add(eVal.substring(eVal.lastIndexOf('.') + 1)); + } + } else { + results.add(val); + } + } + } + } + } + } + + private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, java.util.Set visited) { + if (constructorMd == null || constructorMd.getBody() == null) return -1; + if (!visited.add(constructorMd)) return -1; + + final int[] foundIdx = {-1}; + constructorMd.getBody().accept(new ASTVisitor() { + @Override + 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))) { + Expression right = asn.getRightHandSide(); + if (right instanceof SimpleName snRight) { + String rightName = snRight.getIdentifier(); + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(rightName)) { + foundIdx[0] = i; + } + } + } + } + return super.visit(asn); + } + @Override + public boolean visit(ConstructorInvocation node) { + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + for (MethodDeclaration otherMd : enclosingTd.getMethods()) { + if (otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size()) { + 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(); + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(argName)) { + foundIdx[0] = i; + } + } + } + } + } + } + } + return super.visit(node); + } + @Override + public boolean visit(SuperConstructorInvocation node) { + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + String superFqn = context.getSuperclassFqn(enclosingTd); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + if (superTd != null) { + for (MethodDeclaration otherMd : superTd.getMethods()) { + if (otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size()) { + 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(); + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(argName)) { + foundIdx[0] = i; + } + } + } + } + } + } + } + } + } + return super.visit(node); + } + }); + return foundIdx[0]; + } } \ No newline at end of file diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index 673da84..de2650f 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -418,6 +418,25 @@ public class JdtCallGraphEngine implements CallGraphEngine { TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); if (targetTd == null) targetTd = context.getTypeDeclaration(cName); + if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { + List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); + constants.addAll(delegationResult); + handled = true; + } + } + } + } else if (retExpr instanceof org.eclipse.jdt.core.dom.SuperMethodInvocation smi) { + String superFqn = context.getSuperclassFqn(td); + if (superFqn != null) { + String called = superFqn + "." + smi.getName().getIdentifier(); + if (visited.contains(called)) { + handled = true; + } else { + String cName = superFqn; + String mName = smi.getName().getIdentifier(); + TypeDeclaration targetTd = contextCu != null ? context.getTypeDeclaration(cName, contextCu) : context.getTypeDeclaration(cName); + if (targetTd == null) targetTd = context.getTypeDeclaration(cName); + if (targetTd != null && context.findMethodDeclaration(targetTd, mName, true) != null) { List delegationResult = resolveMethodReturnConstant(cName, mName, depth + 1, visited, contextCu); constants.addAll(delegationResult); @@ -451,6 +470,13 @@ public class JdtCallGraphEngine implements CallGraphEngine { } 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()); + } } } } @@ -1135,35 +1161,18 @@ public class JdtCallGraphEngine implements CallGraphEngine { } @Override public boolean visit(ConstructorInvocation node) { - for (Object argObj : node.arguments()) { - Expression arg = (Expression) argObj; - String val = constantResolver.resolve(arg, context); - if (val != null) { - if (val.startsWith("ENUM_SET:")) { - for (String eVal : val.substring(9).split(",")) { - results.add(eVal.substring(eVal.lastIndexOf('.') + 1)); - } - } else { - results.add(val); - } - } - } + processConstructorInvocationArgs(node.arguments(), findEnclosingType(node), node, results, fieldName, context, visited); return super.visit(node); } @Override public boolean visit(SuperConstructorInvocation node) { - for (Object argObj : node.arguments()) { - Expression arg = (Expression) argObj; - String val = constantResolver.resolve(arg, context); - if (val != null) { - if (val.startsWith("ENUM_SET:")) { - for (String eVal : val.substring(9).split(",")) { - results.add(eVal.substring(eVal.lastIndexOf('.') + 1)); - } - } else { - results.add(val); - } + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + String superFqn = context.getSuperclassFqn(enclosingTd); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + processConstructorInvocationArgs(node.arguments(), superTd, node, results, fieldName, context, visited); } } return super.visit(node); @@ -1187,4 +1196,123 @@ public class JdtCallGraphEngine implements CallGraphEngine { return results; } + private void processConstructorInvocationArgs(List arguments, TypeDeclaration targetTd, ASTNode callNode, List results, String fieldName, CodebaseContext context, Set visited) { + if (targetTd == null) return; + MethodDeclaration callerMd = findEnclosingMethod(callNode); + + org.eclipse.jdt.core.dom.IMethodBinding resolvedBinding = null; + if (callNode instanceof ConstructorInvocation ci) { + resolvedBinding = ci.resolveConstructorBinding(); + } else if (callNode instanceof SuperConstructorInvocation sci) { + resolvedBinding = sci.resolveConstructorBinding(); + } + + for (MethodDeclaration otherMd : targetTd.getMethods()) { + 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() == arguments.size() && otherMd != callerMd; + } + + if (matches) { + int targetIdx = findAssignedParameterIndex(otherMd, fieldName, context, new java.util.HashSet<>()); + if (targetIdx >= 0 && targetIdx < arguments.size()) { + Expression arg = (Expression) arguments.get(targetIdx); + String val = constantResolver.resolve(arg, context); + if (val != null) { + if (val.startsWith("ENUM_SET:")) { + for (String eVal : val.substring(9).split(",")) { + results.add(eVal.substring(eVal.lastIndexOf('.') + 1)); + } + } else { + results.add(val); + } + } + } + } + } + } + + private int findAssignedParameterIndex(MethodDeclaration constructorMd, String fieldName, CodebaseContext context, java.util.Set visited) { + if (constructorMd == null || constructorMd.getBody() == null) return -1; + if (!visited.add(constructorMd)) return -1; + + final int[] foundIdx = {-1}; + constructorMd.getBody().accept(new ASTVisitor() { + @Override + 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))) { + Expression right = asn.getRightHandSide(); + if (right instanceof SimpleName snRight) { + String rightName = snRight.getIdentifier(); + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(rightName)) { + foundIdx[0] = i; + } + } + } + } + return super.visit(asn); + } + @Override + public boolean visit(ConstructorInvocation node) { + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + for (MethodDeclaration otherMd : enclosingTd.getMethods()) { + if (otherMd.isConstructor() && otherMd != constructorMd && otherMd.parameters().size() == node.arguments().size()) { + 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(); + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(argName)) { + foundIdx[0] = i; + } + } + } + } + } + } + } + return super.visit(node); + } + @Override + public boolean visit(SuperConstructorInvocation node) { + TypeDeclaration enclosingTd = findEnclosingType(node); + if (enclosingTd != null) { + String superFqn = context.getSuperclassFqn(enclosingTd); + if (superFqn != null) { + TypeDeclaration superTd = context.getTypeDeclaration(superFqn); + if (superTd != null) { + for (MethodDeclaration otherMd : superTd.getMethods()) { + if (otherMd.isConstructor() && otherMd.parameters().size() == node.arguments().size()) { + 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(); + for (int i = 0; i < constructorMd.parameters().size(); i++) { + SingleVariableDeclaration svd = (SingleVariableDeclaration) constructorMd.parameters().get(i); + if (svd.getName().getIdentifier().equals(argName)) { + foundIdx[0] = i; + } + } + } + } + } + } + } + } + } + return super.visit(node); + } + }); + return foundIdx[0]; + } } \ No newline at end of file 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 new file mode 100644 index 0000000..007e1a4 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/ConstructorInvocationTest.java @@ -0,0 +1,275 @@ +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 ConstructorInvocationTest { + + @Test + void shouldNotBlindlyAddAllConstructorArguments(@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; + private String irrelevantInfo; + + public RichOrderEvent() { + this(OrderEvents.PAY, "DEFAULT_INFO"); + } + + public RichOrderEvent(OrderEvents type, String info) { + this.type = type; + this.irrelevantInfo = 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()) + .contains("OrderEvents.PAY") + .doesNotContain("DEFAULT_INFO"); + } + + + @Test + void shouldHandleOverloadedConstructorsGracefully(@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; + private String irrelevantInfo; + private int count; + + public RichOrderEvent() { + this(OrderEvents.PAY, "DEFAULT_INFO"); + } + + // Overloaded constructor 1 + public RichOrderEvent(OrderEvents type, String info) { + this.type = type; + this.irrelevantInfo = info; + } + + // Overloaded constructor 2 (same number of arguments!) + public RichOrderEvent(String info, int count) { + this.irrelevantInfo = info; + this.count = count; + // type is not set here + } + + 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") + .doesNotContain("DEFAULT_INFO") + .doesNotContain("count"); + } + + + @Test + void shouldHandleChainedConstructorDelegationGracefully(@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 BaseEvent { + protected OrderEvents type; + public BaseEvent(OrderEvents type) { + this.type = type; + } + } + + class RichOrderEvent extends BaseEvent { + public RichOrderEvent() { + this(OrderEvents.PAY); + } + + public RichOrderEvent(OrderEvents type) { + this(type, "DEFAULT_INFO"); + } + + public RichOrderEvent(OrderEvents type, String info) { + super(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") + .doesNotContain("DEFAULT_INFO"); + } + + @Test + void shouldHandleFieldAccessGracefully(@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() { + this(OrderEvents.PAY); + } + + public RichOrderEvent(OrderEvents type) { + this.type = type; + } + + public OrderEvents getType() { + return this.type; // Specifically testing FieldAccess + } + } + + 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"); + } +} + diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/SuperMethodInvocationTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/SuperMethodInvocationTest.java new file mode 100644 index 0000000..79ee812 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/SuperMethodInvocationTest.java @@ -0,0 +1,71 @@ +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 SuperMethodInvocationTest { + + @Test + void shouldHandleSuperMethodInvocation(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(DerivedEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class BaseEvent { + public OrderEvents getType() { + return OrderEvents.PAY; + } + } + + class DerivedEvent extends BaseEvent { + public OrderEvents getType() { + return super.getType(); + } + } + + 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"); + } +}