From f525f3d3ced450ed052049e97c5f35d379dfd873 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sat, 27 Jun 2026 14:17:54 +0200 Subject: [PATCH] more fixes --- .../HeuristicBeanResolutionEngine.java | 15 +++ .../service/AbstractCallGraphEngine.java | 50 ++++++- .../ast/common/CodebaseContext.java | 4 + .../analysis/service/EnterpriseBugsTest.java | 123 ++++++++++++++++++ 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java index 9aeab76..eec5c15 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java @@ -40,6 +40,21 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { boolean hasPositiveMatch = false; boolean hasStrongMismatch = false; + if (chain.getTriggerPoint() != null && chain.getTriggerPoint().getClassName() != null) { + String chainClass = chain.getTriggerPoint().getClassName(); + String chainPackage = getPackageName(chainClass); + String chainSimple = getSimpleClassName(chainClass); + String chainPrefix = getFirstCamelCaseWord(chainSimple); + + if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) { + hasPositiveMatch = true; + } + + if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) { + hasPositiveMatch = true; + } + } + for (String method : chain.getMethodChain()) { String chainClass = getClassNameOnly(method); String chainPackage = getPackageName(chainClass); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index 3c728e4..c64442a 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -551,6 +551,17 @@ import java.util.*; polymorphicEvents.removeIf(e -> { if (e.contains(".")) return false; // If it's a fully qualified enum/constant, keep it String val = e; + boolean isKnownEnumVal = false; + for (java.util.List vals : context.getEnumValuesMap().values()) { + for (String v : vals) { + if (v.endsWith("." + val)) { + isKnownEnumVal = true; + break; + } + } + if (isKnownEnumVal) break; + } + if (isKnownEnumVal) return false; return !val.equals(val.toUpperCase()) || val.length() <= 1; }); @@ -963,6 +974,18 @@ import java.util.*; } protected List resolveClassConstantReturns(String className, click.kamil.springstatemachineexporter.ast.common.CodebaseContext context, CompilationUnit contextCu) { + if (className != null && className.contains(".")) { + String prefix = className.substring(0, className.lastIndexOf('.')); + String suffix = className.substring(className.lastIndexOf('.') + 1); + TypeDeclaration prefixTd = contextCu != null ? context.getTypeDeclaration(prefix, contextCu) : context.getTypeDeclaration(prefix); + if (prefixTd == null) prefixTd = context.getTypeDeclaration(prefix); + boolean isKnownType = prefixTd != null || context.getEnumValues(prefix) != null; + if (isKnownType) { + String simplePrefix = prefix.contains(".") ? prefix.substring(prefix.lastIndexOf('.') + 1) : prefix; + return java.util.Collections.singletonList(simplePrefix + "." + suffix); + } + } + TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(className, contextCu) : context.getTypeDeclaration(className); if (td == null) td = context.getTypeDeclaration(className); if (td == null) return null; @@ -1126,7 +1149,16 @@ import java.util.*; // then evaluates the getter body (handles switch-based enum transformations). java.util.List narrowed = resolveInlineInstantiationGetterResult( cic, methodName, context, new java.util.HashSet<>()); - if (!narrowed.isEmpty()) { + if (narrowed.isEmpty()) { + String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()); + CompilationUnit contextCu = expr.getRoot() instanceof CompilationUnit ? (CompilationUnit) expr.getRoot() : null; + TypeDeclaration typeDecl = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName); + if (typeDecl == null) typeDecl = context.getTypeDeclaration(typeName); + if (typeDecl != null) { + narrowed = resolveMethodReturnConstant(context.getFqn(typeDecl), methodName, 0, new java.util.HashSet<>(), contextCu); + } + } + if (narrowed != null && !narrowed.isEmpty()) { constants.addAll(narrowed); handledByLombok = true; // reuse flag to skip fallback below } @@ -1480,7 +1512,9 @@ import java.util.*; String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils .extractSimpleTypeName(cic.getType()); - org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(typeName); + org.eclipse.jdt.core.dom.CompilationUnit contextCu = cic.getRoot() instanceof org.eclipse.jdt.core.dom.CompilationUnit ? (org.eclipse.jdt.core.dom.CompilationUnit) cic.getRoot() : null; + org.eclipse.jdt.core.dom.TypeDeclaration td = contextCu != null ? context.getTypeDeclaration(typeName, contextCu) : context.getTypeDeclaration(typeName); + if (td == null) td = context.getTypeDeclaration(typeName); if (td == null) return java.util.Collections.emptyList(); org.eclipse.jdt.core.dom.MethodDeclaration getter = @@ -2258,6 +2292,12 @@ import java.util.*; String calledName = mi.getName().getIdentifier(); String targetClass = context.getFqn(td); CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + if (mi.getExpression() == null) { + TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu); + if (targetTd != null) { + targetClass = context.getFqn(targetTd); + } + } results.addAll(resolveMethodReturnConstant(targetClass, calledName, 0, visited, cu)); } else { String val = constantResolver.resolve(tracedRight, context); @@ -2291,6 +2331,12 @@ import java.util.*; if (targetTd != null) { targetClass = context.getFqn(targetTd); } + } else if (mi.getExpression() == null) { + CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; + TypeDeclaration targetTd = context.resolveStaticImport(calledName, cu); + if (targetTd != null) { + targetClass = context.getFqn(targetTd); + } } CompilationUnit cu = td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index f2b07a5..d892e1d 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -328,6 +328,10 @@ public class CodebaseContext { } } + public Map> getEnumValuesMap() { + return enumValues; + } + public List getEnumValues(String fqnOrSimpleName) { if (fqnOrSimpleName == null) return null; List values = enumValues.get(fqnOrSimpleName); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java new file mode 100644 index 0000000..e90caa1 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/EnterpriseBugsTest.java @@ -0,0 +1,123 @@ +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.analysis.enricher.routing.HeuristicBeanResolutionEngine; +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 EnterpriseBugsTest { + + @Test + void shouldResolveStaticImportAndAvoidMismatchedConstructorEnumArgs(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + import static com.example.EventUtils.assertMatchingCancelEventType; + + public class OrderController { + private OrderService service; + public void handleCancel(Cancellation.Sender sender) { + service.process(new OrderCancelledEvent(sender).getType()); + } + } + + class OrderService { + public void process(CommonOrderEvent event) {} + } + + class OrderCancelledEvent { + private CommonOrderEvent type; + public OrderCancelledEvent(Cancellation.Sender sender) { + this.type = assertMatchingCancelEventType(sender); + } + public CommonOrderEvent getType() { return type; } + } + + class EventUtils { + public static CommonOrderEvent assertMatchingCancelEventType(Cancellation.Sender sender) { + return switch(sender) { + case registry_corp -> CommonOrderEvent.CANCELLED_BY_registry_corp; + default -> null; + }; + } + } + + class Cancellation { + public enum Sender { registry_corp } + } + + enum CommonOrderEvent { CANCELLED_BY_registry_corp } + """; + + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("handleCancel") + .build(); + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("process") + .event("event") + .build(); + + List chains = engine.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + TriggerPoint resolvedTp = chains.get(0).getTriggerPoint(); + + // Assert Bug 3: assertMatchingCancelEventType method is resolved via static import + // Assert Bug 2: Cancellation.Sender.registry_corp is NOT extracted as an event argument + assertThat(resolvedTp.getPolymorphicEvents()) + .as("Polymorphic events found: %s", resolvedTp.getPolymorphicEvents()) + .contains("CommonOrderEvent.CANCELLED_BY_registry_corp") + .doesNotContain("registry_corp", "Cancellation.Sender.registry_corp"); + } + + @Test + void shouldAcceptConnectorCallChainUsingTriggerPointDomainMatch() { + HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine(); + + CallChain chain = CallChain.builder() + .triggerPoint(TriggerPoint.builder() + .className("project.shop.OrderService") + .build()) + .methodChain(List.of("project.enterprise.connector.org.OrgOrderControllerImpl.accept")) + .build(); + + // Assert Bug 4: Connector entry points targeting shop SM trigger point are accepted + boolean isMatched = engine.isRoutedToCorrectMachine(chain, "project.shop.OrderStateMachine"); + assertThat(isMatched).isTrue(); + } + + @Test + void shouldResolveClassConstantToSimpleNameWithoutFQNDuplicates(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public enum CommonOrderEvent { ACCEPTED_BY_corp } + """; + Files.writeString(tempDir.resolve("CommonOrderEvent.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + List resolved = engine.resolveClassConstantReturns("com.example.CommonOrderEvent.ACCEPTED_BY_corp", context, null); + + // Assert Bug 6: resolves cleanly to simple name (since prefix is a known type) + assertThat(resolved).containsExactly("CommonOrderEvent.ACCEPTED_BY_corp"); + } +}