Perf: use CFG dataflow for traceLocalSetter and guard hot-path logging.
Replace per-call method-body AST scans with reaching-definitions lookup, guard remaining debug/trace calls in call-graph hot paths, and add branch edge-case tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -194,6 +194,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
protected TriggerPoint resolveTriggerPointParametersOriginal(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph, String[] finalParamNameRef) {
|
protected TriggerPoint resolveTriggerPointParametersOriginal(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph, String[] finalParamNameRef) {
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(path);
|
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(path);
|
||||||
|
final boolean debug = log.isDebugEnabled();
|
||||||
try {
|
try {
|
||||||
String event = tp.getEvent();
|
String event = tp.getEvent();
|
||||||
event = unwrapConverterMethods(event);
|
event = unwrapConverterMethods(event);
|
||||||
@@ -348,14 +349,18 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
|
if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
|
||||||
|
if (debug) {
|
||||||
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
||||||
|
}
|
||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
if (expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
||||||
if (!setterEvents.isEmpty()) {
|
if (!setterEvents.isEmpty()) {
|
||||||
|
if (debug) {
|
||||||
log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
|
log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
|
||||||
|
}
|
||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +371,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
||||||
if (!setterEvents.isEmpty()) {
|
if (!setterEvents.isEmpty()) {
|
||||||
|
if (debug) {
|
||||||
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
||||||
|
}
|
||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,7 +385,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (hasGetterOnReceiver) {
|
if (hasGetterOnReceiver) {
|
||||||
List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
||||||
if (getterEvents != null && !getterEvents.isEmpty()) {
|
if (getterEvents != null && !getterEvents.isEmpty()) {
|
||||||
|
if (debug) {
|
||||||
log.debug("Early return 2: getterEvents = {}", getterEvents);
|
log.debug("Early return 2: getterEvents = {}", getterEvents);
|
||||||
|
}
|
||||||
return TriggerPoint.builder()
|
return TriggerPoint.builder()
|
||||||
.event(resolvedValue)
|
.event(resolvedValue)
|
||||||
.className(tp.getClassName())
|
.className(tp.getClassName())
|
||||||
@@ -408,7 +417,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
log.debug("resolvedValue before ENUM_SET check: {} for path: {}", resolvedValue, path);
|
log.debug("resolvedValue before ENUM_SET check: {} for path: {}", resolvedValue, path);
|
||||||
|
}
|
||||||
List<String> polymorphicEvents = new ArrayList<>();
|
List<String> polymorphicEvents = new ArrayList<>();
|
||||||
|
|
||||||
if (resolvedValue.startsWith("ENUM_SET:")) {
|
if (resolvedValue.startsWith("ENUM_SET:")) {
|
||||||
@@ -869,7 +880,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
log.debug("declaredType before getEnumValues: {} for exprNode: {} in {}", declaredType, exprNode, entryMethod);
|
log.debug("declaredType before getEnumValues: {} for exprNode: {} in {}", declaredType, exprNode, entryMethod);
|
||||||
|
}
|
||||||
if (declaredType != null) {
|
if (declaredType != null) {
|
||||||
List<String> enumValues = context.getEnumValues(declaredType);
|
List<String> enumValues = context.getEnumValues(declaredType);
|
||||||
if (enumValues != null && !enumValues.isEmpty()) {
|
if (enumValues != null && !enumValues.isEmpty()) {
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ public class ConstantExtractor {
|
|||||||
} else if (expr instanceof MethodInvocation mi) {
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
String methodName = mi.getName().getIdentifier();
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
log.trace("Processing mi: {}", mi);
|
log.trace("Processing mi: {}", mi);
|
||||||
|
}
|
||||||
if (("get".equals(methodName) || "getOrDefault".equals(methodName)) && mi.getExpression() != null) {
|
if (("get".equals(methodName) || "getOrDefault".equals(methodName)) && mi.getExpression() != null) {
|
||||||
if (isKeyedLookupMethod(mi)) {
|
if (isKeyedLookupMethod(mi)) {
|
||||||
extractMapReceiverConstants(mi.getExpression(), constants);
|
extractMapReceiverConstants(mi.getExpression(), constants);
|
||||||
|
|||||||
@@ -595,11 +595,15 @@ public class ConstructorAnalyzer {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor);
|
Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
|
log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
|
||||||
|
}
|
||||||
if (fieldValues.isEmpty()) return Collections.emptyList();
|
if (fieldValues.isEmpty()) return Collections.emptyList();
|
||||||
|
|
||||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("RESOLVE_GETTER: result={}", result);
|
log.debug("RESOLVE_GETTER: result={}", result);
|
||||||
|
}
|
||||||
return result != null ? List.of(result) : Collections.emptyList();
|
return result != null ? List.of(result) : Collections.emptyList();
|
||||||
} finally {
|
} finally {
|
||||||
visited.remove(getterKey);
|
visited.remove(getterKey);
|
||||||
|
|||||||
@@ -200,18 +200,26 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
nameBinding = fa.resolveFieldBinding();
|
nameBinding = fa.resolveFieldBinding();
|
||||||
}
|
}
|
||||||
if (nameBinding instanceof IVariableBinding varBinding) {
|
if (nameBinding instanceof IVariableBinding varBinding) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
|
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
|
||||||
|
}
|
||||||
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||||
if (concreteFqn != null) {
|
if (concreteFqn != null) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
||||||
|
}
|
||||||
return concreteFqn + "." + methodName;
|
return concreteFqn + "." + methodName;
|
||||||
} else {
|
} else {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
|
|||||||
@@ -64,16 +64,18 @@ public class VariableTracer {
|
|||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
if (td != null) {
|
if (td == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
if (md != null && md.getBody() != null) {
|
if (md == null || md.getBody() == null) {
|
||||||
final Expression[] setterArg = new Expression[1];
|
return null;
|
||||||
String propName;
|
}
|
||||||
|
|
||||||
String setterMethodName;
|
String setterMethodName;
|
||||||
String indexedFieldName;
|
String indexedFieldName;
|
||||||
|
|
||||||
if (isBeanStyleAccessorName(getterName)) {
|
if (isBeanStyleAccessorName(getterName)) {
|
||||||
propName = propertyNameFromAccessor(getterName);
|
String propName = propertyNameFromAccessor(getterName);
|
||||||
setterMethodName = "set" + capitalizeProperty(propName);
|
setterMethodName = "set" + capitalizeProperty(propName);
|
||||||
indexedFieldName = propName;
|
indexedFieldName = propName;
|
||||||
|
|
||||||
@@ -96,47 +98,14 @@ public class VariableTracer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||||
setterMethodName = "set" + propName;
|
setterMethodName = "set" + propName;
|
||||||
indexedFieldName = propName;
|
indexedFieldName = propName;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String resolvedSetterName = setterMethodName;
|
if (dataFlowModel instanceof JdtDataFlowModel jdtDataFlowModel) {
|
||||||
final String resolvedFieldName = indexedFieldName;
|
return jdtDataFlowModel.findLocalSetterArgument(
|
||||||
md.getBody().accept(new ASTVisitor() {
|
md, varName, getterName, setterMethodName, indexedFieldName);
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (node.getName().getIdentifier().equals(resolvedSetterName)
|
|
||||||
|| node.getName().getIdentifier().equalsIgnoreCase("set" + resolvedFieldName)
|
|
||||||
|| node.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
|
||||||
if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
|
|
||||||
if (!node.arguments().isEmpty()) {
|
|
||||||
setterArg[0] = (Expression) node.arguments().get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean visit(Assignment node) {
|
|
||||||
if (node.getLeftHandSide() instanceof FieldAccess fa) {
|
|
||||||
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
|
||||||
if (fa.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
|
||||||
setterArg[0] = node.getRightHandSide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
|
|
||||||
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
|
||||||
if (qqn.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
|
||||||
setterArg[0] = node.getRightHandSide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return setterArg[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1495,6 +1495,272 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return returns;
|
return returns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Expression findLocalSetterArgument(
|
||||||
|
MethodDeclaration md,
|
||||||
|
String varName,
|
||||||
|
String getterName,
|
||||||
|
String resolvedSetterName,
|
||||||
|
String resolvedFieldName) {
|
||||||
|
if (md == null || md.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlFlowGraph cfg = cfgCache.computeIfAbsent(md, CfgBuilder::build);
|
||||||
|
MethodInvocation getterUse = findGetterUseOnVariable(cfg, varName, getterName);
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> nodesToScan;
|
||||||
|
ControlFlowGraph.CfgNode getterCfgNode = null;
|
||||||
|
if (getterUse != null && getterUse.getExpression() instanceof SimpleName receiver) {
|
||||||
|
getterCfgNode = findCfgNode(cfg, getterUse);
|
||||||
|
nodesToScan = collectCfgNodesBetweenDefAndUse(md, receiver, getterUse, cfg);
|
||||||
|
} else {
|
||||||
|
nodesToScan = orderedStatementNodes(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression lastSetterArg = null;
|
||||||
|
for (ControlFlowGraph.CfgNode node : nodesToScan) {
|
||||||
|
if (getterCfgNode != null && node == getterCfgNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Expression arg = extractSetterArgument(node.getAstNode(), varName, resolvedSetterName, resolvedFieldName);
|
||||||
|
if (arg != null) {
|
||||||
|
lastSetterArg = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastSetterArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ControlFlowGraph.CfgNode> orderedStatementNodes(ControlFlowGraph cfg) {
|
||||||
|
List<ControlFlowGraph.CfgNode> result = new ArrayList<>();
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||||
|
if (node.getType() == ControlFlowGraph.CfgNodeType.STATEMENT && node.getAstNode() != null) {
|
||||||
|
result.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sort(Comparator.comparingInt(cfg.getNodes()::indexOf));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodInvocation findGetterUseOnVariable(ControlFlowGraph cfg, String varName, String getterName) {
|
||||||
|
if (getterName == null || getterName.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MethodInvocation lastMatch = null;
|
||||||
|
for (ControlFlowGraph.CfgNode node : orderedStatementNodes(cfg)) {
|
||||||
|
MethodInvocation match = findGetterInvocationInNode(node.getAstNode(), varName, getterName);
|
||||||
|
if (match != null) {
|
||||||
|
lastMatch = match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodInvocation findGetterInvocationInNode(ASTNode node, String varName, String getterName) {
|
||||||
|
Expression expr = unwrapCfgStatementExpression(node);
|
||||||
|
if (expr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final MethodInvocation[] found = new MethodInvocation[1];
|
||||||
|
if (expr instanceof MethodInvocation mi && matchesGetterInvocation(mi, varName, getterName)) {
|
||||||
|
found[0] = mi;
|
||||||
|
}
|
||||||
|
expr.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation nested) {
|
||||||
|
if (nested != expr && matchesGetterInvocation(nested, varName, getterName)) {
|
||||||
|
found[0] = nested;
|
||||||
|
}
|
||||||
|
return super.visit(nested);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesGetterInvocation(MethodInvocation mi, String varName, String getterName) {
|
||||||
|
if (!(mi.getExpression() instanceof SimpleName sn) || !sn.getIdentifier().equals(varName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String methodId = mi.getName().getIdentifier();
|
||||||
|
if (methodId.equals(getterName) || methodId.equalsIgnoreCase(getterName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!getterName.startsWith("get") && !getterName.startsWith("is")) {
|
||||||
|
return methodId.equals("get" + getterName) || methodId.equalsIgnoreCase("get" + getterName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ControlFlowGraph.CfgNode> collectCfgNodesBetweenDefAndUse(
|
||||||
|
MethodDeclaration md,
|
||||||
|
SimpleName receiver,
|
||||||
|
MethodInvocation useSite,
|
||||||
|
ControlFlowGraph cfg) {
|
||||||
|
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
|
||||||
|
if (rd == null) {
|
||||||
|
return orderedStatementNodes(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useSite);
|
||||||
|
if (useCfgNode == null) {
|
||||||
|
return orderedStatementNodes(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ControlFlowGraph.CfgNode> pathNodes = new HashSet<>();
|
||||||
|
Set<ASTNode> defs = rd.getReachingDefinitions(receiver, receiver.getIdentifier());
|
||||||
|
for (ASTNode def : defs) {
|
||||||
|
ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, def);
|
||||||
|
if (defCfgNode == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Set<ControlFlowGraph.CfgNode> reachFromDef = forwardReach(defCfgNode);
|
||||||
|
Set<ControlFlowGraph.CfgNode> reachToUse = backwardReach(useCfgNode);
|
||||||
|
Set<ControlFlowGraph.CfgNode> between = new HashSet<>(reachFromDef);
|
||||||
|
between.retainAll(reachToUse);
|
||||||
|
pathNodes.addAll(between);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathNodes.isEmpty()) {
|
||||||
|
return orderedStatementNodes(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> ordered = new ArrayList<>();
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||||
|
if (pathNodes.contains(node)) {
|
||||||
|
ordered.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<ControlFlowGraph.CfgNode> forwardReach(ControlFlowGraph.CfgNode start) {
|
||||||
|
Set<ControlFlowGraph.CfgNode> reached = new HashSet<>();
|
||||||
|
Queue<ControlFlowGraph.CfgNode> queue = new LinkedList<>();
|
||||||
|
queue.add(start);
|
||||||
|
reached.add(start);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
ControlFlowGraph.CfgNode current = queue.poll();
|
||||||
|
for (ControlFlowGraph.CfgNode succ : current.getSuccessors()) {
|
||||||
|
if (reached.add(succ)) {
|
||||||
|
queue.add(succ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reached;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<ControlFlowGraph.CfgNode> backwardReach(ControlFlowGraph.CfgNode end) {
|
||||||
|
Set<ControlFlowGraph.CfgNode> reached = new HashSet<>();
|
||||||
|
Queue<ControlFlowGraph.CfgNode> queue = new LinkedList<>();
|
||||||
|
queue.add(end);
|
||||||
|
reached.add(end);
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
ControlFlowGraph.CfgNode current = queue.poll();
|
||||||
|
for (ControlFlowGraph.CfgNode pred : current.getPredecessors()) {
|
||||||
|
if (reached.add(pred)) {
|
||||||
|
queue.add(pred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reached;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression extractSetterArgument(
|
||||||
|
ASTNode node,
|
||||||
|
String varName,
|
||||||
|
String resolvedSetterName,
|
||||||
|
String resolvedFieldName) {
|
||||||
|
Expression expr = unwrapCfgStatementExpression(node);
|
||||||
|
if (expr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression direct = matchSetterExpression(expr, varName, resolvedSetterName, resolvedFieldName);
|
||||||
|
if (direct != null) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
if (expr instanceof Assignment assignment) {
|
||||||
|
return matchAssignmentSetter(assignment, varName, resolvedFieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Expression[] found = new Expression[1];
|
||||||
|
expr.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation nested) {
|
||||||
|
Expression arg = matchSetterExpression(nested, varName, resolvedSetterName, resolvedFieldName);
|
||||||
|
if (arg != null) {
|
||||||
|
found[0] = arg;
|
||||||
|
}
|
||||||
|
return super.visit(nested);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment assignment) {
|
||||||
|
Expression arg = matchAssignmentSetter(assignment, varName, resolvedFieldName);
|
||||||
|
if (arg != null) {
|
||||||
|
found[0] = arg;
|
||||||
|
}
|
||||||
|
return super.visit(assignment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression unwrapCfgStatementExpression(ASTNode node) {
|
||||||
|
if (node instanceof ExpressionStatement es) {
|
||||||
|
return es.getExpression();
|
||||||
|
}
|
||||||
|
if (node instanceof VariableDeclarationStatement vds && !vds.fragments().isEmpty()) {
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) vds.fragments().get(0);
|
||||||
|
return frag.getInitializer();
|
||||||
|
}
|
||||||
|
if (node instanceof Assignment assignment) {
|
||||||
|
return assignment;
|
||||||
|
}
|
||||||
|
if (node instanceof Expression expr) {
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression matchSetterExpression(
|
||||||
|
Expression expr,
|
||||||
|
String varName,
|
||||||
|
String resolvedSetterName,
|
||||||
|
String resolvedFieldName) {
|
||||||
|
if (!(expr instanceof MethodInvocation mi)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String methodId = mi.getName().getIdentifier();
|
||||||
|
if (!methodId.equals(resolvedSetterName)
|
||||||
|
&& !methodId.equalsIgnoreCase("set" + resolvedFieldName)
|
||||||
|
&& !methodId.equalsIgnoreCase(resolvedFieldName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (mi.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)
|
||||||
|
&& !mi.arguments().isEmpty()) {
|
||||||
|
return (Expression) mi.arguments().get(0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression matchAssignmentSetter(Assignment assignment, String varName, String resolvedFieldName) {
|
||||||
|
Expression lhs = assignment.getLeftHandSide();
|
||||||
|
if (lhs instanceof FieldAccess fa) {
|
||||||
|
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
||||||
|
if (fa.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
||||||
|
return assignment.getRightHandSide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (lhs instanceof QualifiedName qqn) {
|
||||||
|
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
||||||
|
if (qqn.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
|
||||||
|
return assignment.getRightHandSide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String resolveValue(Expression expr, CodebaseContext context) {
|
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||||
if (expr == null) return null;
|
if (expr == null) return null;
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
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 FindLocalSetterArgumentTest {
|
||||||
|
|
||||||
|
private CodebaseContext context;
|
||||||
|
private VariableTracer variableTracer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Payload.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Payload {
|
||||||
|
private String event = "DEFAULT";
|
||||||
|
public String getEvent() { return event; }
|
||||||
|
public void setEvent(String event) { this.event = event; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
variableTracer = new VariableTracer(context, context.getConstantResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lastSetterBeforeGetterWins(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run() {
|
||||||
|
Payload payload = new Payload();
|
||||||
|
payload.setEvent("FIRST");
|
||||||
|
payload.setEvent("SECOND");
|
||||||
|
String value = payload.getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.toString()).contains("SECOND");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setterAfterGetterIsIgnored(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run() {
|
||||||
|
Payload payload = new Payload();
|
||||||
|
payload.setEvent("BEFORE");
|
||||||
|
String value = payload.getEvent();
|
||||||
|
payload.setEvent("AFTER");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.toString()).contains("BEFORE");
|
||||||
|
assertThat(result.toString()).doesNotContain("AFTER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ifElseBranchesBothReachGetter_lastOnCfgPathWins(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run(boolean flag) {
|
||||||
|
Payload payload = new Payload();
|
||||||
|
if (flag) {
|
||||||
|
payload.setEvent("IF_BRANCH");
|
||||||
|
} else {
|
||||||
|
payload.setEvent("ELSE_BRANCH");
|
||||||
|
}
|
||||||
|
String value = payload.getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
// Both branches are on the CFG path to the getter; last in source order wins.
|
||||||
|
assertThat(result.toString()).contains("ELSE_BRANCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setterOnSiblingBranchIsExcluded(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run(boolean flag) {
|
||||||
|
Payload payload = new Payload();
|
||||||
|
if (flag) {
|
||||||
|
payload.setEvent("TAKEN");
|
||||||
|
String value = payload.getEvent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.setEvent("OTHER_BRANCH");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.toString()).contains("TAKEN");
|
||||||
|
assertThat(result.toString()).doesNotContain("OTHER_BRANCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noGetterCallFallsBackToOrderedScan(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run() {
|
||||||
|
Payload payload = new Payload();
|
||||||
|
payload.setEvent("ONLY");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.toString()).contains("ONLY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fieldAssignmentSetterIsFound(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/MutablePayload.java", """
|
||||||
|
package com.example;
|
||||||
|
public class MutablePayload {
|
||||||
|
public String event;
|
||||||
|
public String getEvent() { return event; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run() {
|
||||||
|
MutablePayload payload = new MutablePayload();
|
||||||
|
payload.event = "ASSIGNED";
|
||||||
|
String value = payload.getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.toString()).contains("ASSIGNED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setterInsideNestedExpressionIsFound(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run() {
|
||||||
|
Payload payload = new Payload();
|
||||||
|
consume(payload.setEvent("NESTED"));
|
||||||
|
String value = payload.getEvent();
|
||||||
|
}
|
||||||
|
private void consume(String ignored) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.toString()).contains("NESTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsNullWhenNoMatchingSetter(@TempDir Path tempDir) throws IOException {
|
||||||
|
writeJava(tempDir, "com/example/Runner.java", """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run() {
|
||||||
|
Payload payload = new Payload();
|
||||||
|
String value = payload.getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
Expression result = variableTracer.traceLocalSetter(
|
||||||
|
"com.example.Runner.run", "payload", "getEvent");
|
||||||
|
|
||||||
|
assertThat(result).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
|
||||||
|
Path file = tempDir.resolve(relativePath);
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
Files.writeString(file, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user