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) {
|
||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(path);
|
||||
final boolean debug = log.isDebugEnabled();
|
||||
try {
|
||||
String event = tp.getEvent();
|
||||
event = unwrapConverterMethods(event);
|
||||
@@ -348,14 +349,18 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
|
||||
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
||||
if (debug) {
|
||||
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
||||
}
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
||||
}
|
||||
|
||||
if (expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
||||
if (!setterEvents.isEmpty()) {
|
||||
log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
|
||||
if (debug) {
|
||||
log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
|
||||
}
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
||||
}
|
||||
}
|
||||
@@ -366,7 +371,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
||||
if (!setterEvents.isEmpty()) {
|
||||
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
||||
if (debug) {
|
||||
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
||||
}
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
||||
}
|
||||
}
|
||||
@@ -378,7 +385,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (hasGetterOnReceiver) {
|
||||
List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
||||
if (getterEvents != null && !getterEvents.isEmpty()) {
|
||||
log.debug("Early return 2: getterEvents = {}", getterEvents);
|
||||
if (debug) {
|
||||
log.debug("Early return 2: getterEvents = {}", getterEvents);
|
||||
}
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
@@ -408,7 +417,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("resolvedValue before ENUM_SET check: {} for path: {}", resolvedValue, path);
|
||||
if (debug) {
|
||||
log.debug("resolvedValue before ENUM_SET check: {} for path: {}", resolvedValue, path);
|
||||
}
|
||||
List<String> polymorphicEvents = new ArrayList<>();
|
||||
|
||||
if (resolvedValue.startsWith("ENUM_SET:")) {
|
||||
@@ -869,7 +880,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("declaredType before getEnumValues: {} for exprNode: {} in {}", declaredType, exprNode, entryMethod);
|
||||
if (debug) {
|
||||
log.debug("declaredType before getEnumValues: {} for exprNode: {} in {}", declaredType, exprNode, entryMethod);
|
||||
}
|
||||
if (declaredType != null) {
|
||||
List<String> enumValues = context.getEnumValues(declaredType);
|
||||
if (enumValues != null && !enumValues.isEmpty()) {
|
||||
|
||||
@@ -95,7 +95,9 @@ public class ConstantExtractor {
|
||||
} else if (expr instanceof MethodInvocation mi) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
|
||||
log.trace("Processing mi: {}", mi);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Processing mi: {}", mi);
|
||||
}
|
||||
if (("get".equals(methodName) || "getOrDefault".equals(methodName)) && mi.getExpression() != null) {
|
||||
if (isKeyedLookupMethod(mi)) {
|
||||
extractMapReceiverConstants(mi.getExpression(), constants);
|
||||
|
||||
@@ -595,11 +595,15 @@ public class ConstructorAnalyzer {
|
||||
|
||||
try {
|
||||
Map<String, String> fieldValues = buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor);
|
||||
log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("RESOLVE_GETTER: td={}, fieldValues={}", context.getFqn(td), fieldValues);
|
||||
}
|
||||
if (fieldValues.isEmpty()) return Collections.emptyList();
|
||||
|
||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
||||
log.debug("RESOLVE_GETTER: result={}", result);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("RESOLVE_GETTER: result={}", result);
|
||||
}
|
||||
return result != null ? List.of(result) : Collections.emptyList();
|
||||
} finally {
|
||||
visited.remove(getterKey);
|
||||
|
||||
@@ -200,16 +200,24 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
nameBinding = fa.resolveFieldBinding();
|
||||
}
|
||||
if (nameBinding instanceof IVariableBinding varBinding) {
|
||||
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("CALLGRAPH RESOLVING BEAN FOR BINDING: {} calling {}", varBinding.getName(), methodName);
|
||||
}
|
||||
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||
if (concreteFqn != null) {
|
||||
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(" -> RESOLVED CONCRETE FQN: {}", concreteFqn);
|
||||
}
|
||||
return concreteFqn + "." + methodName;
|
||||
} else {
|
||||
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(" -> RESOLVE FAILED, RETURNED NULL");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("CALLGRAPH NAME BINDING IS NOT VARIABLE: {} calling {}", nameBinding, methodName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,79 +64,48 @@ public class VariableTracer {
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
final Expression[] setterArg = new Expression[1];
|
||||
String propName;
|
||||
String setterMethodName;
|
||||
String indexedFieldName;
|
||||
if (td == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md == null || md.getBody() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isBeanStyleAccessorName(getterName)) {
|
||||
propName = propertyNameFromAccessor(getterName);
|
||||
setterMethodName = "set" + capitalizeProperty(propName);
|
||||
indexedFieldName = propName;
|
||||
String setterMethodName;
|
||||
String indexedFieldName;
|
||||
if (isBeanStyleAccessorName(getterName)) {
|
||||
String propName = propertyNameFromAccessor(getterName);
|
||||
setterMethodName = "set" + capitalizeProperty(propName);
|
||||
indexedFieldName = propName;
|
||||
|
||||
String varTypeName = getVariableDeclaredType(methodFqn, varName);
|
||||
if (varTypeName != null) {
|
||||
CompilationUnit contextCu = md.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||
TypeDeclaration varType = contextCu != null
|
||||
? context.getTypeDeclaration(varTypeName, contextCu)
|
||||
: null;
|
||||
if (varType == null) {
|
||||
varType = context.getTypeDeclaration(varTypeName);
|
||||
}
|
||||
if (varType != null) {
|
||||
Optional<AccessorSummary> indexedSetter = context.getAccessorIndex()
|
||||
.lookup(context.getFqn(varType), setterMethodName);
|
||||
if (indexedSetter.isPresent() && indexedSetter.get().isSetter()) {
|
||||
setterMethodName = indexedSetter.get().methodName();
|
||||
indexedFieldName = indexedSetter.get().fieldName();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||
setterMethodName = "set" + propName;
|
||||
indexedFieldName = propName;
|
||||
String varTypeName = getVariableDeclaredType(methodFqn, varName);
|
||||
if (varTypeName != null) {
|
||||
CompilationUnit contextCu = md.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||
TypeDeclaration varType = contextCu != null
|
||||
? context.getTypeDeclaration(varTypeName, contextCu)
|
||||
: null;
|
||||
if (varType == null) {
|
||||
varType = context.getTypeDeclaration(varTypeName);
|
||||
}
|
||||
|
||||
final String resolvedSetterName = setterMethodName;
|
||||
final String resolvedFieldName = indexedFieldName;
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@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);
|
||||
if (varType != null) {
|
||||
Optional<AccessorSummary> indexedSetter = context.getAccessorIndex()
|
||||
.lookup(context.getFqn(varType), setterMethodName);
|
||||
if (indexedSetter.isPresent() && indexedSetter.get().isSetter()) {
|
||||
setterMethodName = indexedSetter.get().methodName();
|
||||
indexedFieldName = indexedSetter.get().fieldName();
|
||||
}
|
||||
@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];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
|
||||
setterMethodName = "set" + propName;
|
||||
indexedFieldName = propName;
|
||||
}
|
||||
|
||||
if (dataFlowModel instanceof JdtDataFlowModel jdtDataFlowModel) {
|
||||
return jdtDataFlowModel.findLocalSetterArgument(
|
||||
md, varName, getterName, setterMethodName, indexedFieldName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1495,6 +1495,272 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
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
|
||||
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||
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