more fixes 5
This commit is contained in:
@@ -146,46 +146,138 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Second, filter by vertical routing based on Service class name
|
|
||||||
if (chain.getMethodChain() != null) {
|
|
||||||
boolean hasElectronicsService = false;
|
// Generic approach combining Prefix, Package, and String-distance heuristics
|
||||||
boolean hasComputerStoreService = false;
|
// without relying on a hardcoded list of magic keywords.
|
||||||
|
if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) {
|
||||||
|
String smPackage = getPackageName(currentMachineName);
|
||||||
|
String smSimple = getSimpleClassName(currentMachineName);
|
||||||
|
String smPrefix = getFirstCamelCaseWord(smSimple);
|
||||||
|
|
||||||
|
boolean hasPositiveMatch = false;
|
||||||
|
boolean hasStrongMismatch = false;
|
||||||
|
|
||||||
for (String method : chain.getMethodChain()) {
|
for (String method : chain.getMethodChain()) {
|
||||||
String className = method;
|
String chainClass = getClassNameOnly(method);
|
||||||
if (method.contains(".")) {
|
String chainPackage = getPackageName(chainClass);
|
||||||
className = method.substring(0, method.lastIndexOf('.'));
|
String chainSimple = getSimpleClassName(chainClass);
|
||||||
if (className.contains(".")) {
|
String chainPrefix = getFirstCamelCaseWord(chainSimple);
|
||||||
className = className.substring(className.lastIndexOf('.') + 1);
|
|
||||||
}
|
// 1. Explicit Prefix Match (e.g. ElectronicsOrderService & ElectronicsStateMachine)
|
||||||
|
if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) {
|
||||||
|
hasPositiveMatch = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String lowerClass = className.toLowerCase();
|
// 2. Explicit Sub-Package Match (e.g. com.company.electronics.service & com.company.electronics.sm)
|
||||||
if (lowerClass.contains("electronics")) {
|
// We consider it a match if they share a package segment beyond the first 2 (which are usually com.company)
|
||||||
hasElectronicsService = true;
|
String deepShared = getDeepSharedPackage(smPackage, chainPackage);
|
||||||
} else if (lowerClass.contains("computerstore")) {
|
if (deepShared != null) {
|
||||||
hasComputerStoreService = true;
|
hasPositiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if there is a strong mismatch.
|
||||||
|
// A strong mismatch happens if the chain class has a VERY distinct prefix/package
|
||||||
|
// that contradicts the SM's distinct prefix/package.
|
||||||
|
// Without magic keywords, we guess if a prefix is distinct if it's long enough and doesn't match.
|
||||||
|
if (smPrefix != null && chainPrefix != null && !smPrefix.equals(chainPrefix)) {
|
||||||
|
// They have different first CamelCase words.
|
||||||
|
// This could be Electronics vs ComputerStore, or it could be Common vs ComputerStore.
|
||||||
|
// If they diverge at a deep package level, it's a strong mismatch.
|
||||||
|
if (isDeepPackageDivergence(smPackage, chainPackage)) {
|
||||||
|
hasStrongMismatch = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String lowerMachine = currentMachineName != null ? currentMachineName.toLowerCase() : "";
|
// If we found a positive structural match, route it!
|
||||||
boolean isElectronicsMachine = lowerMachine.contains("electronics");
|
if (hasPositiveMatch) {
|
||||||
// If the machine is an electronics machine, it must NOT be triggered by computer store
|
return true;
|
||||||
if (isElectronicsMachine && hasComputerStoreService && !hasElectronicsService) {
|
}
|
||||||
|
|
||||||
|
// If there's no positive match, but we detected a strong structural mismatch, reject it!
|
||||||
|
if (hasStrongMismatch) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the machine is NOT an electronics machine (e.g. standard computer store SM),
|
// If neither, we allow it (fallback for common/shared endpoints without distinct domains)
|
||||||
// it must NOT be triggered by electronics services
|
|
||||||
if (!isElectronicsMachine && hasElectronicsService && !hasComputerStoreService) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String simplify(String name) {
|
private String getPackageName(String fqn) {
|
||||||
|
if (fqn == null || !fqn.contains(".")) return "";
|
||||||
|
return fqn.substring(0, fqn.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSimpleClassName(String fqn) {
|
||||||
|
if (fqn == null) return "";
|
||||||
|
if (!fqn.contains(".")) return fqn;
|
||||||
|
return fqn.substring(fqn.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClassNameOnly(String methodFqn) {
|
||||||
|
if (methodFqn == null) return "";
|
||||||
|
String clean = methodFqn;
|
||||||
|
if (clean.contains("(")) {
|
||||||
|
clean = clean.substring(0, clean.indexOf('('));
|
||||||
|
}
|
||||||
|
if (clean.contains(".")) {
|
||||||
|
clean = clean.substring(0, clean.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFirstCamelCaseWord(String simpleName) {
|
||||||
|
if (simpleName == null || simpleName.isEmpty()) return null;
|
||||||
|
// e.g. "ElectronicsOrderService" -> "Electronics"
|
||||||
|
String[] words = simpleName.split("(?<!^)(?=[A-Z])");
|
||||||
|
if (words.length > 0) {
|
||||||
|
return words[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeepSharedPackage(String pkg1, String pkg2) {
|
||||||
|
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return null;
|
||||||
|
String[] p1 = pkg1.split("\\.");
|
||||||
|
String[] p2 = pkg2.split("\\.");
|
||||||
|
|
||||||
|
// Find the deepest common segment index
|
||||||
|
int matchIdx = -1;
|
||||||
|
for (int i = 0; i < Math.min(p1.length, p2.length); i++) {
|
||||||
|
if (p1[i].equals(p2[i])) {
|
||||||
|
matchIdx = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If they share at least 3 segments (e.g. com.company.electronics), it's a deep match
|
||||||
|
if (matchIdx >= 2) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for(int i = 0; i <= matchIdx; i++) {
|
||||||
|
sb.append(p1[i]).append(".");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDeepPackageDivergence(String pkg1, String pkg2) {
|
||||||
|
if (pkg1 == null || pkg2 == null || pkg1.isEmpty() || pkg2.isEmpty()) return false;
|
||||||
|
String[] p1 = pkg1.split("\\.");
|
||||||
|
String[] p2 = pkg2.split("\\.");
|
||||||
|
|
||||||
|
// If they share exactly the root (com.company) but diverge at the 3rd segment (e.g. electronics vs computerstore)
|
||||||
|
// Then it's a divergence.
|
||||||
|
if (p1.length >= 3 && p2.length >= 3) {
|
||||||
|
return p1[0].equals(p2[0]) && p1[1].equals(p2[1]) && !p1[2].equals(p2[2]);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String simplify(String name) {
|
||||||
if (name == null) return null;
|
if (name == null) return null;
|
||||||
// Strip common suffixes
|
// Strip common suffixes
|
||||||
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
|
||||||
|
|||||||
@@ -296,7 +296,11 @@ public class CallGraphBuilder {
|
|||||||
if (!visited.add(fqn)) return Collections.emptyList();
|
if (!visited.add(fqn)) return Collections.emptyList();
|
||||||
|
|
||||||
List<String> constants = new ArrayList<>();
|
List<String> constants = new ArrayList<>();
|
||||||
TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className);
|
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||||
|
if (tempTd == null) {
|
||||||
|
tempTd = context.getTypeDeclaration(className);
|
||||||
|
}
|
||||||
|
final TypeDeclaration td = tempTd;
|
||||||
if (td == null) {
|
if (td == null) {
|
||||||
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
|
System.out.println("DEEP TRACE FAILED TO FIND TD: " + className);
|
||||||
}
|
}
|
||||||
@@ -815,57 +819,109 @@ public class CallGraphBuilder {
|
|||||||
|
|
||||||
private List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
private List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||||
List<String> results = new ArrayList<>();
|
List<String> results = new ArrayList<>();
|
||||||
for (MethodDeclaration md : td.getMethods()) {
|
|
||||||
if (md.isConstructor() && md.getBody() != null) {
|
// 1. Check Field Declarations
|
||||||
md.getBody().accept(new ASTVisitor() {
|
for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) {
|
||||||
@Override
|
for (Object fragObj : fd.fragments()) {
|
||||||
public boolean visit(Assignment node) {
|
org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj;
|
||||||
Expression left = node.getLeftHandSide();
|
if (frag.getName().getIdentifier().equals(fieldName) && frag.getInitializer() != null) {
|
||||||
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
|
Expression right = frag.getInitializer();
|
||||||
(left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(fieldName))) {
|
if (right instanceof MethodInvocation mi) {
|
||||||
String val = constantResolver.resolve(node.getRightHandSide(), context);
|
String calledName = mi.getName().getIdentifier();
|
||||||
if (val != null) results.add(val);
|
String targetClass = context.getFqn(td);
|
||||||
}
|
CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||||
return super.visit(node);
|
results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, new HashSet<>(visited), cu));
|
||||||
|
} else {
|
||||||
|
String val = constantResolver.resolve(right, context);
|
||||||
|
if (val != null) results.add(val);
|
||||||
}
|
}
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
org.eclipse.jdt.core.dom.ASTVisitor assignmentVisitor = new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression left = node.getLeftHandSide();
|
||||||
|
if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(fieldName)) ||
|
||||||
|
(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Check Constructors
|
||||||
|
for (MethodDeclaration md : td.getMethods()) {
|
||||||
|
if (md.isConstructor() && md.getBody() != null) {
|
||||||
|
md.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Initializers
|
||||||
|
for (Object bodyDecl : td.bodyDeclarations()) {
|
||||||
|
if (bodyDecl instanceof org.eclipse.jdt.core.dom.Initializer init && init.getBody() != null) {
|
||||||
|
init.getBody().accept(assignmentVisitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,14 +229,14 @@ class TransitionLinkerEnricherTest {
|
|||||||
CallChain chain = CallChain.builder()
|
CallChain chain = CallChain.builder()
|
||||||
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
.methodChain(List.of(
|
.methodChain(List.of(
|
||||||
"com.example.ElectronicsOrderController.post()",
|
"com.example.electronics.ElectronicsOrderController.post()",
|
||||||
"com.example.ElectronicsOrderService.processOrderEvent()"
|
"com.example.electronics.ElectronicsOrderService.processOrderEvent()"
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Testing against Electronics SM - should match
|
// Testing against Electronics SM - should match
|
||||||
AnalysisResult resultElectronics = AnalysisResult.builder()
|
AnalysisResult resultElectronics = AnalysisResult.builder()
|
||||||
.name("ElectronicsOwnDeliveryStateMachineConfiguration")
|
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||||
.transitions(List.of(t1))
|
.transitions(List.of(t1))
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
.build();
|
.build();
|
||||||
@@ -247,7 +247,7 @@ class TransitionLinkerEnricherTest {
|
|||||||
|
|
||||||
// Testing against ComputerStore SM - should NOT match because chain has Electronics service
|
// Testing against ComputerStore SM - should NOT match because chain has Electronics service
|
||||||
AnalysisResult resultComputer = AnalysisResult.builder()
|
AnalysisResult resultComputer = AnalysisResult.builder()
|
||||||
.name("OwnDeliveryStateMachineConfiguration") // the non-electronics one
|
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration") // the non-electronics one
|
||||||
.transitions(List.of(t1))
|
.transitions(List.of(t1))
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
.build();
|
.build();
|
||||||
@@ -275,7 +275,7 @@ class TransitionLinkerEnricherTest {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
AnalysisResult resultElectronics = AnalysisResult.builder()
|
AnalysisResult resultElectronics = AnalysisResult.builder()
|
||||||
.name("ElectronicsOwnDeliveryStateMachineConfiguration")
|
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||||
.transitions(List.of(t1))
|
.transitions(List.of(t1))
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
.build();
|
.build();
|
||||||
@@ -287,7 +287,7 @@ class TransitionLinkerEnricherTest {
|
|||||||
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
|
assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);
|
||||||
|
|
||||||
AnalysisResult resultComputer = AnalysisResult.builder()
|
AnalysisResult resultComputer = AnalysisResult.builder()
|
||||||
.name("OwnDeliveryStateMachineConfiguration")
|
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
|
||||||
.transitions(List.of(t1))
|
.transitions(List.of(t1))
|
||||||
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
.build();
|
.build();
|
||||||
@@ -299,4 +299,83 @@ class TransitionLinkerEnricherTest {
|
|||||||
assertThat(updatedChainComp.getMatchedTransitions()).hasSize(1);
|
assertThat(updatedChainComp.getMatchedTransitions()).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFilterCrossStateMachineRoutingWithMultipleVerticals() {
|
||||||
|
Transition t1 = new Transition();
|
||||||
|
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
|
||||||
|
t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
|
||||||
|
t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));
|
||||||
|
|
||||||
|
// Chain 1: Electronics
|
||||||
|
CallChain chainElec = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of("com.example.electronics.ElectronicsOrderController.post()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Chain 2: Furniture
|
||||||
|
CallChain chainFurn = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of("com.example.furniture.FurnitureOrderController.post()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Chain 3: Groceries
|
||||||
|
CallChain chainGroc = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
|
||||||
|
.methodChain(List.of("com.example.groceries.GroceriesOrderController.post()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 1: Electronics
|
||||||
|
AnalysisResult resElec = AnalysisResult.builder()
|
||||||
|
.name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 2: Furniture
|
||||||
|
AnalysisResult resFurn = AnalysisResult.builder()
|
||||||
|
.name("com.example.furniture.FurnitureOwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 3: Groceries
|
||||||
|
AnalysisResult resGroc = AnalysisResult.builder()
|
||||||
|
.name("com.example.groceries.GroceriesStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// SM 4: Computer Store (The catch-all base one in computerstore package)
|
||||||
|
AnalysisResult resComp = AnalysisResult.builder()
|
||||||
|
.name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
|
||||||
|
.transitions(List.of(t1))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Test Electronics SM - Should only keep Electronics Chain
|
||||||
|
enricher.enrich(resElec, null, null);
|
||||||
|
assertThat(resElec.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1); // Elec
|
||||||
|
assertThat(resElec.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||||
|
assertThat(resElec.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||||
|
|
||||||
|
// Test Furniture SM - Should only keep Furniture Chain
|
||||||
|
enricher.enrich(resFurn, null, null);
|
||||||
|
assertThat(resFurn.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||||
|
assertThat(resFurn.getMetadata().getCallChains().get(1).getMatchedTransitions()).hasSize(1); // Furn
|
||||||
|
assertThat(resFurn.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||||
|
|
||||||
|
// Test Groceries SM - Should only keep Groceries Chain
|
||||||
|
enricher.enrich(resGroc, null, null);
|
||||||
|
assertThat(resGroc.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||||
|
assertThat(resGroc.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||||
|
assertThat(resGroc.getMetadata().getCallChains().get(2).getMatchedTransitions()).hasSize(1); // Groc
|
||||||
|
|
||||||
|
// Test ComputerStore SM - Should reject ALL because none belong to computerstore
|
||||||
|
enricher.enrich(resComp, null, null);
|
||||||
|
assertThat(resComp.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty(); // Elec
|
||||||
|
assertThat(resComp.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
|
||||||
|
assertThat(resComp.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1723,4 +1723,137 @@ class CallGraphBuilderTest {
|
|||||||
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromDynamicMethodAssignmentInConstructor() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new OrderCancelledEvent().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderCancelledEvent {
|
||||||
|
private String cancelEventType;
|
||||||
|
public OrderCancelledEvent() {
|
||||||
|
this.cancelEventType = assertMatchingCancelEventType("some_sender");
|
||||||
|
}
|
||||||
|
private String assertMatchingCancelEventType(String sender) {
|
||||||
|
return "CANCELLED_BY_SENDER";
|
||||||
|
}
|
||||||
|
public String getType() { return cancelEventType; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_dynamic_rhs");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
CallGraphBuilder builder = new CallGraphBuilder(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_BY_SENDER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromAbstractSuperclass() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus() {
|
||||||
|
service.process(new PostAcceptanceOrderModificationsRejectedEvent().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AbstractPostAcceptanceOrderModificationsEvent {
|
||||||
|
private String event;
|
||||||
|
public AbstractPostAcceptanceOrderModificationsEvent(String event) {
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
public String getType() { return event; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostAcceptanceOrderModificationsRejectedEvent extends AbstractPostAcceptanceOrderModificationsEvent {
|
||||||
|
public PostAcceptanceOrderModificationsRejectedEvent() {
|
||||||
|
super("POST_ACCEPTANCE_REJECTED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_post_acc");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
CallGraphBuilder builder = new CallGraphBuilder(context);
|
||||||
|
|
||||||
|
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("POST_ACCEPTANCE_REJECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveConstantsFromFieldDeclarationsAndInitializers() throws IOException {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void handleStatus1() {
|
||||||
|
service.process(new EventWithFieldDecl().getType());
|
||||||
|
}
|
||||||
|
public void handleStatus2() {
|
||||||
|
service.process(new EventWithInitBlock().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithFieldDecl {
|
||||||
|
private String event = "FIELD_DECL";
|
||||||
|
public String getType() { return event; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventWithInitBlock {
|
||||||
|
private String event;
|
||||||
|
{
|
||||||
|
event = "INIT_BLOCK";
|
||||||
|
}
|
||||||
|
public String getType() { return event; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
public void process(String event) {}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Path tempDir = Files.createTempDirectory("callgraph_field_init");
|
||||||
|
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
CallGraphBuilder builder = new CallGraphBuilder(context);
|
||||||
|
|
||||||
|
EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
|
||||||
|
EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
|
||||||
|
List<CallChain> chains = builder.findChains(List.of(ep1, ep2), List.of(trigger));
|
||||||
|
|
||||||
|
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
|
||||||
|
|
||||||
|
java.util.List<String> allEvents = new java.util.ArrayList<>();
|
||||||
|
chains.forEach(c -> allEvents.addAll(c.getTriggerPoint().getPolymorphicEvents()));
|
||||||
|
org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user