stage 1
This commit is contained in:
@@ -185,9 +185,18 @@ public class ConstructorAnalyzer {
|
|||||||
String tdKey = context.getFqn(td);
|
String tdKey = context.getFqn(td);
|
||||||
if (!visitedCtors.add(tdKey)) return;
|
if (!visitedCtors.add(tdKey)) return;
|
||||||
|
|
||||||
|
IMethodBinding resolvedCtor = cic.resolveConstructorBinding();
|
||||||
|
|
||||||
for (MethodDeclaration ctor : td.getMethods()) {
|
for (MethodDeclaration ctor : td.getMethods()) {
|
||||||
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
|
if (!ctor.isConstructor() || ctor.getBody() == null) continue;
|
||||||
if (ctor.parameters().size() != cic.arguments().size()) continue;
|
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedCtor != null && ctor.resolveBinding() != null) {
|
||||||
|
matches = resolvedCtor.isEqualTo(ctor.resolveBinding()) || resolvedCtor.getKey().equals(ctor.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = ctor.parameters().size() == cic.arguments().size();
|
||||||
|
}
|
||||||
|
if (!matches) continue;
|
||||||
|
|
||||||
Map<String, String> paramValues = new HashMap<>();
|
Map<String, String> paramValues = new HashMap<>();
|
||||||
for (int i = 0; i < ctor.parameters().size(); i++) {
|
for (int i = 0; i < ctor.parameters().size(); i++) {
|
||||||
@@ -247,11 +256,22 @@ public class ConstructorAnalyzer {
|
|||||||
ctor.getBody().accept(new ASTVisitor() {
|
ctor.getBody().accept(new ASTVisitor() {
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(SuperConstructorInvocation sci) {
|
public boolean visit(SuperConstructorInvocation sci) {
|
||||||
|
IMethodBinding binding = sci.resolveConstructorBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
TypeDeclaration superTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
|
||||||
|
if (superTd != null) {
|
||||||
|
processSuperConstructorArgs(superTd, binding, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
String superFqn = context.getSuperclassFqn(td);
|
String superFqn = context.getSuperclassFqn(td);
|
||||||
if (superFqn != null) {
|
if (superFqn != null) {
|
||||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||||
if (superTd != null) {
|
if (superTd != null) {
|
||||||
processSuperConstructorArgs(superTd, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
processSuperConstructorArgs(superTd, null, sci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.visit(sci);
|
return super.visit(sci);
|
||||||
@@ -259,7 +279,8 @@ public class ConstructorAnalyzer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(ConstructorInvocation ci) {
|
public boolean visit(ConstructorInvocation ci) {
|
||||||
processSuperConstructorArgs(td, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
IMethodBinding binding = ci.resolveConstructorBinding();
|
||||||
|
processSuperConstructorArgs(td, binding, ci.arguments(), paramValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
return super.visit(ci);
|
return super.visit(ci);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -269,6 +290,7 @@ public class ConstructorAnalyzer {
|
|||||||
|
|
||||||
private void processSuperConstructorArgs(
|
private void processSuperConstructorArgs(
|
||||||
TypeDeclaration superTd,
|
TypeDeclaration superTd,
|
||||||
|
IMethodBinding resolvedBinding,
|
||||||
List<?> superArgs,
|
List<?> superArgs,
|
||||||
Map<String, String> subClassParamValues,
|
Map<String, String> subClassParamValues,
|
||||||
CodebaseContext context,
|
CodebaseContext context,
|
||||||
@@ -280,7 +302,14 @@ public class ConstructorAnalyzer {
|
|||||||
|
|
||||||
for (MethodDeclaration superCtor : superTd.getMethods()) {
|
for (MethodDeclaration superCtor : superTd.getMethods()) {
|
||||||
if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
|
if (!superCtor.isConstructor() || superCtor.getBody() == null) continue;
|
||||||
if (superCtor.parameters().size() != superArgs.size()) continue;
|
|
||||||
|
boolean matches = false;
|
||||||
|
if (resolvedBinding != null && superCtor.resolveBinding() != null) {
|
||||||
|
matches = resolvedBinding.isEqualTo(superCtor.resolveBinding()) || resolvedBinding.getKey().equals(superCtor.resolveBinding().getKey());
|
||||||
|
} else {
|
||||||
|
matches = superCtor.parameters().size() == superArgs.size();
|
||||||
|
}
|
||||||
|
if (!matches) continue;
|
||||||
|
|
||||||
Map<String, String> superParamValues = new HashMap<>();
|
Map<String, String> superParamValues = new HashMap<>();
|
||||||
for (int i = 0; i < superCtor.parameters().size(); i++) {
|
for (int i = 0; i < superCtor.parameters().size(); i++) {
|
||||||
@@ -343,11 +372,22 @@ public class ConstructorAnalyzer {
|
|||||||
superCtor.getBody().accept(new ASTVisitor() {
|
superCtor.getBody().accept(new ASTVisitor() {
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(SuperConstructorInvocation sci) {
|
public boolean visit(SuperConstructorInvocation sci) {
|
||||||
|
IMethodBinding binding = sci.resolveConstructorBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding declaringClass = binding.getDeclaringClass();
|
||||||
|
if (declaringClass != null) {
|
||||||
|
TypeDeclaration nextSuperTd = context.getTypeDeclaration(declaringClass.getErasure().getQualifiedName());
|
||||||
|
if (nextSuperTd != null) {
|
||||||
|
processSuperConstructorArgs(nextSuperTd, binding, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
|
return super.visit(sci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
String superFqn = context.getSuperclassFqn(superTd);
|
String superFqn = context.getSuperclassFqn(superTd);
|
||||||
if (superFqn != null) {
|
if (superFqn != null) {
|
||||||
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
|
TypeDeclaration nextSuperTd = context.getTypeDeclaration(superFqn);
|
||||||
if (nextSuperTd != null) {
|
if (nextSuperTd != null) {
|
||||||
processSuperConstructorArgs(nextSuperTd, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
processSuperConstructorArgs(nextSuperTd, null, sci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.visit(sci);
|
return super.visit(sci);
|
||||||
@@ -355,7 +395,8 @@ public class ConstructorAnalyzer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(ConstructorInvocation ci) {
|
public boolean visit(ConstructorInvocation ci) {
|
||||||
processSuperConstructorArgs(superTd, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
IMethodBinding binding = ci.resolveConstructorBinding();
|
||||||
|
processSuperConstructorArgs(superTd, binding, ci.arguments(), superParamValues, context, fieldValues, visitedCtors, rhsResolver);
|
||||||
return super.visit(ci);
|
return super.visit(ci);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ public class VariableTracer {
|
|||||||
private final ConstantResolver constantResolver;
|
private final ConstantResolver constantResolver;
|
||||||
private ConstantExtractor constantExtractor;
|
private ConstantExtractor constantExtractor;
|
||||||
|
|
||||||
|
private final click.kamil.springstatemachineexporter.ast.common.DataFlowModel dataFlowModel;
|
||||||
|
|
||||||
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
public VariableTracer(CodebaseContext context, ConstantResolver constantResolver) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.constantResolver = constantResolver;
|
this.constantResolver = constantResolver;
|
||||||
|
this.dataFlowModel = new click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setConstantExtractor(ConstantExtractor constantExtractor) {
|
public void setConstantExtractor(ConstantExtractor constantExtractor) {
|
||||||
@@ -21,101 +24,16 @@ public class VariableTracer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Expression traceVariable(Expression expr) {
|
public Expression traceVariable(Expression expr) {
|
||||||
List<Expression> all = traceVariableAll(expr, new HashSet<>());
|
List<Expression> all = traceVariableAll(expr);
|
||||||
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
return all.isEmpty() ? expr : all.get(all.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Expression> traceVariableAll(Expression expr) {
|
public List<Expression> traceVariableAll(Expression expr) {
|
||||||
return traceVariableAll(expr, new HashSet<>());
|
return dataFlowModel.getReachingDefinitions(expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
|
public List<Expression> traceVariableAll(Expression expr, Set<String> visitedVariables) {
|
||||||
List<Expression> results = new ArrayList<>();
|
return dataFlowModel.getReachingDefinitions(expr);
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
String varName = sn.getIdentifier();
|
|
||||||
String methodKey = "";
|
|
||||||
MethodDeclaration enclosingMethod = findEnclosingMethod(expr);
|
|
||||||
if (enclosingMethod != null) {
|
|
||||||
TypeDeclaration enclosingType = findEnclosingType(enclosingMethod);
|
|
||||||
if (enclosingType != null) {
|
|
||||||
methodKey = context.getFqn(enclosingType) + "." + enclosingMethod.getName().getIdentifier() + "#" + varName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (methodKey.isEmpty()) {
|
|
||||||
methodKey = varName;
|
|
||||||
}
|
|
||||||
if (!visitedVariables.add(methodKey)) {
|
|
||||||
return results; // Break infinite loop
|
|
||||||
}
|
|
||||||
if (enclosingMethod != null) {
|
|
||||||
enclosingMethod.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(VariableDeclarationFragment node) {
|
|
||||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
|
||||||
results.addAll(traceVariableAll(node.getInitializer(), visitedVariables));
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean visit(Assignment node) {
|
|
||||||
if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) {
|
|
||||||
results.addAll(traceVariableAll(node.getRightHandSide(), visitedVariables));
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!results.isEmpty()) {
|
|
||||||
visitedVariables.remove(methodKey);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tracing parameter callers (Inter-procedural within the same class)
|
|
||||||
for (Object paramObj : enclosingMethod.parameters()) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
|
||||||
if (param.getName().getIdentifier().equals(varName)) {
|
|
||||||
int paramIndex = enclosingMethod.parameters().indexOf(param);
|
|
||||||
TypeDeclaration enclosingType = findEnclosingType(enclosingMethod);
|
|
||||||
if (enclosingType != null) {
|
|
||||||
String mName = enclosingMethod.getName().getIdentifier();
|
|
||||||
List<Expression> callerExprs = new ArrayList<>();
|
|
||||||
enclosingType.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (node.getName().getIdentifier().equals(mName) && node.arguments().size() > paramIndex) {
|
|
||||||
callerExprs.addAll(traceVariableAll((Expression) node.arguments().get(paramIndex), visitedVariables));
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean visit(ExpressionMethodReference node) {
|
|
||||||
if (node.getName().getIdentifier().equals(mName)) {
|
|
||||||
if (node.getParent() instanceof MethodInvocation parentMi) {
|
|
||||||
if (click.kamil.springstatemachineexporter.ast.common.AstUtils.isFunctionalCollectionMethod(parentMi)) {
|
|
||||||
callerExprs.addAll(traceVariableAll(parentMi.getExpression(), visitedVariables));
|
|
||||||
} else {
|
|
||||||
for (Object arg : parentMi.arguments()) {
|
|
||||||
if (arg != node) {
|
|
||||||
callerExprs.addAll(traceVariableAll((Expression) arg, visitedVariables));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!callerExprs.isEmpty()) {
|
|
||||||
visitedVariables.remove(methodKey);
|
|
||||||
return callerExprs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visitedVariables.remove(methodKey);
|
|
||||||
}
|
|
||||||
results.add(expr);
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Expression traceLocalSetter(String methodFqn, String varName, String getterName) {
|
public Expression traceLocalSetter(String methodFqn, String varName, String getterName) {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
public final class BindingResolver {
|
||||||
|
|
||||||
|
private BindingResolver() {
|
||||||
|
// Prevent instantiation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding for a given AST Type node.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(Type type) {
|
||||||
|
if (type == null) return null;
|
||||||
|
return type.resolveBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding for a given AST Expression node.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(Expression expression) {
|
||||||
|
if (expression == null) return null;
|
||||||
|
return expression.resolveTypeBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the method binding for a MethodInvocation.
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveMethod(MethodInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveMethodBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a ClassInstanceCreation.
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(ClassInstanceCreation creation) {
|
||||||
|
if (creation == null) return null;
|
||||||
|
return creation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a ConstructorInvocation (e.g. this(...)).
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(ConstructorInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the constructor binding for a SuperConstructorInvocation (e.g. super(...)).
|
||||||
|
*/
|
||||||
|
public static IMethodBinding resolveConstructor(SuperConstructorInvocation invocation) {
|
||||||
|
if (invocation == null) return null;
|
||||||
|
return invocation.resolveConstructorBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type binding of a TypeDeclaration.
|
||||||
|
*/
|
||||||
|
public static ITypeBinding resolveType(TypeDeclaration td) {
|
||||||
|
if (td == null) return null;
|
||||||
|
return td.resolveBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the fully qualified name of a type binding, safely handling nulls and erasure.
|
||||||
|
*/
|
||||||
|
public static String getFullyQualifiedName(ITypeBinding binding) {
|
||||||
|
if (binding == null) return null;
|
||||||
|
return binding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if childBinding is compatible with (extends or implements) targetFqn.
|
||||||
|
*/
|
||||||
|
public static boolean isSubtypeOf(ITypeBinding childBinding, String targetFqn) {
|
||||||
|
if (childBinding == null || targetFqn == null) return false;
|
||||||
|
|
||||||
|
// Direct match
|
||||||
|
if (targetFqn.equals(childBinding.getErasure().getQualifiedName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check interfaces
|
||||||
|
for (ITypeBinding intf : childBinding.getInterfaces()) {
|
||||||
|
if (isSubtypeOf(intf, targetFqn)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check superclass
|
||||||
|
ITypeBinding superclass = childBinding.getSuperclass();
|
||||||
|
if (superclass != null) {
|
||||||
|
return isSubtypeOf(superclass, targetFqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class CfgBuilder {
|
||||||
|
|
||||||
|
public static ControlFlowGraph build(MethodDeclaration method) {
|
||||||
|
ControlFlowGraph cfg = new ControlFlowGraph(method);
|
||||||
|
if (method.getBody() == null) {
|
||||||
|
cfg.getEntryNode().addSuccessor(cfg.getExitNode());
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CFG recursively
|
||||||
|
List<ControlFlowGraph.CfgNode> exits = buildFlow(method.getBody(), List.of(cfg.getEntryNode()), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : exits) {
|
||||||
|
exit.addSuccessor(cfg.getExitNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ControlFlowGraph.CfgNode> buildFlow(
|
||||||
|
ASTNode node,
|
||||||
|
List<ControlFlowGraph.CfgNode> sources,
|
||||||
|
ControlFlowGraph cfg) {
|
||||||
|
|
||||||
|
if (node == null || sources.isEmpty()) {
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof Block block) {
|
||||||
|
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||||
|
for (Object stmtObj : block.statements()) {
|
||||||
|
currentSources = buildFlow((ASTNode) stmtObj, currentSources, cfg);
|
||||||
|
}
|
||||||
|
return currentSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof IfStatement ifStmt) {
|
||||||
|
// Condition node
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(ifStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> thenExits = buildFlow(ifStmt.getThenStatement(), List.of(condNode), cfg);
|
||||||
|
List<ControlFlowGraph.CfgNode> elseExits;
|
||||||
|
if (ifStmt.getElseStatement() != null) {
|
||||||
|
elseExits = buildFlow(ifStmt.getElseStatement(), List.of(condNode), cfg);
|
||||||
|
} else {
|
||||||
|
elseExits = List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> combinedExits = new ArrayList<>();
|
||||||
|
combinedExits.addAll(thenExits);
|
||||||
|
combinedExits.addAll(elseExits);
|
||||||
|
return combinedExits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof WhileStatement whileStmt) {
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(whileStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(whileStmt.getBody(), List.of(condNode), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(condNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(condNode); // Exit loop when cond is false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof DoStatement doStmt) {
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(doStmt.getBody(), sources, cfg);
|
||||||
|
ControlFlowGraph.CfgNode condNode = new ControlFlowGraph.CfgNode(doStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
condNode.addSuccessor(condNode); // Loop back
|
||||||
|
return List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof ForStatement forStmt) {
|
||||||
|
// Initializers
|
||||||
|
List<ControlFlowGraph.CfgNode> currentSources = sources;
|
||||||
|
for (Object initObj : forStmt.initializers()) {
|
||||||
|
ControlFlowGraph.CfgNode initNode = new ControlFlowGraph.CfgNode((ASTNode) initObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(initNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||||
|
src.addSuccessor(initNode);
|
||||||
|
}
|
||||||
|
currentSources = List.of(initNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition
|
||||||
|
ControlFlowGraph.CfgNode condNode;
|
||||||
|
if (forStmt.getExpression() != null) {
|
||||||
|
condNode = new ControlFlowGraph.CfgNode(forStmt.getExpression(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
} else {
|
||||||
|
condNode = new ControlFlowGraph.CfgNode(forStmt, ControlFlowGraph.CfgNodeType.STATEMENT); // Dummy node
|
||||||
|
}
|
||||||
|
cfg.addNode(condNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : currentSources) {
|
||||||
|
src.addSuccessor(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(forStmt.getBody(), List.of(condNode), cfg);
|
||||||
|
|
||||||
|
// Updaters
|
||||||
|
List<ControlFlowGraph.CfgNode> updaterSources = bodyExits;
|
||||||
|
for (Object updateObj : forStmt.updaters()) {
|
||||||
|
ControlFlowGraph.CfgNode updateNode = new ControlFlowGraph.CfgNode((ASTNode) updateObj, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(updateNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||||
|
src.addSuccessor(updateNode);
|
||||||
|
}
|
||||||
|
updaterSources = List.of(updateNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode src : updaterSources) {
|
||||||
|
src.addSuccessor(condNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(condNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof EnhancedForStatement effStmt) {
|
||||||
|
ControlFlowGraph.CfgNode loopNode = new ControlFlowGraph.CfgNode(effStmt.getParameter(), ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(loopNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(loopNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ControlFlowGraph.CfgNode> bodyExits = buildFlow(effStmt.getBody(), List.of(loopNode), cfg);
|
||||||
|
for (ControlFlowGraph.CfgNode exit : bodyExits) {
|
||||||
|
exit.addSuccessor(loopNode); // Loop back
|
||||||
|
}
|
||||||
|
|
||||||
|
return List.of(loopNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof ReturnStatement returnStmt) {
|
||||||
|
ControlFlowGraph.CfgNode retNode = new ControlFlowGraph.CfgNode(returnStmt, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(retNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(retNode);
|
||||||
|
}
|
||||||
|
// Return flows to exit, not to subsequent sequential nodes
|
||||||
|
retNode.addSuccessor(cfg.getExitNode());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: general statement
|
||||||
|
ControlFlowGraph.CfgNode stmtNode = new ControlFlowGraph.CfgNode(node, ControlFlowGraph.CfgNodeType.STATEMENT);
|
||||||
|
cfg.addNode(stmtNode);
|
||||||
|
for (ControlFlowGraph.CfgNode src : sources) {
|
||||||
|
src.addSuccessor(stmtNode);
|
||||||
|
}
|
||||||
|
return List.of(stmtNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -356,6 +356,7 @@ public class CodebaseContext {
|
|||||||
parser.setSource(source.toCharArray());
|
parser.setSource(source.toCharArray());
|
||||||
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
parser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
parser.setResolveBindings(resolveBindings);
|
parser.setResolveBindings(resolveBindings);
|
||||||
|
parser.setBindingsRecovery(true);
|
||||||
|
|
||||||
Map<String, String> options = JavaCore.getOptions();
|
Map<String, String> options = JavaCore.getOptions();
|
||||||
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
|
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
|
||||||
@@ -445,6 +446,14 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getSuperclassFqn(TypeDeclaration td) {
|
public String getSuperclassFqn(TypeDeclaration td) {
|
||||||
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
ITypeBinding superBinding = binding.getSuperclass();
|
||||||
|
if (superBinding != null) {
|
||||||
|
return superBinding.getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Type superType = td.getSuperclassType();
|
Type superType = td.getSuperclassType();
|
||||||
if (superType == null) return null;
|
if (superType == null) return null;
|
||||||
|
|
||||||
@@ -466,6 +475,44 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||||
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
if (binding != null && includeSuper) {
|
||||||
|
IMethodBinding methodBinding = findMethodBinding(binding, methodName);
|
||||||
|
if (methodBinding != null) {
|
||||||
|
CompilationUnit declaringCu = classes.get(methodBinding.getDeclaringClass().getErasure().getQualifiedName());
|
||||||
|
if (declaringCu != null) {
|
||||||
|
ASTNode declNode = declaringCu.findDeclaringNode(methodBinding.getKey());
|
||||||
|
if (declNode instanceof MethodDeclaration) {
|
||||||
|
return (MethodDeclaration) declNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findMethodDeclarationLegacy(td, methodName, includeSuper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IMethodBinding findMethodBinding(ITypeBinding typeBinding, String methodName) {
|
||||||
|
for (IMethodBinding mb : typeBinding.getDeclaredMethods()) {
|
||||||
|
if (mb.getName().equals(methodName)) {
|
||||||
|
return mb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ITypeBinding superclass = typeBinding.getSuperclass();
|
||||||
|
if (superclass != null) {
|
||||||
|
IMethodBinding mb = findMethodBinding(superclass, methodName);
|
||||||
|
if (mb != null) return mb;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ITypeBinding intf : typeBinding.getInterfaces()) {
|
||||||
|
IMethodBinding mb = findMethodBinding(intf, methodName);
|
||||||
|
if (mb != null) return mb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodDeclarationLegacy(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||||
for (MethodDeclaration method : td.getMethods()) {
|
for (MethodDeclaration method : td.getMethods()) {
|
||||||
if (method.getName().getIdentifier().equals(methodName)) {
|
if (method.getName().getIdentifier().equals(methodName)) {
|
||||||
return method;
|
return method;
|
||||||
@@ -477,7 +524,7 @@ public class CodebaseContext {
|
|||||||
if (superFqn != null) {
|
if (superFqn != null) {
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
||||||
if (superTd != null) {
|
if (superTd != null) {
|
||||||
return findMethodDeclaration(superTd, methodName, true);
|
return findMethodDeclarationLegacy(superTd, methodName, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +534,7 @@ public class CodebaseContext {
|
|||||||
String interfaceName = extractTypeName(interfaceType);
|
String interfaceName = extractTypeName(interfaceType);
|
||||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (CompilationUnit) td.getRoot());
|
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (CompilationUnit) td.getRoot());
|
||||||
if (interfaceTd != null) {
|
if (interfaceTd != null) {
|
||||||
MethodDeclaration found = findMethodDeclaration(interfaceTd, methodName, true);
|
MethodDeclaration found = findMethodDeclarationLegacy(interfaceTd, methodName, true);
|
||||||
if (found != null) return found;
|
if (found != null) return found;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,10 +712,19 @@ public class CodebaseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
public boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td) {
|
||||||
return extendsStateMachineConfigurerAdapter(td, new HashSet<>());
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
boolean isSubtype = BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.builders.StateMachineConfigurer")
|
||||||
|
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.StateMachineConfigurerAdapter")
|
||||||
|
|| BindingResolver.isSubtypeOf(binding, "org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter");
|
||||||
|
if (isSubtype) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extendsStateMachineConfigurerAdapterLegacy(td, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean extendsStateMachineConfigurerAdapter(TypeDeclaration td, Set<String> visited) {
|
private boolean extendsStateMachineConfigurerAdapterLegacy(TypeDeclaration td, Set<String> visited) {
|
||||||
String fqn = getFqn(td);
|
String fqn = getFqn(td);
|
||||||
if (visited.contains(fqn)) {
|
if (visited.contains(fqn)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -685,7 +741,7 @@ public class CodebaseContext {
|
|||||||
if (knownAdapters.contains(superclassName))
|
if (knownAdapters.contains(superclassName))
|
||||||
return true;
|
return true;
|
||||||
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
TypeDeclaration superTd = getTypeDeclaration(superclassName, cu);
|
||||||
if (superTd != null && extendsStateMachineConfigurerAdapter(superTd, visited))
|
if (superTd != null && extendsStateMachineConfigurerAdapterLegacy(superTd, visited))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +752,7 @@ public class CodebaseContext {
|
|||||||
if (knownAdapters.contains(interfaceName))
|
if (knownAdapters.contains(interfaceName))
|
||||||
return true;
|
return true;
|
||||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
||||||
if (interfaceTd != null && extendsStateMachineConfigurerAdapter(interfaceTd, visited))
|
if (interfaceTd != null && extendsStateMachineConfigurerAdapterLegacy(interfaceTd, visited))
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ControlFlowGraph {
|
||||||
|
private final MethodDeclaration method;
|
||||||
|
private final CfgNode entryNode = new CfgNode(null, CfgNodeType.ENTRY);
|
||||||
|
private final CfgNode exitNode = new CfgNode(null, CfgNodeType.EXIT);
|
||||||
|
private final List<CfgNode> nodes = new ArrayList<>();
|
||||||
|
|
||||||
|
public enum CfgNodeType {
|
||||||
|
ENTRY,
|
||||||
|
EXIT,
|
||||||
|
STATEMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CfgNode {
|
||||||
|
private final ASTNode astNode;
|
||||||
|
private final CfgNodeType type;
|
||||||
|
private final Set<CfgNode> predecessors = new HashSet<>();
|
||||||
|
private final Set<CfgNode> successors = new HashSet<>();
|
||||||
|
|
||||||
|
public CfgNode(ASTNode astNode, CfgNodeType type) {
|
||||||
|
this.astNode = astNode;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ASTNode getAstNode() {
|
||||||
|
return astNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CfgNodeType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<CfgNode> getPredecessors() {
|
||||||
|
return predecessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<CfgNode> getSuccessors() {
|
||||||
|
return successors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSuccessor(CfgNode succ) {
|
||||||
|
if (succ != null) {
|
||||||
|
this.successors.add(succ);
|
||||||
|
succ.predecessors.add(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ControlFlowGraph(MethodDeclaration method) {
|
||||||
|
this.method = method;
|
||||||
|
this.nodes.add(entryNode);
|
||||||
|
this.nodes.add(exitNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodDeclaration getMethod() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CfgNode getEntryNode() {
|
||||||
|
return entryNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CfgNode getExitNode() {
|
||||||
|
return exitNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CfgNode> getNodes() {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNode(CfgNode node) {
|
||||||
|
this.nodes.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface DataFlowModel {
|
||||||
|
/**
|
||||||
|
* Resolves all possible Expressions that define the value of the given expression
|
||||||
|
* at its location in the control flow.
|
||||||
|
*/
|
||||||
|
List<Expression> getReachingDefinitions(Expression expr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the single constant/literal value if all definitions converge,
|
||||||
|
* or if a single value is expected.
|
||||||
|
*/
|
||||||
|
String resolveValue(Expression expr, CodebaseContext context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class JdtDataFlowModel implements DataFlowModel {
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final Map<MethodDeclaration, ControlFlowGraph> cfgCache = new HashMap<>();
|
||||||
|
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
||||||
|
|
||||||
|
public JdtDataFlowModel(CodebaseContext context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof MethodDeclaration) {
|
||||||
|
return (MethodDeclaration) current;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReachingDefinitions getReachingDefinitionsForMethod(MethodDeclaration method) {
|
||||||
|
if (method == null) return null;
|
||||||
|
return rdCache.computeIfAbsent(method, m -> {
|
||||||
|
ControlFlowGraph cfg = cfgCache.computeIfAbsent(m, CfgBuilder::build);
|
||||||
|
return new ReachingDefinitions(cfg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Expression> getReachingDefinitions(Expression expr) {
|
||||||
|
return getReachingDefinitions(expr, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Expression> getReachingDefinitions(Expression expr, Set<ASTNode> visited) {
|
||||||
|
if (expr == null || !visited.add(expr)) return List.of();
|
||||||
|
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
MethodDeclaration md = findEnclosingMethod(sn);
|
||||||
|
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
|
||||||
|
if (rd != null) {
|
||||||
|
Set<ASTNode> defs = rd.getReachingDefinitions(sn, sn.getIdentifier());
|
||||||
|
if (!defs.isEmpty()) {
|
||||||
|
List<Expression> exprs = new ArrayList<>();
|
||||||
|
for (ASTNode def : defs) {
|
||||||
|
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
|
||||||
|
if (defExpr != null) {
|
||||||
|
exprs.addAll(getReachingDefinitions(defExpr, visited));
|
||||||
|
} else {
|
||||||
|
exprs.add(expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visited.remove(expr);
|
||||||
|
return exprs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.remove(expr);
|
||||||
|
return List.of(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||||
|
if (expr == null) return null;
|
||||||
|
|
||||||
|
List<Expression> defs = getReachingDefinitions(expr);
|
||||||
|
if (defs.isEmpty()) {
|
||||||
|
return context.resolveExpression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
String firstVal = null;
|
||||||
|
for (Expression def : defs) {
|
||||||
|
String val = context.resolveExpression(def);
|
||||||
|
if (val != null) {
|
||||||
|
if (firstVal == null) {
|
||||||
|
firstVal = val;
|
||||||
|
} else if (!firstVal.equals(val)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstVal != null ? firstVal : context.resolveExpression(expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LegacyDataFlowModel implements DataFlowModel {
|
||||||
|
private final VariableTracer variableTracer;
|
||||||
|
|
||||||
|
public LegacyDataFlowModel(VariableTracer variableTracer) {
|
||||||
|
this.variableTracer = variableTracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Expression> getReachingDefinitions(Expression expr) {
|
||||||
|
if (variableTracer == null || expr == null) {
|
||||||
|
return expr != null ? List.of(expr) : List.of();
|
||||||
|
}
|
||||||
|
return variableTracer.traceVariableAll(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveValue(Expression expr, CodebaseContext context) {
|
||||||
|
if (expr == null) return null;
|
||||||
|
if (variableTracer == null) {
|
||||||
|
return context.resolveExpression(expr);
|
||||||
|
}
|
||||||
|
Expression traced = variableTracer.traceVariable(expr);
|
||||||
|
return context.resolveExpression(traced);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ReachingDefinitions {
|
||||||
|
private final ControlFlowGraph cfg;
|
||||||
|
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> inMap = new HashMap<>();
|
||||||
|
private final Map<ControlFlowGraph.CfgNode, Set<ASTNode>> outMap = new HashMap<>();
|
||||||
|
|
||||||
|
public ReachingDefinitions(ControlFlowGraph cfg) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
analyze();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void analyze() {
|
||||||
|
List<ControlFlowGraph.CfgNode> cfgNodes = cfg.getNodes();
|
||||||
|
Map<ControlFlowGraph.CfgNode, ASTNode> nodeDefs = new HashMap<>();
|
||||||
|
Map<String, Set<ASTNode>> varToDefs = new HashMap<>();
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||||
|
ASTNode ast = node.getAstNode();
|
||||||
|
if (ast == null) continue;
|
||||||
|
|
||||||
|
ASTNode def = findDefinition(ast);
|
||||||
|
if (def != null) {
|
||||||
|
nodeDefs.put(node, def);
|
||||||
|
String varName = getDefinedVariableName(def);
|
||||||
|
if (varName != null) {
|
||||||
|
varToDefs.computeIfAbsent(varName, k -> new HashSet<>()).add(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||||
|
inMap.put(node, new HashSet<>());
|
||||||
|
outMap.put(node, new HashSet<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean changed = true;
|
||||||
|
while (changed) {
|
||||||
|
changed = false;
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfgNodes) {
|
||||||
|
Set<ASTNode> newIn = new HashSet<>();
|
||||||
|
for (ControlFlowGraph.CfgNode pred : node.getPredecessors()) {
|
||||||
|
newIn.addAll(outMap.get(pred));
|
||||||
|
}
|
||||||
|
|
||||||
|
inMap.put(node, newIn);
|
||||||
|
|
||||||
|
Set<ASTNode> newOut = new HashSet<>(newIn);
|
||||||
|
ASTNode def = nodeDefs.get(node);
|
||||||
|
if (def != null) {
|
||||||
|
String varName = getDefinedVariableName(def);
|
||||||
|
if (varName != null) {
|
||||||
|
Set<ASTNode> allDefs = varToDefs.get(varName);
|
||||||
|
if (allDefs != null) {
|
||||||
|
newOut.removeAll(allDefs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newOut.add(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ASTNode> oldOut = outMap.put(node, newOut);
|
||||||
|
if (!newOut.equals(oldOut)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<ASTNode> getReachingDefinitions(ASTNode usageNode, String varName) {
|
||||||
|
ControlFlowGraph.CfgNode cfgNode = findCfgNodeForUsage(usageNode);
|
||||||
|
if (cfgNode == null) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<ASTNode> reaching = new HashSet<>();
|
||||||
|
Set<ASTNode> inDefs = inMap.get(cfgNode);
|
||||||
|
if (inDefs != null) {
|
||||||
|
for (ASTNode def : inDefs) {
|
||||||
|
if (varName.equals(getDefinedVariableName(def))) {
|
||||||
|
reaching.add(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reaching;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControlFlowGraph.CfgNode findCfgNodeForUsage(ASTNode usageNode) {
|
||||||
|
ASTNode current = usageNode;
|
||||||
|
while (current != null) {
|
||||||
|
for (ControlFlowGraph.CfgNode node : cfg.getNodes()) {
|
||||||
|
if (node.getAstNode() == current) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ASTNode findDefinition(ASTNode ast) {
|
||||||
|
if (ast instanceof VariableDeclarationStatement vds) {
|
||||||
|
if (!vds.fragments().isEmpty()) {
|
||||||
|
return (ASTNode) vds.fragments().get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ast instanceof VariableDeclarationFragment) {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
if (ast instanceof SingleVariableDeclaration) {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
if (ast instanceof Assignment) {
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
if (ast instanceof ExpressionStatement es) {
|
||||||
|
return findDefinition(es.getExpression());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDefinedVariableName(ASTNode def) {
|
||||||
|
if (def instanceof VariableDeclarationFragment frag) {
|
||||||
|
return frag.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (def instanceof SingleVariableDeclaration svd) {
|
||||||
|
return svd.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (def instanceof Assignment assign) {
|
||||||
|
if (assign.getLeftHandSide() instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression getDefinitionExpression(ASTNode def) {
|
||||||
|
if (def instanceof VariableDeclarationFragment frag) {
|
||||||
|
return frag.getInitializer();
|
||||||
|
}
|
||||||
|
if (def instanceof Assignment assign) {
|
||||||
|
return assign.getRightHandSide();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -88,4 +89,42 @@ public class ResolutionRobustnessTest {
|
|||||||
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
|
assertThat(context.getTypeDeclaration("p1.X")).isNotNull();
|
||||||
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
|
assertThat(context.getTypeDeclaration("p2.X")).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testJdtBindingsResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Base.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Base {\n" +
|
||||||
|
" public void run() {}\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
Files.writeString(comFoo.resolve("Child.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Child extends Base {\n" +
|
||||||
|
" public void execute() {\n" +
|
||||||
|
" run();\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
|
||||||
|
assertThat(childTd).isNotNull();
|
||||||
|
|
||||||
|
// Resolve the binding for Child class
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding binding = childTd.resolveBinding();
|
||||||
|
assertThat(binding).isNotNull();
|
||||||
|
assertThat(binding.getQualifiedName()).isEqualTo("com.foo.Child");
|
||||||
|
|
||||||
|
// Verify we can get the superclass binding
|
||||||
|
org.eclipse.jdt.core.dom.ITypeBinding superBinding = binding.getSuperclass();
|
||||||
|
assertThat(superBinding).isNotNull();
|
||||||
|
assertThat(superBinding.getQualifiedName()).isEqualTo("com.foo.Base");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
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 BindingResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBindingResolverMethods(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Base.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public interface Base {\n" +
|
||||||
|
" void run();\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
Files.writeString(comFoo.resolve("Child.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Child implements Base {\n" +
|
||||||
|
" public void run() {}\n" +
|
||||||
|
" public void execute() {\n" +
|
||||||
|
" Child c = new Child();\n" +
|
||||||
|
" c.run();\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration childTd = context.getTypeDeclaration("com.foo.Child");
|
||||||
|
assertThat(childTd).isNotNull();
|
||||||
|
|
||||||
|
ITypeBinding binding = BindingResolver.resolveType(childTd);
|
||||||
|
assertThat(binding).isNotNull();
|
||||||
|
assertThat(BindingResolver.getFullyQualifiedName(binding)).isEqualTo("com.foo.Child");
|
||||||
|
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.Base")).isTrue();
|
||||||
|
assertThat(BindingResolver.isSubtypeOf(binding, "java.lang.Object")).isTrue();
|
||||||
|
assertThat(BindingResolver.isSubtypeOf(binding, "com.foo.NonExistent")).isFalse();
|
||||||
|
|
||||||
|
// Find MethodDeclaration execute
|
||||||
|
MethodDeclaration executeMethod = null;
|
||||||
|
for (MethodDeclaration md : childTd.getMethods()) {
|
||||||
|
if (md.getName().getIdentifier().equals("execute")) {
|
||||||
|
executeMethod = md;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(executeMethod).isNotNull();
|
||||||
|
|
||||||
|
// Traverse the body to find ClassInstanceCreation and MethodInvocation
|
||||||
|
class Visitor extends ASTVisitor {
|
||||||
|
ClassInstanceCreation cic = null;
|
||||||
|
MethodInvocation mi = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ClassInstanceCreation node) {
|
||||||
|
cic = node;
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
mi = node;
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Visitor visitor = new Visitor();
|
||||||
|
executeMethod.accept(visitor);
|
||||||
|
|
||||||
|
assertThat(visitor.cic).isNotNull();
|
||||||
|
IMethodBinding constrBinding = BindingResolver.resolveConstructor(visitor.cic);
|
||||||
|
assertThat(constrBinding).isNotNull();
|
||||||
|
assertThat(constrBinding.isConstructor()).isTrue();
|
||||||
|
|
||||||
|
assertThat(visitor.mi).isNotNull();
|
||||||
|
IMethodBinding methodBinding = BindingResolver.resolveMethod(visitor.mi);
|
||||||
|
assertThat(methodBinding).isNotNull();
|
||||||
|
assertThat(methodBinding.getName()).isEqualTo("run");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
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 ControlFlowGraphTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSequentialAndBranchingCfg(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" int a = 1;\n" +
|
||||||
|
" if (flag) {\n" +
|
||||||
|
" a = 2;\n" +
|
||||||
|
" } else {\n" +
|
||||||
|
" a = 3;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" int b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
ControlFlowGraph cfg = CfgBuilder.build(md);
|
||||||
|
|
||||||
|
assertThat(cfg).isNotNull();
|
||||||
|
assertThat(cfg.getEntryNode().getSuccessors()).hasSize(1);
|
||||||
|
|
||||||
|
// Entry goes to first statement: 'int a = 1;'
|
||||||
|
ControlFlowGraph.CfgNode firstStmtNode = cfg.getEntryNode().getSuccessors().iterator().next();
|
||||||
|
assertThat(firstStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
|
||||||
|
|
||||||
|
// First stmt goes to condition of 'if (flag)'
|
||||||
|
assertThat(firstStmtNode.getSuccessors()).hasSize(1);
|
||||||
|
ControlFlowGraph.CfgNode condNode = firstStmtNode.getSuccessors().iterator().next();
|
||||||
|
assertThat(condNode.getAstNode()).isInstanceOf(SimpleName.class); // flag
|
||||||
|
|
||||||
|
// Cond node goes to two branches: 'a = 2;' and 'a = 3;'
|
||||||
|
assertThat(condNode.getSuccessors()).hasSize(2);
|
||||||
|
|
||||||
|
for (ControlFlowGraph.CfgNode branchSuccessor : condNode.getSuccessors()) {
|
||||||
|
assertThat(branchSuccessor.getAstNode()).isInstanceOf(ExpressionStatement.class); // assignment expression statement
|
||||||
|
|
||||||
|
// Each branch successor should flow into the next sequential statement: 'int b = a;'
|
||||||
|
assertThat(branchSuccessor.getSuccessors()).hasSize(1);
|
||||||
|
ControlFlowGraph.CfgNode nextStmtNode = branchSuccessor.getSuccessors().iterator().next();
|
||||||
|
assertThat(nextStmtNode.getAstNode()).isInstanceOf(VariableDeclarationStatement.class);
|
||||||
|
assertThat(nextStmtNode.getSuccessors()).contains(cfg.getExitNode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
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 JdtDataFlowModelTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRecursiveJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" String a = \"VAL1\";\n" +
|
||||||
|
" String b = a;\n" +
|
||||||
|
" String c = b;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
VariableDeclarationStatement cStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment cFrag = (VariableDeclarationFragment) cStmt.fragments().get(0);
|
||||||
|
Expression initializer = cFrag.getInitializer(); // b
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
|
||||||
|
List<Expression> defs = model.getReachingDefinitions(initializer);
|
||||||
|
assertThat(defs).isNotEmpty();
|
||||||
|
|
||||||
|
Expression singleDef = defs.get(defs.size() - 1);
|
||||||
|
assertThat(singleDef).isInstanceOf(StringLiteral.class);
|
||||||
|
assertThat(((StringLiteral) singleDef).getLiteralValue()).isEqualTo("VAL1");
|
||||||
|
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
assertThat(val).isEqualTo("VAL1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBranchDivergenceJdtDataFlowResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" String a = \"SAME\";\n" +
|
||||||
|
" if (flag) {\n" +
|
||||||
|
" a = \"SAME\";\n" +
|
||||||
|
" } else {\n" +
|
||||||
|
" a = \"SAME\";\n" +
|
||||||
|
" }\n" +
|
||||||
|
" String b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = bFrag.getInitializer(); // a
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
// Both branches assign "SAME", so it converges to "SAME"
|
||||||
|
assertThat(val).isEqualTo("SAME");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
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 LegacyDataFlowModelTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLegacyDataFlowResolution(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" String a = \"VAL1\";\n" +
|
||||||
|
" String b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(1);
|
||||||
|
VariableDeclarationFragment frag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = frag.getInitializer(); // 'a' reference
|
||||||
|
|
||||||
|
VariableTracer tracer = new VariableTracer(context, new ConstantResolver());
|
||||||
|
DataFlowModel model = new LegacyDataFlowModel(tracer);
|
||||||
|
|
||||||
|
List<Expression> defs = model.getReachingDefinitions(initializer);
|
||||||
|
assertThat(defs).isNotEmpty();
|
||||||
|
|
||||||
|
Expression resolvedExpr = defs.get(defs.size() - 1);
|
||||||
|
assertThat(resolvedExpr).isInstanceOf(StringLiteral.class);
|
||||||
|
assertThat(((StringLiteral) resolvedExpr).getLiteralValue()).isEqualTo("VAL1");
|
||||||
|
|
||||||
|
String val = model.resolveValue(initializer, context);
|
||||||
|
assertThat(val).isEqualTo("VAL1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
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.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class ReachingDefinitionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReachingDefinitionsBranchMerge(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run(boolean flag) {\n" +
|
||||||
|
" int a = 1;\n" +
|
||||||
|
" if (flag) {\n" +
|
||||||
|
" a = 2;\n" +
|
||||||
|
" } else {\n" +
|
||||||
|
" a = 3;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" int b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
ControlFlowGraph cfg = CfgBuilder.build(md);
|
||||||
|
ReachingDefinitions rd = new ReachingDefinitions(cfg);
|
||||||
|
|
||||||
|
// Find usage 'a' in 'int b = a;'
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = bFrag.getInitializer(); // simpleName 'a'
|
||||||
|
|
||||||
|
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
|
||||||
|
assertThat(defs).hasSize(2);
|
||||||
|
|
||||||
|
Set<String> values = defs.stream()
|
||||||
|
.map(ReachingDefinitions::getDefinitionExpression)
|
||||||
|
.map(expr -> {
|
||||||
|
if (expr instanceof NumberLiteral nl) {
|
||||||
|
return nl.getToken();
|
||||||
|
}
|
||||||
|
return expr.toString();
|
||||||
|
})
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
assertThat(values).containsExactlyInAnyOrder("2", "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReachingDefinitionsKilled(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path comFoo = tempDir.resolve("com/foo");
|
||||||
|
Files.createDirectories(comFoo);
|
||||||
|
Files.writeString(comFoo.resolve("Test.java"),
|
||||||
|
"package com.foo;\n" +
|
||||||
|
"public class Test {\n" +
|
||||||
|
" public void run() {\n" +
|
||||||
|
" int a = 1;\n" +
|
||||||
|
" a = 2;\n" +
|
||||||
|
" int b = a;\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.foo.Test");
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
|
||||||
|
MethodDeclaration md = td.getMethods()[0];
|
||||||
|
ControlFlowGraph cfg = CfgBuilder.build(md);
|
||||||
|
ReachingDefinitions rd = new ReachingDefinitions(cfg);
|
||||||
|
|
||||||
|
// Find usage 'a' in 'int b = a;'
|
||||||
|
VariableDeclarationStatement bStmt = (VariableDeclarationStatement) md.getBody().statements().get(2);
|
||||||
|
VariableDeclarationFragment bFrag = (VariableDeclarationFragment) bStmt.fragments().get(0);
|
||||||
|
Expression initializer = bFrag.getInitializer();
|
||||||
|
|
||||||
|
Set<ASTNode> defs = rd.getReachingDefinitions(initializer, "a");
|
||||||
|
assertThat(defs).hasSize(1);
|
||||||
|
|
||||||
|
ASTNode singleDef = defs.iterator().next();
|
||||||
|
Expression defExpr = ReachingDefinitions.getDefinitionExpression(singleDef);
|
||||||
|
assertThat(defExpr).isInstanceOf(NumberLiteral.class);
|
||||||
|
assertThat(((NumberLiteral) defExpr).getToken()).isEqualTo("2");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user