forward analysis
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String> 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<String> 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<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) {
|
||||
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user