forward analysis

This commit is contained in:
2026-06-22 18:38:19 +02:00
parent 1b690582d6
commit 14b311be2b
7 changed files with 316 additions and 6 deletions

View File

@@ -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<CallChain> chains = result.getMetadata().getCallChains();
List<CallChain> 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<String> estimatedValues = estimator.estimatePossibleValues(chain, context);
if (estimatedValues != null && !estimatedValues.isEmpty()) {
log.info("Symbolic Path Analysis estimated values for {} -> {}", chain.getTriggerPoint().getEvent(), estimatedValues);
List<String> 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);
}
}

View File

@@ -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<String> estimatePossibleValues(CallChain chain, CodebaseContext context);
}

View File

@@ -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<String> estimatePossibleValues(CallChain chain, CodebaseContext context) {
if (chain.getMethodChain() == null || chain.getMethodChain().isEmpty()) {
return null;
}
Set<String> 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<String> 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;
}
}

View File

@@ -8,7 +8,7 @@ import lombok.extern.jackson.Jacksonized;
import java.util.Map; import java.util.Map;
@Data @Data
@Builder @Builder(toBuilder = true)
@Jacksonized @Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class TriggerPoint { public class TriggerPoint {

View File

@@ -108,7 +108,11 @@ public class ConstantResolver {
TypeDeclaration td = findEnclosingType(mi); TypeDeclaration td = findEnclosingType(mi);
if (td != null) { if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true); MethodDeclaration md = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
if (md != null && md.getReturnType2() != null) { if (md != null) {
String evaluated = evaluateMethodOutput(mi, md, context, visited);
if (evaluated != null) return evaluated;
if (md.getReturnType2() != null) {
java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString()); java.util.List<String> values = context.getEnumValues(md.getReturnType2().toString());
if (values != null && !values.isEmpty()) { if (values != null && !values.isEmpty()) {
return "ENUM_SET:" + String.join(",", values); return "ENUM_SET:" + String.join(",", values);
@@ -117,10 +121,117 @@ public class ConstantResolver {
} }
} }
} }
}
return null; return null;
} }
private String evaluateMethodOutput(MethodInvocation mi, MethodDeclaration md, CodebaseContext context, Set<String> visited) {
if (md.getBody() == null || mi.arguments().size() != md.parameters().size()) return null;
java.util.Map<String, String> 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<String, String> paramValues, CodebaseContext context, Set<String> 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<String, String> paramValues, CodebaseContext context, Set<String> 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<String, String> 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<String> visited) { private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null; if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;

View File

@@ -1147,7 +1147,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
if (lowerName.startsWith("map") || lowerName.startsWith("parse") || if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
lowerName.startsWith("convert") || lowerName.startsWith("to") || lowerName.startsWith("convert") || lowerName.startsWith("to") ||
lowerName.startsWith("resolve") || lowerName.startsWith("build") || 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; return mi;
} }

View File

@@ -45,6 +45,7 @@ public class ExportService {
new EntryPointEnricher(), new EntryPointEnricher(),
new PropertyEnricher(), new PropertyEnricher(),
new CallChainEnricher(), new CallChainEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.path.ForwardPathEstimationEnricher(),
new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher() new click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher()
))); )));
} }