From 14b311be2be76eecd45a5e8cc0c6d606b1c6bc8e Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Mon, 22 Jun 2026 18:38:19 +0200 Subject: [PATCH] forward analysis --- .../path/ForwardPathEstimationEnricher.java | 69 ++++++++++ .../enricher/path/PathValueEstimator.java | 10 ++ .../path/SymbolicPathValueEstimator.java | 114 +++++++++++++++++ .../analysis/model/TriggerPoint.java | 2 +- .../analysis/resolver/ConstantResolver.java | 119 +++++++++++++++++- .../service/AbstractCallGraphEngine.java | 7 +- .../service/ExportService.java | 1 + 7 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/ForwardPathEstimationEnricher.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/PathValueEstimator.java create mode 100644 state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/ForwardPathEstimationEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/ForwardPathEstimationEnricher.java new file mode 100644 index 0000000..5a5ea61 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/ForwardPathEstimationEnricher.java @@ -0,0 +1,69 @@ +package click.kamil.springstatemachineexporter.analysis.enricher.path; + +import click.kamil.springstatemachineexporter.analysis.enricher.AnalysisEnricher; +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +public class ForwardPathEstimationEnricher implements AnalysisEnricher { + + private final boolean isEnabled; + private final PathValueEstimator estimator; + + public ForwardPathEstimationEnricher() { + this.isEnabled = Boolean.parseBoolean(System.getProperty("analyzer.forward-path-estimation.enabled", "false")); + this.estimator = new SymbolicPathValueEstimator(); + } + + @Override + public void enrich(AnalysisResult result, CodebaseContext context, CodebaseIntelligenceProvider intelligence) { + if (!isEnabled || result.getMetadata() == null || result.getMetadata().getCallChains() == null) { + return; + } + + log.info("Running Symbolic Forward Path Analysis for {}", result.getName()); + + List chains = result.getMetadata().getCallChains(); + List updatedChains = chains.stream().map(chain -> { + if (chain.getTriggerPoint() == null || chain.getTriggerPoint().getEvent() == null) return chain; + + // If the event looks like a dynamic variable (no dots, not an enum format) + if (!chain.getTriggerPoint().getEvent().contains(".")) { + List estimatedValues = estimator.estimatePossibleValues(chain, context); + if (estimatedValues != null && !estimatedValues.isEmpty()) { + log.info("Symbolic Path Analysis estimated values for {} -> {}", chain.getTriggerPoint().getEvent(), estimatedValues); + + List polyEvents = new ArrayList<>(); + if (chain.getTriggerPoint().getPolymorphicEvents() != null) { + polyEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents()); + } + polyEvents.addAll(estimatedValues); + + TriggerPoint newTp = chain.getTriggerPoint().toBuilder() + .polymorphicEvents(polyEvents) + .build(); + + return chain.toBuilder().triggerPoint(newTp).build(); + } + } + return chain; + }).collect(Collectors.toList()); + click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata updatedMetadata = + click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder() + .triggers(result.getMetadata().getTriggers()) + .entryPoints(result.getMetadata().getEntryPoints()) + .callChains(updatedChains) + .properties(result.getMetadata().getProperties()) + .build(); + + result.setMetadata(updatedMetadata); + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/PathValueEstimator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/PathValueEstimator.java new file mode 100644 index 0000000..1f5f817 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/PathValueEstimator.java @@ -0,0 +1,10 @@ +package click.kamil.springstatemachineexporter.analysis.enricher.path; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; + +import java.util.List; + +public interface PathValueEstimator { + List estimatePossibleValues(CallChain chain, CodebaseContext context); +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java new file mode 100644 index 0000000..b9d75a5 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/path/SymbolicPathValueEstimator.java @@ -0,0 +1,114 @@ +package click.kamil.springstatemachineexporter.analysis.enricher.path; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.eclipse.jdt.core.dom.*; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SymbolicPathValueEstimator implements PathValueEstimator { + + @Override + public List estimatePossibleValues(CallChain chain, CodebaseContext context) { + if (chain.getMethodChain() == null || chain.getMethodChain().isEmpty()) { + return null; + } + + Set collectedConstants = new HashSet<>(); + + for (String methodFqn : chain.getMethodChain()) { + int lastDot = methodFqn.lastIndexOf('.'); + if (lastDot == -1) continue; + + String className = methodFqn.substring(0, lastDot); + String methodName = methodFqn.substring(lastDot + 1); + + TypeDeclaration td = context.getTypeDeclaration(className); + if (td == null) continue; + + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md == null || md.getBody() == null) continue; + + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(MethodInvocation node) { + if ((node.getName().getIdentifier().equals("equals") || node.getName().getIdentifier().equals("equalsIgnoreCase")) + && node.arguments().size() == 1) { + + Expression caller = node.getExpression(); + Expression arg = (Expression) node.arguments().get(0); + + click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver(); + + String callerVal = resolver.resolve(caller, context); + if (callerVal != null && !callerVal.isEmpty()) { + addValue(collectedConstants, callerVal); + } + + String argVal = resolver.resolve(arg, context); + if (argVal != null && !argVal.isEmpty()) { + addValue(collectedConstants, argVal); + } + } + return super.visit(node); + } + + @Override + public boolean visit(SwitchStatement node) { + for (Object stmt : node.statements()) { + if (stmt instanceof SwitchCase sc && !sc.isDefault()) { + for (Object expr : sc.expressions()) { + click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver(); + String val = resolver.resolve((Expression) expr, context); + if (val != null && !val.isEmpty()) { + addValue(collectedConstants, val); + } + } + } + } + return super.visit(node); + } + + @Override + public boolean visit(SwitchExpression node) { + for (Object stmt : node.statements()) { + if (stmt instanceof SwitchCase sc && !sc.isDefault()) { + for (Object expr : sc.expressions()) { + click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver resolver = new click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver(); + String val = resolver.resolve((Expression) expr, context); + if (val != null && !val.isEmpty()) { + addValue(collectedConstants, val); + } + } + } + } + return super.visit(node); + } + }); + } + + if (collectedConstants.isEmpty()) return null; + return new ArrayList<>(collectedConstants); + } + + private void addValue(Set collectedConstants, String val) { + if (val.startsWith("ENUM_SET:")) { + for (String eVal : val.substring(9).split(",")) { + String parsed = parseEnumSetElement(eVal); + if (!collectedConstants.contains(parsed)) collectedConstants.add(parsed); + } + } else { + collectedConstants.add(val); + } + } + + private String parseEnumSetElement(String element) { + if (element.contains(".")) { + return element.substring(element.lastIndexOf('.') + 1); + } + return element; + } +} diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java index ea2ae00..c6ec1b9 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/model/TriggerPoint.java @@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized; import java.util.Map; @Data -@Builder +@Builder(toBuilder = true) @Jacksonized @JsonIgnoreProperties(ignoreUnknown = true) public class TriggerPoint { 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 43d8bed..b86e233 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 @@ -108,10 +108,15 @@ public class ConstantResolver { TypeDeclaration td = findEnclosingType(mi); if (td != null) { MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true); - if (md != null && md.getReturnType2() != null) { - java.util.List values = context.getEnumValues(md.getReturnType2().toString()); - if (values != null && !values.isEmpty()) { - return "ENUM_SET:" + String.join(",", values); + if (md != null) { + String evaluated = evaluateMethodOutput(mi, md, context, visited); + if (evaluated != null) return evaluated; + + if (md.getReturnType2() != null) { + java.util.List values = context.getEnumValues(md.getReturnType2().toString()); + if (values != null && !values.isEmpty()) { + return "ENUM_SET:" + String.join(",", values); + } } } } @@ -121,6 +126,112 @@ public class ConstantResolver { return null; } + private String evaluateMethodOutput(MethodInvocation mi, MethodDeclaration md, CodebaseContext context, Set visited) { + if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null; + + java.util.Map paramValues = new java.util.HashMap<>(); + for (int i = 0; i < mi.arguments().size(); i++) { + Expression arg = (Expression) mi.arguments().get(i); + String val = resolveInternal(arg, context, visited); + if (val != null) { + org.eclipse.jdt.core.dom.SingleVariableDeclaration param = (org.eclipse.jdt.core.dom.SingleVariableDeclaration) md.parameters().get(i); + paramValues.put(param.getName().getIdentifier(), val); + } + } + + if (paramValues.isEmpty()) return null; + + for (Object stmtObj : md.getBody().statements()) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchStatement ss) { + String result = evaluateSwitchStatement(ss, paramValues, context, visited); + if (result != null) return result; + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) { + if (rs.getExpression() instanceof org.eclipse.jdt.core.dom.SwitchExpression se) { + String result = evaluateSwitchExpression(se, paramValues, context, visited); + if (result != null) return result; + } + } + } + return null; + } + + private String evaluateSwitchStatement(org.eclipse.jdt.core.dom.SwitchStatement ss, java.util.Map paramValues, CodebaseContext context, Set visited) { + String switchVar = resolveExpressionWithParams(ss.getExpression(), paramValues); + if (switchVar == null) return null; + + boolean matchingCase = false; + for (Object stmtObj : ss.statements()) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) { + matchingCase = false; + if (sc.isDefault()) { + matchingCase = true; + } else { + for (Object exprObj : sc.expressions()) { + String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues); + if (caseVal != null && switchVar.endsWith(caseVal)) { + matchingCase = true; + break; + } + } + } + } else if (matchingCase) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) { + return resolveInternal(rs.getExpression(), context, visited); + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) { + return resolveInternal(ys.getExpression(), context, visited); + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) { + return resolveInternal(es.getExpression(), context, visited); + } + } + } + return null; + } + + private String evaluateSwitchExpression(org.eclipse.jdt.core.dom.SwitchExpression se, java.util.Map paramValues, CodebaseContext context, Set visited) { + String switchVar = resolveExpressionWithParams(se.getExpression(), paramValues); + if (switchVar == null) return null; + + boolean matchingCase = false; + for (Object stmtObj : se.statements()) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.SwitchCase sc) { + matchingCase = false; + if (sc.isDefault()) { + matchingCase = true; + } else { + for (Object exprObj : sc.expressions()) { + String caseVal = resolveExpressionWithParams((Expression) exprObj, paramValues); + if (caseVal != null && switchVar.endsWith(caseVal)) { + matchingCase = true; + break; + } + } + } + } else if (matchingCase) { + if (stmtObj instanceof org.eclipse.jdt.core.dom.ExpressionStatement es) { + return resolveInternal(es.getExpression(), context, visited); + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.YieldStatement ys) { + return resolveInternal(ys.getExpression(), context, visited); + } else if (stmtObj instanceof org.eclipse.jdt.core.dom.ReturnStatement rs) { + return resolveInternal(rs.getExpression(), context, visited); + } + } + } + return null; + } + + private String resolveExpressionWithParams(Expression expr, java.util.Map paramValues) { + if (expr instanceof SimpleName sn) { + String name = sn.getIdentifier(); + if (paramValues.containsKey(name)) return paramValues.get(name); + return name; + } else if (expr instanceof QualifiedName qn) { + return qn.getName().getIdentifier(); + } else if (expr instanceof org.eclipse.jdt.core.dom.StringLiteral sl) { + return sl.getLiteralValue(); + } + return null; + } + private String resolveInfix(InfixExpression expr, CodebaseContext context, Set visited) { if (expr.getOperator() != InfixExpression.Operator.PLUS) return null; 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 357eedd..d26f4ec 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 @@ -1147,7 +1147,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { if (lowerName.startsWith("map") || lowerName.startsWith("parse") || lowerName.startsWith("convert") || lowerName.startsWith("to") || lowerName.startsWith("resolve") || lowerName.startsWith("build") || - lowerName.startsWith("create") || lowerName.startsWith("from")) { + lowerName.startsWith("create") || lowerName.startsWith("from") || + lowerName.startsWith("transform") || lowerName.startsWith("translate") || + lowerName.startsWith("derive") || lowerName.startsWith("determine") || + lowerName.startsWith("calculate") || lowerName.startsWith("decode") || + lowerName.startsWith("extract") || lowerName.startsWith("get") || + lowerName.startsWith("find")) { return mi; } diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java index a1b65a4..0225dcc 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/service/ExportService.java @@ -45,6 +45,7 @@ public class ExportService { new EntryPointEnricher(), new PropertyEnricher(), new CallChainEnricher(), + new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(), new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher() ))); }