Unify getter and path-binding resolution through anchored dataflow.
Route chained getters, path bindings, and setter-before-getter reads through JdtDataFlowModel instead of parallel mini-interpreters, with regression tests covering interface hops, anonymous classes, and anchored vs detached AST resolution. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,6 +14,10 @@ public final class LibraryUnwrapRegistry {
|
|||||||
private static final Set<String> UNWRAP_METHOD_NAMES = Set.of(
|
private static final Set<String> UNWRAP_METHOD_NAMES = Set.of(
|
||||||
"of", "ofEntries", "asList", "entry", "just", "withPayload", "success");
|
"of", "ofEntries", "asList", "entry", "just", "withPayload", "success");
|
||||||
|
|
||||||
|
private static final Set<String> REACTIVE_FACTORY_METHODS = Set.of("just", "withPayload", "success");
|
||||||
|
|
||||||
|
private static final Set<String> REACTIVE_TRANSFORM_METHODS = Set.of("map", "flatMap", "switchMap", "concatMap");
|
||||||
|
|
||||||
private static final String[] STOP_UNWRAP_PREFIXES = {
|
private static final String[] STOP_UNWRAP_PREFIXES = {
|
||||||
"map", "parse", "convert", "to", "resolve", "build", "create", "from",
|
"map", "parse", "convert", "to", "resolve", "build", "create", "from",
|
||||||
"transform", "translate", "derive", "determine", "calculate", "decode",
|
"transform", "translate", "derive", "determine", "calculate", "decode",
|
||||||
@@ -31,6 +35,14 @@ public final class LibraryUnwrapRegistry {
|
|||||||
return methodName != null && UNWRAP_METHOD_NAMES.contains(methodName);
|
return methodName != null && UNWRAP_METHOD_NAMES.contains(methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isReactiveFactoryMethod(String methodName) {
|
||||||
|
return methodName != null && REACTIVE_FACTORY_METHODS.contains(methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isReactiveTransformMethod(String methodName) {
|
||||||
|
return methodName != null && REACTIVE_TRANSFORM_METHODS.contains(methodName);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean shouldStopUnwrap(String methodName) {
|
public static boolean shouldStopUnwrap(String methodName) {
|
||||||
if (methodName == null) {
|
if (methodName == null) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.Block;
|
import org.eclipse.jdt.core.dom.Block;
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
@@ -10,16 +11,11 @@ import org.eclipse.jdt.core.dom.ReturnStatement;
|
|||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
import org.eclipse.jdt.core.dom.SimpleName;
|
||||||
import org.eclipse.jdt.core.dom.ASTNode;
|
import org.eclipse.jdt.core.dom.ASTNode;
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts terminal payload literals from reactive factory chains ({@code Mono.just}, nested transforms).
|
* Extracts terminal payload literals from reactive factory chains ({@code Mono.just}, nested transforms).
|
||||||
*/
|
*/
|
||||||
public final class ReactiveExpressionSupport {
|
public final class ReactiveExpressionSupport {
|
||||||
|
|
||||||
private static final Set<String> REACTIVE_FACTORY_METHODS = Set.of("just", "withPayload", "success");
|
|
||||||
private static final Set<String> REACTIVE_TRANSFORM_METHODS = Set.of("map", "flatMap", "switchMap", "concatMap");
|
|
||||||
|
|
||||||
private ReactiveExpressionSupport() {
|
private ReactiveExpressionSupport() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +23,45 @@ public final class ReactiveExpressionSupport {
|
|||||||
return extractPayload(expression, null, constantResolver, context);
|
return extractPayload(expression, null, constantResolver, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peels {@code just}/{@code withPayload}/{@code success} wrappers on an invocation chain and returns
|
||||||
|
* the payload {@link Expression} for dataflow analysis.
|
||||||
|
*/
|
||||||
|
public static Expression peelFactoryPayloadExpression(Expression expression) {
|
||||||
|
if (!(expression instanceof MethodInvocation mi)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MethodInvocation current = mi;
|
||||||
|
while (current != null) {
|
||||||
|
String methodName = current.getName().getIdentifier();
|
||||||
|
if (LibraryUnwrapRegistry.isReactiveFactoryMethod(methodName) && !current.arguments().isEmpty()) {
|
||||||
|
return MethodInvocationUnwrapper.peelExpression((Expression) current.arguments().get(0));
|
||||||
|
}
|
||||||
|
Expression receiver = current.getExpression();
|
||||||
|
if (receiver instanceof MethodInvocation nextMi) {
|
||||||
|
current = nextMi;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression lambdaBodyExpression(LambdaExpression lambda) {
|
||||||
|
if (lambda.getBody() instanceof Expression bodyExpression) {
|
||||||
|
return bodyExpression;
|
||||||
|
}
|
||||||
|
if (lambda.getBody() instanceof Block block) {
|
||||||
|
for (Object statement : block.statements()) {
|
||||||
|
if (statement instanceof ReturnStatement returnStatement
|
||||||
|
&& returnStatement.getExpression() != null) {
|
||||||
|
return returnStatement.getExpression();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewrites {@code p.getEvent()} inside a reactive transform lambda to {@code payload.getEvent()}
|
* Rewrites {@code p.getEvent()} inside a reactive transform lambda to {@code payload.getEvent()}
|
||||||
* when {@code p} is fed by {@code Mono.just(payload)} (or a chained reactive source).
|
* when {@code p} is fed by {@code Mono.just(payload)} (or a chained reactive source).
|
||||||
@@ -70,7 +105,7 @@ public final class ReactiveExpressionSupport {
|
|||||||
String methodFqn,
|
String methodFqn,
|
||||||
ConstantResolver constantResolver,
|
ConstantResolver constantResolver,
|
||||||
CodebaseContext context) {
|
CodebaseContext context) {
|
||||||
MethodInvocation getter = findMethodInvocationInMethod(methodFqn, expressionText, context);
|
MethodInvocation getter = AstUtils.findMethodInvocationInMethod(methodFqn, expressionText, context);
|
||||||
if (getter == null) {
|
if (getter == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -81,37 +116,6 @@ public final class ReactiveExpressionSupport {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MethodInvocation findMethodInvocationInMethod(
|
|
||||||
String methodFqn,
|
|
||||||
String expressionText,
|
|
||||||
CodebaseContext context) {
|
|
||||||
if (methodFqn == null || expressionText == null || !methodFqn.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
org.eclipse.jdt.core.dom.MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (md == null || md.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final MethodInvocation[] match = new MethodInvocation[1];
|
|
||||||
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (expressionText.equals(node.toString())) {
|
|
||||||
match[0] = node;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LambdaExpression findEnclosingLambda(ASTNode node) {
|
private static LambdaExpression findEnclosingLambda(ASTNode node) {
|
||||||
ASTNode current = node.getParent();
|
ASTNode current = node.getParent();
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
@@ -127,7 +131,7 @@ public final class ReactiveExpressionSupport {
|
|||||||
ASTNode current = node.getParent();
|
ASTNode current = node.getParent();
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
if (current instanceof MethodInvocation mi
|
if (current instanceof MethodInvocation mi
|
||||||
&& REACTIVE_TRANSFORM_METHODS.contains(mi.getName().getIdentifier())) {
|
&& LibraryUnwrapRegistry.isReactiveTransformMethod(mi.getName().getIdentifier())) {
|
||||||
return mi;
|
return mi;
|
||||||
}
|
}
|
||||||
current = current.getParent();
|
current = current.getParent();
|
||||||
@@ -145,14 +149,14 @@ public final class ReactiveExpressionSupport {
|
|||||||
}
|
}
|
||||||
if (expression instanceof MethodInvocation mi) {
|
if (expression instanceof MethodInvocation mi) {
|
||||||
String methodName = mi.getName().getIdentifier();
|
String methodName = mi.getName().getIdentifier();
|
||||||
if (REACTIVE_TRANSFORM_METHODS.contains(methodName) && !mi.arguments().isEmpty()) {
|
if (LibraryUnwrapRegistry.isReactiveTransformMethod(methodName) && !mi.arguments().isEmpty()) {
|
||||||
String fromArgument = extractTransformArgumentPayload(
|
String fromArgument = extractTransformArgumentPayload(
|
||||||
(Expression) mi.arguments().get(0), mi.getExpression(), constantResolver, context);
|
(Expression) mi.arguments().get(0), mi.getExpression(), constantResolver, context);
|
||||||
if (fromArgument != null) {
|
if (fromArgument != null) {
|
||||||
return fromArgument;
|
return fromArgument;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (REACTIVE_FACTORY_METHODS.contains(methodName) && !mi.arguments().isEmpty()) {
|
if (LibraryUnwrapRegistry.isReactiveFactoryMethod(methodName) && !mi.arguments().isEmpty()) {
|
||||||
Expression arg = (Expression) mi.arguments().get(0);
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
if (constantResolver != null && context != null) {
|
if (constantResolver != null && context != null) {
|
||||||
String resolved = constantResolver.resolve(arg, context);
|
String resolved = constantResolver.resolve(arg, context);
|
||||||
@@ -197,7 +201,7 @@ public final class ReactiveExpressionSupport {
|
|||||||
if (!(bodyExpression instanceof MethodInvocation factoryCall)) {
|
if (!(bodyExpression instanceof MethodInvocation factoryCall)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!REACTIVE_FACTORY_METHODS.contains(factoryCall.getName().getIdentifier())
|
if (!LibraryUnwrapRegistry.isReactiveFactoryMethod(factoryCall.getName().getIdentifier())
|
||||||
|| factoryCall.arguments().isEmpty()) {
|
|| factoryCall.arguments().isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -237,7 +241,7 @@ public final class ReactiveExpressionSupport {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (transformReceiver instanceof MethodInvocation receiverTransform
|
if (transformReceiver instanceof MethodInvocation receiverTransform
|
||||||
&& REACTIVE_TRANSFORM_METHODS.contains(receiverTransform.getName().getIdentifier())
|
&& LibraryUnwrapRegistry.isReactiveTransformMethod(receiverTransform.getName().getIdentifier())
|
||||||
&& !receiverTransform.arguments().isEmpty()
|
&& !receiverTransform.arguments().isEmpty()
|
||||||
&& receiverTransform.arguments().get(0) instanceof LambdaExpression feederLambda
|
&& receiverTransform.arguments().get(0) instanceof LambdaExpression feederLambda
|
||||||
&& feederLambda != lambda) {
|
&& feederLambda != lambda) {
|
||||||
@@ -287,29 +291,13 @@ public final class ReactiveExpressionSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Expression peelJustArgument(Expression expression) {
|
private static Expression peelJustArgument(Expression expression) {
|
||||||
if (expression instanceof MethodInvocation mi
|
Expression peeled = peelFactoryPayloadExpression(expression);
|
||||||
&& REACTIVE_FACTORY_METHODS.contains(mi.getName().getIdentifier())
|
if (peeled != null) {
|
||||||
&& !mi.arguments().isEmpty()) {
|
return peeled;
|
||||||
return (Expression) mi.arguments().get(0);
|
|
||||||
}
|
}
|
||||||
if (expression instanceof MethodInvocation mi && mi.getExpression() != null) {
|
if (expression instanceof MethodInvocation mi && mi.getExpression() != null) {
|
||||||
return peelJustArgument(mi.getExpression());
|
return peelJustArgument(mi.getExpression());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression lambdaBodyExpression(LambdaExpression lambda) {
|
|
||||||
if (lambda.getBody() instanceof Expression bodyExpression) {
|
|
||||||
return bodyExpression;
|
|
||||||
}
|
|
||||||
if (lambda.getBody() instanceof Block block) {
|
|
||||||
for (Object statement : block.statements()) {
|
|
||||||
if (statement instanceof ReturnStatement returnStatement
|
|
||||||
&& returnStatement.getExpression() != null) {
|
|
||||||
return returnStatement.getExpression();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
@@ -850,7 +849,7 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String classFqn : classesFromCurrentCallPath()) {
|
for (String classFqn : classesFromCurrentCallPath(context)) {
|
||||||
TypeDeclaration pathTd = context.getTypeDeclaration(classFqn);
|
TypeDeclaration pathTd = context.getTypeDeclaration(classFqn);
|
||||||
if (pathTd != null) {
|
if (pathTd != null) {
|
||||||
String result = resolveFieldInType(pathTd, sn.getIdentifier(), classFqn, context, visited);
|
String result = resolveFieldInType(pathTd, sn.getIdentifier(), classFqn, context, visited);
|
||||||
@@ -922,7 +921,7 @@ public class ConstantResolver {
|
|||||||
if (td != null) {
|
if (td != null) {
|
||||||
return td;
|
return td;
|
||||||
}
|
}
|
||||||
String entryClass = getCurrentCallPathEntryClass();
|
String entryClass = getCurrentCallPathEntryClass(context);
|
||||||
if (entryClass == null) {
|
if (entryClass == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -934,15 +933,15 @@ public class ConstantResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private TypeDeclaration typeFromCurrentCallPath(CodebaseContext context) {
|
private TypeDeclaration typeFromCurrentCallPath(CodebaseContext context) {
|
||||||
List<String> classes = classesFromCurrentCallPath();
|
List<String> classes = classesFromCurrentCallPath(context);
|
||||||
if (classes.isEmpty()) {
|
if (classes.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return context.getTypeDeclaration(classes.get(0));
|
return context.getTypeDeclaration(classes.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> classesFromCurrentCallPath() {
|
private List<String> classesFromCurrentCallPath(CodebaseContext context) {
|
||||||
List<String> path = JdtDataFlowModel.getCurrentPath();
|
List<String> path = context.getAnalysisCallPath();
|
||||||
if (path == null || path.isEmpty()) {
|
if (path == null || path.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
@@ -960,8 +959,8 @@ public class ConstantResolver {
|
|||||||
return classes;
|
return classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getCurrentCallPathEntryClass() {
|
private String getCurrentCallPathEntryClass(CodebaseContext context) {
|
||||||
List<String> classes = classesFromCurrentCallPath();
|
List<String> classes = classesFromCurrentCallPath(context);
|
||||||
return classes.isEmpty() ? null : classes.get(classes.size() - 1);
|
return classes.isEmpty() ? null : classes.get(classes.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,36 +37,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
protected Map<String, List<CallEdge>> graph;
|
protected Map<String, List<CallEdge>> graph;
|
||||||
|
|
||||||
private ASTNode parseExpressionString(String expr) {
|
private ASTNode parseExpressionString(String expr) {
|
||||||
if (expr == null) return null;
|
if (expr == null) {
|
||||||
return parsedNodeCache.computeIfAbsent(expr, e -> {
|
return null;
|
||||||
ASTParser parser = ASTParser.newParser(AST.JLS17);
|
}
|
||||||
Map<String, String> options = org.eclipse.jdt.core.JavaCore.getOptions();
|
return parsedNodeCache.computeIfAbsent(expr, click.kamil.springstatemachineexporter.ast.common.AstUtils::parseExpression);
|
||||||
org.eclipse.jdt.core.JavaCore.setComplianceOptions(org.eclipse.jdt.core.JavaCore.VERSION_17, options);
|
|
||||||
parser.setCompilerOptions(options);
|
|
||||||
parser.setKind(ASTParser.K_EXPRESSION);
|
|
||||||
parser.setSource(e.toCharArray());
|
|
||||||
try {
|
|
||||||
ASTNode node = parser.createAST(null);
|
|
||||||
if (node instanceof org.eclipse.jdt.core.dom.CompilationUnit) {
|
|
||||||
ASTParser fallbackParser = ASTParser.newParser(AST.JLS17);
|
|
||||||
fallbackParser.setCompilerOptions(options);
|
|
||||||
fallbackParser.setKind(ASTParser.K_COMPILATION_UNIT);
|
|
||||||
fallbackParser.setSource(("class A { Object o = " + e + "; }").toCharArray());
|
|
||||||
org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) fallbackParser.createAST(null);
|
|
||||||
if (!cu.types().isEmpty()) {
|
|
||||||
org.eclipse.jdt.core.dom.TypeDeclaration td = (org.eclipse.jdt.core.dom.TypeDeclaration) cu.types().get(0);
|
|
||||||
if (!td.bodyDeclarations().isEmpty() && td.bodyDeclarations().get(0) instanceof org.eclipse.jdt.core.dom.FieldDeclaration fd) {
|
|
||||||
if (!fd.fragments().isEmpty() && fd.fragments().get(0) instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment vdf) {
|
|
||||||
return vdf.getInitializer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
@@ -258,7 +232,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
Map<String, List<CallEdge>> callGraph,
|
Map<String, List<CallEdge>> callGraph,
|
||||||
String[] finalParamNameRef,
|
String[] finalParamNameRef,
|
||||||
Map<String, String> initialBindings) {
|
Map<String, String> initialBindings) {
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(path);
|
click.kamil.springstatemachineexporter.ast.common.CodebaseContext contextRef = context;
|
||||||
|
contextRef.setAnalysisCallPath(path);
|
||||||
final boolean debug = log.isDebugEnabled();
|
final boolean debug = log.isDebugEnabled();
|
||||||
try {
|
try {
|
||||||
String event = tp.getEvent();
|
String event = tp.getEvent();
|
||||||
@@ -1171,7 +1146,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
return tp;
|
return tp;
|
||||||
} finally {
|
} finally {
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.clearCurrentPath();
|
context.clearAnalysisCallPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1207,6 +1182,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (expr instanceof LambdaExpression) {
|
||||||
|
String lambdaText = expr.toString();
|
||||||
|
String val = constantResolver.resolve(expr, context);
|
||||||
|
return val != null ? val : lambdaText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expr instanceof ExpressionMethodReference emr) {
|
if (expr instanceof ExpressionMethodReference emr) {
|
||||||
@@ -1854,6 +1834,47 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
|
String expressionText = click.kamil.springstatemachineexporter.ast.common.AstUtils.combineExpressionText(
|
||||||
|
resolvedValue, methodSuffix);
|
||||||
|
List<String> searchMethods = buildGetterSearchMethods(resolvedValue, entryMethod, path);
|
||||||
|
|
||||||
|
List<String> constants = resolveGetterConstantsViaDataflow(expressionText, searchMethods, path, callGraph, entryMethod);
|
||||||
|
if (!constants.isEmpty()) {
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String scopeMethod : searchMethods) {
|
||||||
|
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
|
||||||
|
expressionText, scopeMethod, constantResolver, context);
|
||||||
|
if (remapped != null && !remapped.equals(expressionText)) {
|
||||||
|
constants = resolveGetterConstantsViaDataflow(remapped, searchMethods, path, callGraph, entryMethod);
|
||||||
|
if (!constants.isEmpty()) {
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolveGetterConstantsViaAccessorPipeline(resolvedValue, methodSuffix, entryMethod, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveGetterConstantsViaDataflow(
|
||||||
|
String expressionText,
|
||||||
|
List<String> searchMethods,
|
||||||
|
List<String> path,
|
||||||
|
Map<String, List<CallEdge>> callGraph,
|
||||||
|
String entryMethod) {
|
||||||
|
Expression anchored = click.kamil.springstatemachineexporter.ast.common.AstUtils.findExpressionInMethods(
|
||||||
|
searchMethods, expressionText, context);
|
||||||
|
if (anchored != null) {
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
addConstantsFromTracedExpression(anchored, constants, path, callGraph, entryMethod);
|
||||||
|
if (!constants.isEmpty()) {
|
||||||
|
return constants.stream().distinct().toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return variableTracer.resolveConstantsFromSourceText(expressionText, searchMethods, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveGetterConstantsViaAccessorPipeline(String resolvedValue, String methodSuffix, String entryMethod, List<String> path) {
|
||||||
ASTNode exprNode = parseExpressionString(resolvedValue);
|
ASTNode exprNode = parseExpressionString(resolvedValue);
|
||||||
while (exprNode instanceof ParenthesizedExpression pe) {
|
while (exprNode instanceof ParenthesizedExpression pe) {
|
||||||
exprNode = pe.getExpression();
|
exprNode = pe.getExpression();
|
||||||
@@ -1954,7 +1975,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
mi.toString(), scopeMethod, constantResolver, context);
|
mi.toString(), scopeMethod, constantResolver, context);
|
||||||
}
|
}
|
||||||
if (remapped != null) {
|
if (remapped != null) {
|
||||||
return evaluateGetterOnReceiver(remapped, "", entryMethod, path, callGraph);
|
return evaluateGetterOnReceiver(remapped, "", entryMethod, path, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2057,6 +2078,35 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> buildGetterSearchMethods(String resolvedValue, String entryMethod, List<String> path) {
|
||||||
|
java.util.LinkedHashSet<String> methods = new java.util.LinkedHashSet<>();
|
||||||
|
methods.add(resolveScopeMethodForResolvedValue(resolvedValue, entryMethod, path));
|
||||||
|
if (path != null) {
|
||||||
|
methods.addAll(path);
|
||||||
|
}
|
||||||
|
if (entryMethod != null) {
|
||||||
|
methods.add(entryMethod);
|
||||||
|
}
|
||||||
|
return List.copyOf(methods);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveScopeMethodForResolvedValue(String resolvedValue, String entryMethod, List<String> path) {
|
||||||
|
ASTNode node = parseExpressionString(resolvedValue);
|
||||||
|
while (node instanceof ParenthesizedExpression pe) {
|
||||||
|
node = pe.getExpression();
|
||||||
|
}
|
||||||
|
if (node instanceof Expression expression) {
|
||||||
|
Expression root = expression;
|
||||||
|
while (root instanceof MethodInvocation mi) {
|
||||||
|
root = mi.getExpression();
|
||||||
|
}
|
||||||
|
if (root instanceof SimpleName sn) {
|
||||||
|
return resolveScopeMethodForVariable(sn.getIdentifier(), entryMethod, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entryMethod;
|
||||||
|
}
|
||||||
|
|
||||||
protected Expression findVariableInitializer(String methodFqn, String varName) {
|
protected Expression findVariableInitializer(String methodFqn, String varName) {
|
||||||
int lastDot = methodFqn.lastIndexOf('.');
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
if (lastDot < 0) return null;
|
if (lastDot < 0) return null;
|
||||||
@@ -3180,6 +3230,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (tp.isExternal() && hasConcrete) {
|
if (tp.isExternal() && hasConcrete) {
|
||||||
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||||
}
|
}
|
||||||
|
narrowed.removeIf(pe -> pe != null && (pe.equals("*>") || pe.equals(".*>")));
|
||||||
return narrowed;
|
return narrowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.IMethodBinding;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches {@link CallEdge}s to concrete {@link MethodInvocation}s in caller source when
|
||||||
|
* multiple call sites target the same method.
|
||||||
|
*/
|
||||||
|
public final class CallSiteMatcher {
|
||||||
|
|
||||||
|
private static final Pattern ENUM_LITERAL = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*\\.[A-Z_][A-Z0-9_]*");
|
||||||
|
|
||||||
|
private CallSiteMatcher() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CallEdge selectEdge(
|
||||||
|
String caller,
|
||||||
|
String target,
|
||||||
|
List<CallEdge> edges,
|
||||||
|
Map<String, String> bindings) {
|
||||||
|
if (edges == null || edges.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<CallEdge> matching = new ArrayList<>();
|
||||||
|
for (CallEdge edge : edges) {
|
||||||
|
if (edge.getTargetMethod().equals(target) || heuristicTargetMatch(edge.getTargetMethod(), target)) {
|
||||||
|
matching.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matching.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (matching.size() == 1) {
|
||||||
|
return matching.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CallEdge> constraintFiltered = matching;
|
||||||
|
if (bindings != null && !bindings.isEmpty()) {
|
||||||
|
List<CallEdge> compatible = new ArrayList<>();
|
||||||
|
for (CallEdge edge : matching) {
|
||||||
|
String constraint = edge.getConstraint();
|
||||||
|
if (constraint == null
|
||||||
|
|| constraint.isBlank()
|
||||||
|
|| BooleanConstraintEvaluator.isCompatibleWithBindings(constraint, bindings)) {
|
||||||
|
compatible.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!compatible.isEmpty()) {
|
||||||
|
constraintFiltered = compatible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (constraintFiltered.size() == 1) {
|
||||||
|
return constraintFiltered.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindings != null && !bindings.isEmpty()) {
|
||||||
|
for (CallEdge edge : constraintFiltered) {
|
||||||
|
if (edge.getArguments() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String edgeArg : edge.getArguments()) {
|
||||||
|
for (String bindingValue : bindings.values()) {
|
||||||
|
if (edgeArg == null || bindingValue == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (edgeArg.contains(bindingValue) || bindingValue.contains(edgeArg)) {
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CallEdge edge : constraintFiltered) {
|
||||||
|
if (findMatchingInvocation(caller, target, edge, null) != null) {
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return constraintFiltered.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MethodInvocation findMatchingInvocation(
|
||||||
|
String caller,
|
||||||
|
String callee,
|
||||||
|
CallEdge edge,
|
||||||
|
CodebaseContext context) {
|
||||||
|
List<MethodInvocation> invocations = findCallInvocations(caller, callee, context);
|
||||||
|
if (invocations.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (edge == null || invocations.size() == 1) {
|
||||||
|
return invocations.get(0);
|
||||||
|
}
|
||||||
|
for (MethodInvocation invocation : invocations) {
|
||||||
|
if (invocationMatchesEdge(invocation, edge)) {
|
||||||
|
return invocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression findCallSiteArgumentExpression(
|
||||||
|
String caller,
|
||||||
|
String callee,
|
||||||
|
int paramIndex,
|
||||||
|
CallEdge edge,
|
||||||
|
CodebaseContext context) {
|
||||||
|
MethodInvocation invocation = findMatchingInvocation(caller, callee, edge, context);
|
||||||
|
if (invocation == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (paramIndex < 0 || paramIndex >= invocation.arguments().size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (Expression) invocation.arguments().get(paramIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MethodInvocation> findCallInvocations(String caller, String callee, CodebaseContext context) {
|
||||||
|
if (caller == null || callee == null || !caller.contains(".") || !callee.contains(".") || context == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
String callerClass = caller.substring(0, caller.lastIndexOf('.'));
|
||||||
|
String callerMethod = caller.substring(caller.lastIndexOf('.') + 1);
|
||||||
|
String calleeMethod = callee.substring(callee.lastIndexOf('.') + 1);
|
||||||
|
String calleeClass = callee.substring(0, callee.lastIndexOf('.'));
|
||||||
|
TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
|
||||||
|
if (callerType == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
MethodDeclaration method = context.findMethodDeclaration(callerType, callerMethod, true);
|
||||||
|
if (method == null || method.getBody() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MethodInvocation> invocations = new ArrayList<>();
|
||||||
|
method.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (!calleeMethod.equals(node.getName().getIdentifier())) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
IMethodBinding binding = node.resolveMethodBinding();
|
||||||
|
if (binding != null && binding.getDeclaringClass() != null) {
|
||||||
|
String declaringFqn = binding.getDeclaringClass().getErasure().getQualifiedName();
|
||||||
|
if (!declaringFqn.equals(calleeClass) && !declaringFqn.endsWith("." + calleeClass)) {
|
||||||
|
String calleeSimple = calleeClass.substring(calleeClass.lastIndexOf('.') + 1);
|
||||||
|
if (!declaringFqn.endsWith("." + calleeSimple)) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invocations.add(node);
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return invocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean invocationMatchesEdge(MethodInvocation invocation, CallEdge edge) {
|
||||||
|
if (edge == null || edge.getArguments() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
List<?> astArgs = invocation.arguments();
|
||||||
|
List<String> edgeArgs = edge.getArguments();
|
||||||
|
int count = Math.min(astArgs.size(), edgeArgs.size());
|
||||||
|
if (count == 0) {
|
||||||
|
return edgeArgs.isEmpty();
|
||||||
|
}
|
||||||
|
int matched = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
if (argumentTokensMatch(((Expression) astArgs.get(i)).toString(), edgeArgs.get(i))) {
|
||||||
|
matched++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched == count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean argumentTokensMatch(String astArg, String edgeArg) {
|
||||||
|
if (astArg == null || edgeArg == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String left = normalizeArgument(astArg);
|
||||||
|
String right = normalizeArgument(edgeArg);
|
||||||
|
if (left.equals(right)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String leftEnum = extractEnumLiteral(left);
|
||||||
|
String rightEnum = extractEnumLiteral(right);
|
||||||
|
return leftEnum != null && leftEnum.equals(rightEnum);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeArgument(String value) {
|
||||||
|
return value.replaceAll("\\s+", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractEnumLiteral(String value) {
|
||||||
|
var matcher = ENUM_LITERAL.matcher(value);
|
||||||
|
return matcher.find() ? matcher.group() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean heuristicTargetMatch(String edgeTarget, String pathTarget) {
|
||||||
|
if (edgeTarget == null || pathTarget == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (edgeTarget.equals(pathTarget)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String edgeSimple = edgeTarget.substring(edgeTarget.lastIndexOf('.') + 1);
|
||||||
|
String pathSimple = pathTarget.substring(pathTarget.lastIndexOf('.') + 1);
|
||||||
|
return edgeSimple.equals(pathSimple);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,18 @@ public class ConstantExtractor {
|
|||||||
extractConstantsFromArgument((Expression) expObj, constants);
|
extractConstantsFromArgument((Expression) expObj, constants);
|
||||||
}
|
}
|
||||||
} else if (expr instanceof MethodInvocation mi) {
|
} else if (expr instanceof MethodInvocation mi) {
|
||||||
|
if (variableTracer != null) {
|
||||||
|
int sizeBefore = constants.size();
|
||||||
|
for (Expression def : variableTracer.traceVariableAll(mi)) {
|
||||||
|
if (def != null && !def.toString().equals(mi.toString())) {
|
||||||
|
extractConstantsFromExpression(def, constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (constants.size() > sizeBefore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
|
||||||
if (log.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
@@ -99,42 +111,20 @@ public class ConstantExtractor {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event")) && mi.getExpression() instanceof SimpleName sn) {
|
if ((methodName.startsWith("get") || methodName.equals("type") || methodName.equals("event"))
|
||||||
String varName = sn.getIdentifier();
|
&& mi.getExpression() instanceof SimpleName sn
|
||||||
String propName = methodName.startsWith("get") ? methodName.substring(3) : methodName;
|
&& variableTracer != null) {
|
||||||
|
org.eclipse.jdt.core.dom.MethodDeclaration md =
|
||||||
Block block = findEnclosingBlock(mi);
|
click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingMethod(mi);
|
||||||
if (block != null) {
|
TypeDeclaration td = findEnclosingType(mi);
|
||||||
for (Object stmtObj : block.statements()) {
|
if (md != null && td != null) {
|
||||||
if (stmtObj == mi.getParent() || stmtObj == mi) break;
|
String methodFqn = context.getFqn(td) + "." + md.getName().getIdentifier();
|
||||||
if (stmtObj instanceof ExpressionStatement es) {
|
Expression setterArg = variableTracer.traceLocalSetter(methodFqn, sn.getIdentifier(), methodName);
|
||||||
if (es.getExpression() instanceof MethodInvocation setterMi) {
|
if (setterArg != null) {
|
||||||
if (setterMi.getName().getIdentifier().equalsIgnoreCase("set" + propName) || setterMi.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
extractConstantsFromExpression(setterArg, constants);
|
||||||
if (setterMi.getExpression() instanceof SimpleName setterSn && setterSn.getIdentifier().equals(varName)) {
|
if (!constants.isEmpty()) {
|
||||||
if (!setterMi.arguments().isEmpty()) {
|
return;
|
||||||
extractConstantsFromExpression((Expression) setterMi.arguments().get(0), constants);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (es.getExpression() instanceof Assignment assignment) {
|
|
||||||
if (assignment.getLeftHandSide() instanceof FieldAccess fa) {
|
|
||||||
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
|
|
||||||
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
|
||||||
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (assignment.getLeftHandSide() instanceof QualifiedName qqn) {
|
|
||||||
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
|
|
||||||
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
|
|
||||||
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,14 +668,6 @@ public class ConstantExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Block findEnclosingBlock(ASTNode node) {
|
|
||||||
ASTNode parent = node.getParent();
|
|
||||||
while (parent != null && !(parent instanceof Block)) {
|
|
||||||
parent = parent.getParent();
|
|
||||||
}
|
|
||||||
return (Block) parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||||
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
return click.kamil.springstatemachineexporter.ast.common.AstUtils.findEnclosingType(node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,38 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recognizes JDK functional interface parameters used as delayed event providers
|
* Recognizes JDK functional interface parameters used as delayed event providers
|
||||||
* ({@code Supplier.get()}, etc.) in call-graph argument tracing.
|
* ({@code Supplier.get()}, {@code Runnable.run()}, {@code Consumer.accept()}, etc.)
|
||||||
|
* in call-graph argument tracing.
|
||||||
*/
|
*/
|
||||||
public final class FunctionalInterfaceTypes {
|
public final class FunctionalInterfaceTypes {
|
||||||
|
|
||||||
|
private static final Set<String> SAM_METHOD_NAMES = Set.of(
|
||||||
|
"run", "get", "call", "accept", "apply", "test");
|
||||||
|
|
||||||
private FunctionalInterfaceTypes() {
|
private FunctionalInterfaceTypes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isSamMethodName(String methodName) {
|
||||||
|
return methodName != null && SAM_METHOD_NAMES.contains(methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether {@code typeName.methodName} is a single-abstract-method invocation on a functional interface
|
||||||
|
* (e.g. {@code java.lang.Runnable.run}, {@code java.util.function.Consumer.accept}).
|
||||||
|
*/
|
||||||
|
public static boolean isFunctionalSamMethod(String methodFqn) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
|
String methodName = methodFqn.substring(lastDot + 1);
|
||||||
|
String typeName = methodFqn.substring(0, lastDot);
|
||||||
|
return isSamMethodName(methodName) && isFunctionalInterface(typeName);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isFunctionalInterface(String typeName) {
|
public static boolean isFunctionalInterface(String typeName) {
|
||||||
if (typeName == null || typeName.isBlank()) {
|
if (typeName == null || typeName.isBlank()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -17,8 +41,11 @@ public final class FunctionalInterfaceTypes {
|
|||||||
if (simple.contains("<")) {
|
if (simple.contains("<")) {
|
||||||
simple = simple.substring(0, simple.indexOf('<'));
|
simple = simple.substring(0, simple.indexOf('<'));
|
||||||
}
|
}
|
||||||
return "Supplier".equals(simple) || "Function".equals(simple) || "Callable".equals(simple)
|
return switch (simple) {
|
||||||
|| "Runnable".equals(simple);
|
case "Runnable", "Supplier", "Function", "Callable", "Consumer", "BiConsumer", "Predicate", "BiFunction" ->
|
||||||
|
true;
|
||||||
|
default -> simple.contains("Supplier") || simple.contains("Function") || simple.contains("Callable");
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isLambdaArgument(String argValue) {
|
public static boolean isLambdaArgument(String argValue) {
|
||||||
|
|||||||
@@ -130,16 +130,10 @@ public class GenericEventDetector {
|
|||||||
return reactivePayload;
|
return reactivePayload;
|
||||||
}
|
}
|
||||||
if (receiver == null) return null;
|
if (receiver == null) return null;
|
||||||
if (receiver instanceof MethodInvocation mi) {
|
Expression factoryPayload = ReactiveExpressionSupport.peelFactoryPayloadExpression(receiver);
|
||||||
String mName = mi.getName().getIdentifier();
|
if (factoryPayload != null) {
|
||||||
if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) {
|
String resolved = constantResolver.resolve(factoryPayload, context);
|
||||||
Expression arg = (Expression) mi.arguments().get(0);
|
return resolved != null ? resolved : factoryPayload.toString();
|
||||||
String resolved = constantResolver.resolve(arg, context);
|
|
||||||
return resolved != null ? resolved : arg.toString();
|
|
||||||
}
|
|
||||||
if (mi.getExpression() != null) {
|
|
||||||
return extractEventFromReceiver(mi.getExpression());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (receiver instanceof SimpleName sn) {
|
if (receiver instanceof SimpleName sn) {
|
||||||
return sn.getIdentifier();
|
return sn.getIdentifier();
|
||||||
@@ -804,36 +798,33 @@ public class GenericEventDetector {
|
|||||||
|
|
||||||
if (!(expr instanceof MethodInvocation mi)) return null;
|
if (!(expr instanceof MethodInvocation mi)) return null;
|
||||||
|
|
||||||
// Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build()
|
Expression factoryPayload = ReactiveExpressionSupport.peelFactoryPayloadExpression(mi);
|
||||||
|
if (factoryPayload != null) {
|
||||||
|
String extracted = extractEventFromMessageBuilder(factoryPayload);
|
||||||
|
if (extracted != null) {
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
String resolved = constantResolver.resolve(factoryPayload, context);
|
||||||
|
if (resolved != null) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
Expression payloadExpr = factoryPayload;
|
||||||
|
if (payloadExpr instanceof CastExpression ce) {
|
||||||
|
payloadExpr = ce.getExpression();
|
||||||
|
}
|
||||||
|
if (payloadExpr instanceof SimpleName sn) {
|
||||||
|
String traced = extractEventFromMessageBuilder(payloadExpr);
|
||||||
|
if (traced != null) {
|
||||||
|
return traced;
|
||||||
|
}
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return payloadExpr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
MethodInvocation current = mi;
|
MethodInvocation current = mi;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
String name = current.getName().getIdentifier();
|
if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
|
||||||
if (("withPayload".equals(name) || "just".equals(name)) && !current.arguments().isEmpty()) {
|
|
||||||
Expression payloadExpr = (Expression) current.arguments().get(0);
|
|
||||||
|
|
||||||
// If it's Mono.just(msg), where msg is a variable
|
|
||||||
if ("just".equals(name)) {
|
|
||||||
String extracted = extractEventFromMessageBuilder(payloadExpr);
|
|
||||||
if (extracted != null) return extracted;
|
|
||||||
}
|
|
||||||
|
|
||||||
String resolved = constantResolver.resolve(payloadExpr, context);
|
|
||||||
if (resolved != null) return resolved;
|
|
||||||
|
|
||||||
// If not a constant, it might be a parameter like 'event'
|
|
||||||
if (payloadExpr instanceof CastExpression ce) {
|
|
||||||
payloadExpr = ce.getExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payloadExpr instanceof SimpleName sn) {
|
|
||||||
String extracted = extractEventFromMessageBuilder(payloadExpr);
|
|
||||||
if (extracted != null) {
|
|
||||||
return extracted;
|
|
||||||
}
|
|
||||||
return sn.getIdentifier(); // Fall back to returning the parameter name
|
|
||||||
}
|
|
||||||
return payloadExpr.toString();
|
|
||||||
} else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
|
|
||||||
// If the event is obtained by calling a method on a provider/supplier parameter,
|
// If the event is obtained by calling a method on a provider/supplier parameter,
|
||||||
// return the provider's name so the call-graph engine can trace the lambda argument
|
// return the provider's name so the call-graph engine can trace the lambda argument
|
||||||
String traced = extractEventFromMessageBuilder(sn);
|
String traced = extractEventFromMessageBuilder(sn);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
|
||||||
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -139,21 +140,7 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Expression unwrapMessageBuilder(Expression expr) {
|
private Expression unwrapMessageBuilder(Expression expr) {
|
||||||
if (expr instanceof MethodInvocation mi) {
|
Expression peeled = ReactiveExpressionSupport.peelFactoryPayloadExpression(expr);
|
||||||
MethodInvocation current = mi;
|
return peeled != null ? peeled : expr;
|
||||||
while (current != null) {
|
|
||||||
String name = current.getName().getIdentifier();
|
|
||||||
if (("withPayload".equals(name) || "just".equals(name)) && !current.arguments().isEmpty()) {
|
|
||||||
return (Expression) current.arguments().get(0);
|
|
||||||
}
|
|
||||||
Expression receiver = current.getExpression();
|
|
||||||
if (receiver instanceof MethodInvocation nextMi) {
|
|
||||||
current = nextMi;
|
|
||||||
} else {
|
|
||||||
current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -21,6 +20,7 @@ public class PathBindingEvaluator {
|
|||||||
private final VariableTracer variableTracer;
|
private final VariableTracer variableTracer;
|
||||||
private final ConstantResolver constantResolver;
|
private final ConstantResolver constantResolver;
|
||||||
private final TypeResolver typeResolver;
|
private final TypeResolver typeResolver;
|
||||||
|
private final PathBindingExpressionResolver expressionResolver;
|
||||||
private final Map<String, Map<String, String>> traceBindingsCache = new HashMap<>();
|
private final Map<String, Map<String, String>> traceBindingsCache = new HashMap<>();
|
||||||
|
|
||||||
public PathBindingEvaluator(
|
public PathBindingEvaluator(
|
||||||
@@ -28,10 +28,20 @@ public class PathBindingEvaluator {
|
|||||||
VariableTracer variableTracer,
|
VariableTracer variableTracer,
|
||||||
ConstantResolver constantResolver,
|
ConstantResolver constantResolver,
|
||||||
TypeResolver typeResolver) {
|
TypeResolver typeResolver) {
|
||||||
|
this(context, variableTracer, constantResolver, typeResolver, new PathBindingExpressionResolver(context, variableTracer, constantResolver));
|
||||||
|
}
|
||||||
|
|
||||||
|
PathBindingEvaluator(
|
||||||
|
CodebaseContext context,
|
||||||
|
VariableTracer variableTracer,
|
||||||
|
ConstantResolver constantResolver,
|
||||||
|
TypeResolver typeResolver,
|
||||||
|
PathBindingExpressionResolver expressionResolver) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.variableTracer = variableTracer;
|
this.variableTracer = variableTracer;
|
||||||
this.constantResolver = constantResolver;
|
this.constantResolver = constantResolver;
|
||||||
this.typeResolver = typeResolver;
|
this.typeResolver = typeResolver;
|
||||||
|
this.expressionResolver = expressionResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
|
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
|
||||||
@@ -55,17 +65,22 @@ public class PathBindingEvaluator {
|
|||||||
|
|
||||||
private Map<String, String> computeTraceBindings(
|
private Map<String, String> computeTraceBindings(
|
||||||
List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
|
||||||
Map<String, String> bindings = new HashMap<>();
|
context.setAnalysisCallPath(path);
|
||||||
for (int i = 0; i < path.size() - 1; i++) {
|
try {
|
||||||
String caller = path.get(i);
|
Map<String, String> bindings = new HashMap<>();
|
||||||
String target = path.get(i + 1);
|
for (int i = 0; i < path.size() - 1; i++) {
|
||||||
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
|
String caller = path.get(i);
|
||||||
if (edge == null) {
|
String target = path.get(i + 1);
|
||||||
continue;
|
CallEdge edge = findEdge(caller, target, callGraph, pathFinder);
|
||||||
|
if (edge == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
||||||
}
|
}
|
||||||
enrichBindings(caller, target, edge, path, i + 1, callGraph, pathFinder, bindings);
|
return bindings;
|
||||||
|
} finally {
|
||||||
|
context.clearAnalysisCallPath();
|
||||||
}
|
}
|
||||||
return bindings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String pathCacheKey(List<String> path) {
|
private static String pathCacheKey(List<String> path) {
|
||||||
@@ -143,20 +158,24 @@ public class PathBindingEvaluator {
|
|||||||
Map<String, List<CallEdge>> callGraph,
|
Map<String, List<CallEdge>> callGraph,
|
||||||
CallGraphPathFinder pathFinder,
|
CallGraphPathFinder pathFinder,
|
||||||
Map<String, String> bindings) {
|
Map<String, String> bindings) {
|
||||||
if (VariableTracer.isRunnableRunMethod(target) && pathIndex >= 2) {
|
if (FunctionalInterfaceTypes.isFunctionalSamMethod(target) && pathIndex >= 2) {
|
||||||
int paramIndex = variableTracer.findFunctionalParameterIndex(caller);
|
int paramIndex = variableTracer.findFunctionalParameterIndex(caller);
|
||||||
if (paramIndex < 0) {
|
if (paramIndex < 0) {
|
||||||
paramIndex = 0;
|
paramIndex = 0;
|
||||||
}
|
}
|
||||||
|
String functionalCaller = path.get(pathIndex - 2);
|
||||||
|
CallEdge functionalEdge = CallSiteMatcher.selectEdge(
|
||||||
|
functionalCaller, caller, callGraph.get(functionalCaller), bindings);
|
||||||
List<String> constants = variableTracer.resolveConstantsFromCallSiteArgument(
|
List<String> constants = variableTracer.resolveConstantsFromCallSiteArgument(
|
||||||
path.get(pathIndex - 2), caller, paramIndex);
|
functionalCaller, caller, paramIndex, functionalEdge);
|
||||||
for (String constant : constants) {
|
for (String constant : constants) {
|
||||||
if (constant != null && !constant.isBlank()) {
|
if (constant != null && !constant.isBlank()) {
|
||||||
bindings.putIfAbsent(constant, constant);
|
bindings.putIfAbsent(constant, constant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map<String, String> paramValues = variableTracer.buildParameterValuesMap(caller, target, callGraph, path, pathIndex);
|
Map<String, String> paramValues = variableTracer.buildParameterValuesMap(
|
||||||
|
caller, target, callGraph, path, pathIndex, bindings);
|
||||||
mergeResolvedBindings(bindings, paramValues, caller);
|
mergeResolvedBindings(bindings, paramValues, caller);
|
||||||
|
|
||||||
List<String> targetParams = typeResolver.getParameterNames(target);
|
List<String> targetParams = typeResolver.getParameterNames(target);
|
||||||
@@ -168,7 +187,7 @@ public class PathBindingEvaluator {
|
|||||||
String paramName = targetParams.get(i);
|
String paramName = targetParams.get(i);
|
||||||
if (typeResolver != null
|
if (typeResolver != null
|
||||||
&& FunctionalInterfaceTypes.isFunctionalInterface(typeResolver.getParameterType(target, i))) {
|
&& FunctionalInterfaceTypes.isFunctionalInterface(typeResolver.getParameterType(target, i))) {
|
||||||
List<String> constants = variableTracer.resolveConstantsFromCallSiteArgument(caller, target, i);
|
List<String> constants = variableTracer.resolveConstantsFromCallSiteArgument(caller, target, i, edge);
|
||||||
if (constants.size() == 1) {
|
if (constants.size() == 1) {
|
||||||
bindings.put(paramName, constants.get(0));
|
bindings.put(paramName, constants.get(0));
|
||||||
continue;
|
continue;
|
||||||
@@ -218,7 +237,7 @@ public class PathBindingEvaluator {
|
|||||||
if (traced.startsWith("\"") || isEnumLikeConstant(traced)) {
|
if (traced.startsWith("\"") || isEnumLikeConstant(traced)) {
|
||||||
return traced;
|
return traced;
|
||||||
}
|
}
|
||||||
String fromCall = resolveMethodCallFromSource(callerFqn, rawValue, bindings);
|
String fromCall = expressionResolver.resolveMethodCallFromSource(callerFqn, rawValue, bindings);
|
||||||
if (fromCall != null) {
|
if (fromCall != null) {
|
||||||
return fromCall;
|
return fromCall;
|
||||||
}
|
}
|
||||||
@@ -229,155 +248,6 @@ public class PathBindingEvaluator {
|
|||||||
return rawValue;
|
return rawValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveMethodCallFromSource(String callerFqn, String varName, Map<String, String> bindings) {
|
|
||||||
MethodDeclaration md = findMethodDeclaration(callerFqn);
|
|
||||||
if (md == null || md.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final Expression[] initializer = new Expression[1];
|
|
||||||
md.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(VariableDeclarationFragment node) {
|
|
||||||
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
|
||||||
initializer[0] = node.getInitializer();
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (initializer[0] == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return evaluateExpressionValue(initializer[0], callerFqn, bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String evaluateExpressionValue(Expression expr, String callerFqn, Map<String, String> bindings) {
|
|
||||||
if (expr instanceof StringLiteral sl) {
|
|
||||||
return "\"" + sl.getLiteralValue() + "\"";
|
|
||||||
}
|
|
||||||
if (expr instanceof QualifiedName qn) {
|
|
||||||
return qn.getFullyQualifiedName();
|
|
||||||
}
|
|
||||||
if (expr instanceof SimpleName sn) {
|
|
||||||
return bindings.getOrDefault(sn.getIdentifier(), sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (expr instanceof MethodInvocation mi) {
|
|
||||||
return evaluateMethodInvocationReturn(mi, callerFqn, bindings);
|
|
||||||
}
|
|
||||||
if (expr instanceof SwitchExpression se) {
|
|
||||||
return constantResolver.evaluateSwitchWithParams(se, bindings, context);
|
|
||||||
}
|
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String evaluateMethodInvocationReturn(MethodInvocation mi, String callerFqn, Map<String, String> bindings) {
|
|
||||||
String ownerFqn = resolveMethodOwnerFqn(mi, callerFqn);
|
|
||||||
if (ownerFqn == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String methodName = mi.getName().getIdentifier();
|
|
||||||
TypeDeclaration owner = context.getTypeDeclaration(ownerFqn);
|
|
||||||
if (owner == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(owner, methodName, true);
|
|
||||||
if (md == null || md.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> callBindings = new HashMap<>(bindings);
|
|
||||||
for (int i = 0; i < md.parameters().size() && i < mi.arguments().size(); i++) {
|
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
|
|
||||||
String paramName = param.getName().getIdentifier();
|
|
||||||
String argValue = evaluateExpressionValue((Expression) mi.arguments().get(i), callerFqn, callBindings);
|
|
||||||
if (argValue != null) {
|
|
||||||
callBindings.put(paramName, argValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final String[] result = new String[1];
|
|
||||||
md.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement node) {
|
|
||||||
if (node.getExpression() instanceof SwitchExpression se) {
|
|
||||||
result[0] = constantResolver.evaluateSwitchWithParams(se, callBindings, context);
|
|
||||||
} else if (node.getExpression() != null) {
|
|
||||||
result[0] = evaluateExpressionValue(node.getExpression(), ownerFqn + "." + methodName, callBindings);
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveFieldTypeFqn(String callerFqn, String fieldName) {
|
|
||||||
if (callerFqn == null || !callerFqn.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = callerFqn.substring(0, callerFqn.lastIndexOf('.'));
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (FieldDeclaration field : td.getFields()) {
|
|
||||||
for (Object fragmentObj : field.fragments()) {
|
|
||||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObj;
|
|
||||||
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
|
||||||
IVariableBinding binding = fragment.resolveBinding();
|
|
||||||
if (binding != null && binding.getType() != null) {
|
|
||||||
return binding.getType().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
String simpleType = field.getType().toString();
|
|
||||||
TypeDeclaration resolved = context.getTypeDeclaration(simpleType);
|
|
||||||
if (resolved != null) {
|
|
||||||
return context.getFqn(resolved);
|
|
||||||
}
|
|
||||||
if (td.getRoot() instanceof CompilationUnit cu && cu.getPackage() != null) {
|
|
||||||
String pkg = cu.getPackage().getName().getFullyQualifiedName();
|
|
||||||
resolved = context.getTypeDeclaration(pkg + "." + simpleType);
|
|
||||||
if (resolved != null) {
|
|
||||||
return context.getFqn(resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return simpleType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveMethodOwnerFqn(MethodInvocation mi, String callerFqn) {
|
|
||||||
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
|
||||||
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
|
|
||||||
return methodBinding.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
Expression receiver = mi.getExpression();
|
|
||||||
if (receiver instanceof SimpleName sn) {
|
|
||||||
return resolveFieldTypeFqn(callerFqn, sn.getIdentifier());
|
|
||||||
}
|
|
||||||
if (receiver instanceof FieldAccess fa) {
|
|
||||||
IVariableBinding fieldBinding = fa.resolveFieldBinding();
|
|
||||||
if (fieldBinding != null && fieldBinding.getType() != null) {
|
|
||||||
return fieldBinding.getType().getErasure().getQualifiedName();
|
|
||||||
}
|
|
||||||
return resolveFieldTypeFqn(callerFqn, fa.getName().getIdentifier());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodDeclaration findMethodDeclaration(String methodFqn) {
|
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return context.findMethodDeclaration(td, methodName, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeLiteral(String value) {
|
private static String normalizeLiteral(String value) {
|
||||||
if (value != null && value.startsWith("\"") && value.endsWith("\"")) {
|
if (value != null && value.startsWith("\"") && value.endsWith("\"")) {
|
||||||
return value.substring(1, value.length() - 1);
|
return value.substring(1, value.length() - 1);
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves expression values under path-binding context using the dataflow pipeline
|
||||||
|
* ({@link VariableTracer} / {@link click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel})
|
||||||
|
* instead of a parallel AST mini-interpreter.
|
||||||
|
*/
|
||||||
|
public class PathBindingExpressionResolver {
|
||||||
|
|
||||||
|
private final CodebaseContext context;
|
||||||
|
private final VariableTracer variableTracer;
|
||||||
|
private final ConstantResolver constantResolver;
|
||||||
|
|
||||||
|
public PathBindingExpressionResolver(
|
||||||
|
CodebaseContext context,
|
||||||
|
VariableTracer variableTracer,
|
||||||
|
ConstantResolver constantResolver) {
|
||||||
|
this.context = context;
|
||||||
|
this.variableTracer = variableTracer;
|
||||||
|
this.constantResolver = constantResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveMethodCallFromSource(String callerFqn, String varName, Map<String, String> bindings) {
|
||||||
|
MethodDeclaration md = findMethodDeclaration(callerFqn);
|
||||||
|
if (md == null || md.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Expression[] initializer = new Expression[1];
|
||||||
|
md.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationFragment node) {
|
||||||
|
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
|
||||||
|
initializer[0] = node.getInitializer();
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (initializer[0] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resolve(initializer[0], callerFqn, bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolve(Expression expr, String callerFqn, Map<String, String> bindings) {
|
||||||
|
if (expr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, String> effectiveBindings = bindings == null ? Map.of() : new HashMap<>(bindings);
|
||||||
|
context.setAnalysisNamedBindings(effectiveBindings);
|
||||||
|
try {
|
||||||
|
return resolveInternal(expr, callerFqn, effectiveBindings);
|
||||||
|
} finally {
|
||||||
|
context.clearAnalysisNamedBindings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveInternal(Expression expr, String callerFqn, Map<String, String> bindings) {
|
||||||
|
if (expr instanceof StringLiteral sl) {
|
||||||
|
return "\"" + sl.getLiteralValue() + "\"";
|
||||||
|
}
|
||||||
|
if (expr instanceof QualifiedName qn) {
|
||||||
|
return qn.getFullyQualifiedName();
|
||||||
|
}
|
||||||
|
if (expr instanceof SimpleName sn) {
|
||||||
|
return bindings.getOrDefault(sn.getIdentifier(), sn.getIdentifier());
|
||||||
|
}
|
||||||
|
if (expr instanceof SwitchExpression se) {
|
||||||
|
return constantResolver.evaluateSwitchWithParams(se, bindings, context);
|
||||||
|
}
|
||||||
|
if (expr instanceof MethodInvocation mi) {
|
||||||
|
return resolveMethodInvocationReturn(mi, callerFqn, bindings);
|
||||||
|
}
|
||||||
|
return resolveViaDataflow(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveMethodInvocationReturn(MethodInvocation mi, String callerFqn, Map<String, String> bindings) {
|
||||||
|
String ownerFqn = resolveMethodOwnerFqn(mi, callerFqn);
|
||||||
|
if (ownerFqn == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
TypeDeclaration owner = context.getTypeDeclaration(ownerFqn);
|
||||||
|
if (owner == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(owner, methodName, true);
|
||||||
|
if (md == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> callBindings = new HashMap<>(bindings);
|
||||||
|
for (int i = 0; i < md.parameters().size() && i < mi.arguments().size(); i++) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(i);
|
||||||
|
String paramName = param.getName().getIdentifier();
|
||||||
|
String argValue = resolveInternal((Expression) mi.arguments().get(i), callerFqn, callBindings);
|
||||||
|
if (argValue != null) {
|
||||||
|
callBindings.put(paramName, argValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setAnalysisNamedBindings(callBindings);
|
||||||
|
try {
|
||||||
|
return resolveViaDataflow(mi);
|
||||||
|
} finally {
|
||||||
|
context.setAnalysisNamedBindings(bindings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveViaDataflow(Expression expr) {
|
||||||
|
Set<String> constants = new LinkedHashSet<>();
|
||||||
|
for (Expression def : variableTracer.traceVariableAll(expr)) {
|
||||||
|
String val = constantResolver.resolve(def, context);
|
||||||
|
if (val != null && !val.isBlank()) {
|
||||||
|
constants.add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (constants.size() == 1) {
|
||||||
|
return formatResolvedConstant(constants.iterator().next());
|
||||||
|
}
|
||||||
|
if (constants.isEmpty()) {
|
||||||
|
String direct = constantResolver.resolve(expr, context);
|
||||||
|
return direct != null ? formatResolvedConstant(direct) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatResolvedConstant(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value.startsWith("ENUM_SET:")
|
||||||
|
|| value.startsWith("<SYMBOLIC:")
|
||||||
|
|| value.contains("SYMBOLIC:")
|
||||||
|
|| value.endsWith(".*>")) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value.startsWith("\"") || AstUtils.isEnumLikeConstantName(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return "\"" + value + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveMethodOwnerFqn(MethodInvocation mi, String callerFqn) {
|
||||||
|
IMethodBinding methodBinding = mi.resolveMethodBinding();
|
||||||
|
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
|
||||||
|
return methodBinding.getDeclaringClass().getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
|
if (receiver instanceof SimpleName sn) {
|
||||||
|
return resolveFieldTypeFqn(callerFqn, sn.getIdentifier());
|
||||||
|
}
|
||||||
|
if (receiver instanceof FieldAccess fa) {
|
||||||
|
IVariableBinding fieldBinding = fa.resolveFieldBinding();
|
||||||
|
if (fieldBinding != null && fieldBinding.getType() != null) {
|
||||||
|
return fieldBinding.getType().getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
return resolveFieldTypeFqn(callerFqn, fa.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveFieldTypeFqn(String callerFqn, String fieldName) {
|
||||||
|
if (callerFqn == null || !callerFqn.contains(".")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String className = callerFqn.substring(0, callerFqn.lastIndexOf('.'));
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (FieldDeclaration field : td.getFields()) {
|
||||||
|
for (Object fragmentObj : field.fragments()) {
|
||||||
|
VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObj;
|
||||||
|
if (fragment.getName().getIdentifier().equals(fieldName)) {
|
||||||
|
IVariableBinding binding = fragment.resolveBinding();
|
||||||
|
if (binding != null && binding.getType() != null) {
|
||||||
|
return binding.getType().getErasure().getQualifiedName();
|
||||||
|
}
|
||||||
|
String simpleType = field.getType().toString();
|
||||||
|
TypeDeclaration resolved = context.getTypeDeclaration(simpleType);
|
||||||
|
if (resolved != null) {
|
||||||
|
return context.getFqn(resolved);
|
||||||
|
}
|
||||||
|
if (td.getRoot() instanceof CompilationUnit cu && cu.getPackage() != null) {
|
||||||
|
String pkg = cu.getPackage().getName().getFullyQualifiedName();
|
||||||
|
resolved = context.getTypeDeclaration(pkg + "." + simpleType);
|
||||||
|
if (resolved != null) {
|
||||||
|
return context.getFqn(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return simpleType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclaration findMethodDeclaration(String methodFqn) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return context.findMethodDeclaration(td, methodName, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver
|
|||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorNaming;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorNaming;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
@@ -481,7 +482,33 @@ public class VariableTracer {
|
|||||||
private record BranchAssignment(Expression expression, String constraint) {}
|
private record BranchAssignment(Expression expression, String constraint) {}
|
||||||
|
|
||||||
public Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
|
public Map<String, String> buildParameterValuesMap(String caller, String target, Map<String, List<CallEdge>> callGraph, List<String> path, int pathIndex) {
|
||||||
|
return buildParameterValuesMap(caller, target, callGraph, path, pathIndex, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> buildParameterValuesMap(
|
||||||
|
String caller,
|
||||||
|
String target,
|
||||||
|
Map<String, List<CallEdge>> callGraph,
|
||||||
|
List<String> path,
|
||||||
|
int pathIndex,
|
||||||
|
Map<String, String> bindings) {
|
||||||
Map<String, String> paramValues = new HashMap<>();
|
Map<String, String> paramValues = new HashMap<>();
|
||||||
|
context.setAnalysisCallPath(path);
|
||||||
|
try {
|
||||||
|
return buildParameterValuesMapInternal(caller, target, callGraph, path, pathIndex, bindings, paramValues);
|
||||||
|
} finally {
|
||||||
|
context.clearAnalysisCallPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> buildParameterValuesMapInternal(
|
||||||
|
String caller,
|
||||||
|
String target,
|
||||||
|
Map<String, List<CallEdge>> callGraph,
|
||||||
|
List<String> path,
|
||||||
|
int pathIndex,
|
||||||
|
Map<String, String> bindings,
|
||||||
|
Map<String, String> paramValues) {
|
||||||
List<CallEdge> edges = callGraph.get(caller);
|
List<CallEdge> edges = callGraph.get(caller);
|
||||||
|
|
||||||
boolean hasEdge = false;
|
boolean hasEdge = false;
|
||||||
@@ -514,45 +541,51 @@ public class VariableTracer {
|
|||||||
return paramValues;
|
return paramValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We also want to support heuristic matches using a helper if isHeuristicMatch is not directly available, but let's check
|
CallEdge selectedEdge = CallSiteMatcher.selectEdge(caller, target, edges, bindings);
|
||||||
// We can check if name equals or if it resolves to target
|
if (selectedEdge == null) {
|
||||||
for (CallEdge edge : edges) {
|
return paramValues;
|
||||||
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
}
|
||||||
List<String> args = edge.getArguments();
|
|
||||||
if (args == null || args.isEmpty()) break;
|
|
||||||
|
|
||||||
int lastDot = target.lastIndexOf('.');
|
List<String> args = selectedEdge.getArguments();
|
||||||
if (lastDot < 0) break;
|
if (args == null || args.isEmpty()) {
|
||||||
String className = target.substring(0, lastDot);
|
return paramValues;
|
||||||
String methodName = target.substring(lastDot + 1);
|
}
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
int lastDot = target.lastIndexOf('.');
|
||||||
if (td == null) break;
|
if (lastDot < 0) {
|
||||||
|
return paramValues;
|
||||||
|
}
|
||||||
|
String className = target.substring(0, lastDot);
|
||||||
|
String methodName = target.substring(lastDot + 1);
|
||||||
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
if (md == null) break;
|
if (td == null) {
|
||||||
|
return paramValues;
|
||||||
|
}
|
||||||
|
|
||||||
List<String> paramNames = new ArrayList<>();
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
for (Object paramObj : md.parameters()) {
|
if (md == null) {
|
||||||
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
return paramValues;
|
||||||
paramNames.add(param.getName().getIdentifier());
|
}
|
||||||
|
|
||||||
|
List<String> paramNames = new ArrayList<>();
|
||||||
|
for (Object paramObj : md.parameters()) {
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) paramObj;
|
||||||
|
paramNames.add(param.getName().getIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
int minSize = Math.min(paramNames.size(), args.size());
|
||||||
|
for (int j = 0; j < minSize; j++) {
|
||||||
|
String argValue = args.get(j);
|
||||||
|
String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
|
||||||
|
if (typeResolver != null
|
||||||
|
&& FunctionalInterfaceTypes.isFunctionalInterface(typeResolver.getParameterType(target, j))) {
|
||||||
|
List<String> constants = resolveConstantsFromCallSiteArgument(caller, target, j, selectedEdge);
|
||||||
|
if (constants.size() == 1) {
|
||||||
|
resolvedArgValue = constants.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
int minSize = Math.min(paramNames.size(), args.size());
|
|
||||||
for (int j = 0; j < minSize; j++) {
|
|
||||||
String argValue = args.get(j);
|
|
||||||
String resolvedArgValue = resolveArgumentValue(argValue, caller, path, pathIndex, callGraph);
|
|
||||||
if (typeResolver != null
|
|
||||||
&& FunctionalInterfaceTypes.isFunctionalInterface(typeResolver.getParameterType(target, j))) {
|
|
||||||
List<String> constants = resolveConstantsFromCallSiteArgument(caller, target, j);
|
|
||||||
if (constants.size() == 1) {
|
|
||||||
resolvedArgValue = constants.get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paramValues.put(paramNames.get(j), resolvedArgValue);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
paramValues.put(paramNames.get(j), resolvedArgValue);
|
||||||
}
|
}
|
||||||
return paramValues;
|
return paramValues;
|
||||||
}
|
}
|
||||||
@@ -716,11 +749,17 @@ public class VariableTracer {
|
|||||||
* Used for {@code Supplier}, {@code Runnable}, and similar functional parameters.
|
* Used for {@code Supplier}, {@code Runnable}, and similar functional parameters.
|
||||||
*/
|
*/
|
||||||
public List<String> resolveConstantsFromCallSiteArgument(String caller, String callee, int paramIndex) {
|
public List<String> resolveConstantsFromCallSiteArgument(String caller, String callee, int paramIndex) {
|
||||||
|
return resolveConstantsFromCallSiteArgument(caller, callee, paramIndex, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveConstantsFromCallSiteArgument(
|
||||||
|
String caller, String callee, int paramIndex, CallEdge edge) {
|
||||||
if (constantExtractor == null) {
|
if (constantExtractor == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<String> constants = new ArrayList<>();
|
List<String> constants = new ArrayList<>();
|
||||||
Expression sourceArg = findCallSiteArgumentExpression(caller, callee, paramIndex);
|
Expression sourceArg = CallSiteMatcher.findCallSiteArgumentExpression(
|
||||||
|
caller, callee, paramIndex, edge, context);
|
||||||
if (sourceArg != null) {
|
if (sourceArg != null) {
|
||||||
resolveConstantsFromExpressionViaDataflow(sourceArg, constants);
|
resolveConstantsFromExpressionViaDataflow(sourceArg, constants);
|
||||||
}
|
}
|
||||||
@@ -743,77 +782,47 @@ public class VariableTracer {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isRunnableRunMethod(String methodFqn) {
|
public static boolean isFunctionalSamMethod(String methodFqn) {
|
||||||
return "java.lang.Runnable.run".equals(methodFqn) || "Runnable.run".equals(methodFqn);
|
return FunctionalInterfaceTypes.isFunctionalSamMethod(methodFqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resolveConstantsViaDataflow(Expression expression, List<String> constants) {
|
||||||
|
resolveConstantsFromExpressionViaDataflow(expression, constants);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveConstantsFromSourceText(
|
||||||
|
String expressionText, List<String> searchMethods, List<String> callPath) {
|
||||||
|
if (expressionText == null || expressionText.isBlank()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
context.setAnalysisCallPath(callPath);
|
||||||
|
try {
|
||||||
|
Expression anchored = AstUtils.findExpressionInMethods(searchMethods, expressionText, context);
|
||||||
|
Expression expr = anchored != null ? anchored : AstUtils.parseExpression(expressionText);
|
||||||
|
if (expr == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
resolveConstantsViaDataflow(expr, constants);
|
||||||
|
return constants.stream()
|
||||||
|
.filter(c -> c != null && !c.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
} finally {
|
||||||
|
context.clearAnalysisCallPath();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveConstantsFromExpressionViaDataflow(Expression expression, List<String> constants) {
|
private void resolveConstantsFromExpressionViaDataflow(Expression expression, List<String> constants) {
|
||||||
Expression effective = unwrapLambdaBody(expression);
|
for (Expression def : dataFlowModel.getReachingDefinitions(expression)) {
|
||||||
if (effective != expression) {
|
|
||||||
resolveConstantsFromExpressionViaDataflow(effective, constants);
|
|
||||||
if (!constants.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Expression def : dataFlowModel.getReachingDefinitions(effective)) {
|
|
||||||
constantExtractor.extractConstantsFromExpression(def, constants);
|
constantExtractor.extractConstantsFromExpression(def, constants);
|
||||||
}
|
}
|
||||||
if (constants.isEmpty()) {
|
if (constants.isEmpty()) {
|
||||||
constantExtractor.extractConstantsFromExpression(effective, constants);
|
constantExtractor.extractConstantsFromExpression(expression, constants);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression unwrapLambdaBody(Expression expression) {
|
|
||||||
if (!(expression instanceof LambdaExpression lambda)) {
|
|
||||||
return expression;
|
|
||||||
}
|
|
||||||
ASTNode body = lambda.getBody();
|
|
||||||
if (body instanceof Expression bodyExpr) {
|
|
||||||
return bodyExpr;
|
|
||||||
}
|
|
||||||
return expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression findCallSiteArgumentExpression(String caller, String callee, int paramIndex) {
|
Expression findCallSiteArgumentExpression(String caller, String callee, int paramIndex) {
|
||||||
if (caller == null || callee == null || paramIndex < 0
|
return CallSiteMatcher.findCallSiteArgumentExpression(caller, callee, paramIndex, null, context);
|
||||||
|| !caller.contains(".") || !callee.contains(".")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String callerClass = caller.substring(0, caller.lastIndexOf('.'));
|
|
||||||
String callerMethod = caller.substring(caller.lastIndexOf('.') + 1);
|
|
||||||
String calleeMethod = callee.substring(callee.lastIndexOf('.') + 1);
|
|
||||||
TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
|
|
||||||
if (callerType == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MethodDeclaration method = context.findMethodDeclaration(callerType, callerMethod, true);
|
|
||||||
if (method == null || method.getBody() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final Expression[] found = new Expression[1];
|
|
||||||
method.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (found[0] != null || !calleeMethod.equals(node.getName().getIdentifier())) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
IMethodBinding binding = node.resolveMethodBinding();
|
|
||||||
if (binding != null && binding.getDeclaringClass() != null) {
|
|
||||||
String declaringFqn = binding.getDeclaringClass().getErasure().getQualifiedName();
|
|
||||||
String calleeClass = callee.substring(0, callee.lastIndexOf('.'));
|
|
||||||
if (!declaringFqn.equals(calleeClass) && !declaringFqn.endsWith("." + calleeClass)) {
|
|
||||||
String calleeSimple = calleeClass.substring(calleeClass.lastIndexOf('.') + 1);
|
|
||||||
if (!declaringFqn.endsWith("." + calleeSimple)) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (paramIndex < node.arguments().size()) {
|
|
||||||
found[0] = (Expression) node.arguments().get(paramIndex);
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return found[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package click.kamil.springstatemachineexporter.ast.common;
|
package click.kamil.springstatemachineexporter.ast.common;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.JavaCore;
|
||||||
|
import org.eclipse.jdt.core.dom.AST;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTNode;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTParser;
|
||||||
import org.eclipse.jdt.core.dom.Annotation;
|
import org.eclipse.jdt.core.dom.Annotation;
|
||||||
import org.eclipse.jdt.core.dom.ArrayType;
|
import org.eclipse.jdt.core.dom.ArrayType;
|
||||||
import org.eclipse.jdt.core.dom.ASTNode;
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||||
import org.eclipse.jdt.core.dom.MemberValuePair;
|
import org.eclipse.jdt.core.dom.MemberValuePair;
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
import org.eclipse.jdt.core.dom.Name;
|
import org.eclipse.jdt.core.dom.Name;
|
||||||
@@ -17,6 +24,9 @@ import org.eclipse.jdt.core.dom.SimpleType;
|
|||||||
import org.eclipse.jdt.core.dom.Type;
|
import org.eclipse.jdt.core.dom.Type;
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public final class AstUtils {
|
public final class AstUtils {
|
||||||
private AstUtils() {
|
private AstUtils() {
|
||||||
}
|
}
|
||||||
@@ -263,4 +273,145 @@ public final class AstUtils {
|
|||||||
combined.append(')');
|
combined.append(')');
|
||||||
return combined.toString();
|
return combined.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Expression parseExpression(String snippet) {
|
||||||
|
if (snippet == null || snippet.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ASTParser parser = ASTParser.newParser(AST.JLS17);
|
||||||
|
Map<String, String> options = JavaCore.getOptions();
|
||||||
|
JavaCore.setComplianceOptions(JavaCore.VERSION_17, options);
|
||||||
|
parser.setCompilerOptions(options);
|
||||||
|
parser.setKind(ASTParser.K_EXPRESSION);
|
||||||
|
parser.setSource(snippet.toCharArray());
|
||||||
|
try {
|
||||||
|
ASTNode node = parser.createAST(null);
|
||||||
|
if (node instanceof Expression expression) {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
if (node instanceof CompilationUnit) {
|
||||||
|
ASTParser fallbackParser = ASTParser.newParser(AST.JLS17);
|
||||||
|
fallbackParser.setCompilerOptions(options);
|
||||||
|
fallbackParser.setKind(ASTParser.K_COMPILATION_UNIT);
|
||||||
|
fallbackParser.setSource(("class A { Object o = " + snippet + "; }").toCharArray());
|
||||||
|
CompilationUnit cu = (CompilationUnit) fallbackParser.createAST(null);
|
||||||
|
if (!cu.types().isEmpty() && cu.types().get(0) instanceof TypeDeclaration td) {
|
||||||
|
if (!td.bodyDeclarations().isEmpty() && td.bodyDeclarations().get(0) instanceof FieldDeclaration fd) {
|
||||||
|
if (!fd.fragments().isEmpty() && fd.fragments().get(0) instanceof VariableDeclarationFragment vdf) {
|
||||||
|
return vdf.getInitializer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression findExpressionInMethod(String methodFqn, String expressionText, CodebaseContext context) {
|
||||||
|
if (methodFqn == null || expressionText == null || !methodFqn.contains(".") || context == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
|
if (td == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
|
if (md == null || md.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Expression[] match = new Expression[1];
|
||||||
|
md.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.MethodInvocation node) {
|
||||||
|
if (expressionText.equals(node.toString())) {
|
||||||
|
match[0] = node;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.SuperMethodInvocation node) {
|
||||||
|
if (expressionText.equals(node.toString())) {
|
||||||
|
match[0] = node;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(QualifiedName node) {
|
||||||
|
if (expressionText.equals(node.toString())) {
|
||||||
|
match[0] = node;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(org.eclipse.jdt.core.dom.SimpleName node) {
|
||||||
|
if (expressionText.equals(node.getIdentifier())) {
|
||||||
|
match[0] = node;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static org.eclipse.jdt.core.dom.MethodInvocation findMethodInvocationInMethod(
|
||||||
|
String methodFqn, String expressionText, CodebaseContext context) {
|
||||||
|
Expression found = findExpressionInMethod(methodFqn, expressionText, context);
|
||||||
|
return found instanceof org.eclipse.jdt.core.dom.MethodInvocation mi ? mi : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression findExpressionInMethods(
|
||||||
|
List<String> methodFqns, String expressionText, CodebaseContext context) {
|
||||||
|
if (methodFqns == null || expressionText == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String methodFqn : methodFqns) {
|
||||||
|
Expression found = findExpressionInMethod(methodFqn, expressionText, context);
|
||||||
|
if (found != null) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String combineExpressionText(String base, String suffix) {
|
||||||
|
if (suffix == null || suffix.isEmpty()) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
return base + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toParseSnippet(String boundValue) {
|
||||||
|
if (boundValue == null || boundValue.isBlank()) {
|
||||||
|
return boundValue;
|
||||||
|
}
|
||||||
|
if (boundValue.startsWith("\"") && boundValue.endsWith("\"")) {
|
||||||
|
return boundValue;
|
||||||
|
}
|
||||||
|
if (isEnumLikeConstantName(boundValue)) {
|
||||||
|
return boundValue;
|
||||||
|
}
|
||||||
|
return "\"" + boundValue.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEnumLikeConstantName(String value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int dot = value.lastIndexOf('.');
|
||||||
|
if (dot <= 0 || dot >= value.length() - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Character.isUpperCase(value.charAt(dot + 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,6 +177,32 @@ public class CodebaseContext {
|
|||||||
private String[] classpath = new String[0];
|
private String[] classpath = new String[0];
|
||||||
private String[] sourcepath = new String[0];
|
private String[] sourcepath = new String[0];
|
||||||
private boolean resolveBindings = false;
|
private boolean resolveBindings = false;
|
||||||
|
private transient List<String> analysisCallPath;
|
||||||
|
private transient Map<String, String> analysisNamedBindings;
|
||||||
|
|
||||||
|
public void setAnalysisCallPath(List<String> path) {
|
||||||
|
this.analysisCallPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAnalysisCallPath() {
|
||||||
|
return analysisCallPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAnalysisCallPath() {
|
||||||
|
this.analysisCallPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAnalysisNamedBindings(Map<String, String> bindings) {
|
||||||
|
this.analysisNamedBindings = bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAnalysisNamedBindings() {
|
||||||
|
return analysisNamedBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAnalysisNamedBindings() {
|
||||||
|
this.analysisNamedBindings = null;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Map<String, String>> getProperties() {
|
public Map<String, Map<String, String>> getProperties() {
|
||||||
return Collections.unmodifiableMap(allProperties);
|
return Collections.unmodifiableMap(allProperties);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package click.kamil.springstatemachineexporter.ast.common;
|
|||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorInlining;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorInlining;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.pipeline.LibraryUnwrapRegistry;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||||
import org.eclipse.jdt.core.dom.*;
|
import org.eclipse.jdt.core.dom.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -15,20 +17,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
private final Map<MethodDeclaration, ReachingDefinitions> rdCache = new HashMap<>();
|
||||||
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = new HashMap<>();
|
private final Map<ASTNode, Map<IVariableBinding, Expression>> objectFieldBindings = new HashMap<>();
|
||||||
|
|
||||||
private static final ThreadLocal<List<String>> CURRENT_PATH = new ThreadLocal<>();
|
|
||||||
|
|
||||||
public static void setCurrentPath(List<String> path) {
|
|
||||||
CURRENT_PATH.set(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<String> getCurrentPath() {
|
|
||||||
return CURRENT_PATH.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearCurrentPath() {
|
|
||||||
CURRENT_PATH.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JdtDataFlowModel(CodebaseContext context) {
|
public JdtDataFlowModel(CodebaseContext context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
@@ -64,6 +52,13 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (expr instanceof LambdaExpression lambda) {
|
||||||
|
List<Expression> resolved = resolveFunctionalBoundExpression(
|
||||||
|
lambda, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Handle Ternary / Conditional Expression
|
// 1. Handle Ternary / Conditional Expression
|
||||||
if (expr instanceof ConditionalExpression ce) {
|
if (expr instanceof ConditionalExpression ce) {
|
||||||
String condVal = resolveValue(ce.getExpression(), context);
|
String condVal = resolveValue(ce.getExpression(), context);
|
||||||
@@ -111,6 +106,18 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> namedBindings = context.getAnalysisNamedBindings();
|
||||||
|
if (namedBindings != null && namedBindings.containsKey(sn.getIdentifier())) {
|
||||||
|
String boundValue = namedBindings.get(sn.getIdentifier());
|
||||||
|
Expression synthetic = AstUtils.parseExpression(AstUtils.toParseSnippet(boundValue));
|
||||||
|
if (synthetic != null) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions(
|
||||||
|
synthetic, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: Local Reaching Definitions
|
// Fallback: Local Reaching Definitions
|
||||||
MethodDeclaration md = findEnclosingMethod(sn);
|
MethodDeclaration md = findEnclosingMethod(sn);
|
||||||
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
|
ReachingDefinitions rd = getReachingDefinitionsForMethod(md);
|
||||||
@@ -193,22 +200,51 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
|
|
||||||
// 4. Handle Method Invocations (Static Factory methods / Transforming Getters)
|
// 4. Handle Method Invocations (Static Factory methods / Transforming Getters)
|
||||||
if (expr instanceof MethodInvocation mi) {
|
if (expr instanceof MethodInvocation mi) {
|
||||||
String mName = mi.getName().getIdentifier();
|
Expression factoryPayload = ReactiveExpressionSupport.peelFactoryPayloadExpression(mi);
|
||||||
if (("just".equals(mName) || "withPayload".equals(mName) || "success".equals(mName)) && !mi.arguments().isEmpty()) {
|
if (factoryPayload != null) {
|
||||||
List<Expression> resolved = getReachingDefinitions((Expression) mi.arguments().get(0), visited, paramBindings, instanceFieldBindings, depth + 1);
|
List<Expression> resolved = getReachingDefinitions(
|
||||||
|
factoryPayload, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
visited.remove(expr);
|
visited.remove(expr);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("run".equals(mName) && mi.arguments().isEmpty() && isRunnableReceiver(mi.getExpression())) {
|
String mName = mi.getName().getIdentifier();
|
||||||
List<Expression> functional = resolveFunctionalInterfaceInvocation(
|
if (LibraryUnwrapRegistry.isReactiveTransformMethod(mName) && !mi.arguments().isEmpty()) {
|
||||||
mi, visited, paramBindings, instanceFieldBindings, depth);
|
Expression arg = (Expression) mi.arguments().get(0);
|
||||||
if (!functional.isEmpty()) {
|
if (arg instanceof LambdaExpression lambda && lambda.parameters().size() == 1) {
|
||||||
visited.remove(expr);
|
Expression source = ReactiveExpressionSupport.peelFactoryPayloadExpression(mi.getExpression());
|
||||||
return functional;
|
if (source != null) {
|
||||||
|
Object parameter = lambda.parameters().get(0);
|
||||||
|
if (parameter instanceof SingleVariableDeclaration svd) {
|
||||||
|
IVariableBinding paramBinding = svd.resolveBinding();
|
||||||
|
if (paramBinding != null) {
|
||||||
|
Map<IVariableBinding, Expression> transformBindings = new HashMap<>(paramBindings);
|
||||||
|
transformBindings.put(paramBinding, source);
|
||||||
|
Expression bodyExpression = ReactiveExpressionSupport.lambdaBodyExpression(lambda);
|
||||||
|
if (bodyExpression != null) {
|
||||||
|
Expression nestedFactory = ReactiveExpressionSupport.peelFactoryPayloadExpression(bodyExpression);
|
||||||
|
if (nestedFactory != null) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions(
|
||||||
|
nestedFactory, visited, transformBindings, instanceFieldBindings, depth + 1);
|
||||||
|
if (!resolved.isEmpty()) {
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Expression> resolved = getReachingDefinitions(
|
||||||
|
bodyExpression, visited, transformBindings, instanceFieldBindings, depth + 1);
|
||||||
|
if (!resolved.isEmpty()) {
|
||||||
|
visited.remove(expr);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ("get".equals(mName) && mi.arguments().isEmpty() && isSupplierLikeReceiver(mi.getExpression())) {
|
|
||||||
|
if (isFunctionalSamInvocation(mi)) {
|
||||||
List<Expression> functional = resolveFunctionalInterfaceInvocation(
|
List<Expression> functional = resolveFunctionalInterfaceInvocation(
|
||||||
mi, visited, paramBindings, instanceFieldBindings, depth);
|
mi, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
if (!functional.isEmpty()) {
|
if (!functional.isEmpty()) {
|
||||||
@@ -372,6 +408,11 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
receiverDefs = getReachingDefinitions(receiver, visited, paramBindings, instanceFieldBindings, depth + 1);
|
receiverDefs = getReachingDefinitions(receiver, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||||
accessor = AccessorInlining.lookupAccessor(context, mi, receiverDefs);
|
accessor = AccessorInlining.lookupAccessor(context, mi, receiverDefs);
|
||||||
if (accessor.isEmpty()) {
|
if (accessor.isEmpty()) {
|
||||||
|
List<Expression> anonymous = inlineAnonymousFromReceiverDefs(
|
||||||
|
methodName, receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
|
if (!anonymous.isEmpty()) {
|
||||||
|
return anonymous;
|
||||||
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,6 +441,11 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (accessor.isEmpty()) {
|
if (accessor.isEmpty()) {
|
||||||
|
List<Expression> anonymous = inlineAnonymousFromReceiverDefs(
|
||||||
|
methodName, receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
|
if (!anonymous.isEmpty()) {
|
||||||
|
return anonymous;
|
||||||
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
if (!accessor.get().isGetter()) {
|
if (!accessor.get().isGetter()) {
|
||||||
@@ -643,11 +689,253 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
if (fieldValue != null) {
|
if (fieldValue != null) {
|
||||||
results.addAll(getReachingDefinitions(fieldValue, visited, paramBindings, fieldBindings, depth + 1));
|
results.addAll(getReachingDefinitions(fieldValue, visited, paramBindings, fieldBindings, depth + 1));
|
||||||
}
|
}
|
||||||
|
if (results.isEmpty() && cic.getAnonymousClassDeclaration() != null) {
|
||||||
|
results.addAll(inlineAnonymousGetterReturns(
|
||||||
|
cic, accessor.methodName(), visited, paramBindings, fieldBindings, depth));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TypeDeclaration receiverType = resolveExpressionTypeDeclaration(
|
||||||
|
candidate, paramBindings, instanceFieldBindings);
|
||||||
|
if (receiverType != null) {
|
||||||
|
Map<IVariableBinding, Expression> typeFieldBindings =
|
||||||
|
buildTypeDefaultFieldBindings(receiverType);
|
||||||
|
if (receiver instanceof SimpleName sn && candidate == sn) {
|
||||||
|
applyFlowSensitiveMutationsForLocalReceiver(
|
||||||
|
sn, mi, accessor, typeFieldBindings, paramBindings, visited, depth);
|
||||||
|
}
|
||||||
|
Expression fieldValue = readFieldValue(accessor, typeFieldBindings, mi);
|
||||||
|
if (fieldValue != null) {
|
||||||
|
results.addAll(getReachingDefinitions(
|
||||||
|
fieldValue, visited, paramBindings, typeFieldBindings, depth + 1));
|
||||||
|
}
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
results.addAll(inlineGetterMethodReturns(
|
||||||
|
accessor, receiverType, visited, paramBindings, typeFieldBindings, depth));
|
||||||
|
}
|
||||||
|
if (results.isEmpty() && receiverType.isInterface()) {
|
||||||
|
results.addAll(inlineConcreteAccessorFromReceiverDefs(
|
||||||
|
accessor, receiverDefs, visited, paramBindings, typeFieldBindings, depth));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Expression> inlineAnonymousGetterReturns(
|
||||||
|
ClassInstanceCreation cic,
|
||||||
|
String methodName,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> fieldBindings,
|
||||||
|
int depth) {
|
||||||
|
if (cic.getAnonymousClassDeclaration() == null || methodName == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||||
|
if (declObj instanceof MethodDeclaration methodDeclaration
|
||||||
|
&& methodName.equals(methodDeclaration.getName().getIdentifier())
|
||||||
|
&& methodDeclaration.getBody() != null) {
|
||||||
|
List<Expression> results = new ArrayList<>();
|
||||||
|
for (ReturnStatement rs : findReturnStatements(methodDeclaration.getBody())) {
|
||||||
|
if (rs.getExpression() != null) {
|
||||||
|
results.addAll(getReachingDefinitions(
|
||||||
|
rs.getExpression(), visited, paramBindings, fieldBindings, depth + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Expression> inlineConcreteAccessorFromReceiverDefs(
|
||||||
|
AccessorSummary accessor,
|
||||||
|
List<Expression> receiverDefs,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> fieldBindings,
|
||||||
|
int depth) {
|
||||||
|
List<Expression> results = new ArrayList<>();
|
||||||
|
for (Expression receiverDef : receiverDefs) {
|
||||||
|
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
|
||||||
|
if (concreteFqn == null || concreteFqn.equals(accessor.ownerFqn())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TypeDeclaration concreteType = context.getTypeDeclaration(concreteFqn);
|
||||||
|
if (concreteType == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<IVariableBinding, Expression> concreteBindings = buildTypeDefaultFieldBindings(concreteType);
|
||||||
|
if (receiverDef instanceof ClassInstanceCreation cic) {
|
||||||
|
concreteBindings = getOrCreateFieldBindings(cic, paramBindings, fieldBindings);
|
||||||
|
}
|
||||||
|
Expression fieldValue = readFieldValue(accessor, concreteBindings, null);
|
||||||
|
if (fieldValue != null) {
|
||||||
|
results.addAll(getReachingDefinitions(
|
||||||
|
fieldValue, visited, paramBindings, concreteBindings, depth + 1));
|
||||||
|
}
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
results.addAll(inlineGetterMethodReturns(
|
||||||
|
accessor, concreteType, visited, paramBindings, concreteBindings, depth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Expression> inlineAnonymousFromReceiverDefs(
|
||||||
|
String methodName,
|
||||||
|
List<Expression> receiverDefs,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||||
|
int depth) {
|
||||||
|
if (receiverDefs == null || methodName == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
for (Expression receiverDef : receiverDefs) {
|
||||||
|
if (receiverDef instanceof ClassInstanceCreation cic) {
|
||||||
|
List<Expression> inlined = inlineAnonymousGetterReturns(
|
||||||
|
cic, methodName, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
|
if (!inlined.isEmpty()) {
|
||||||
|
return inlined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFlowSensitiveMutationsForLocalReceiver(
|
||||||
|
SimpleName receiverName,
|
||||||
|
MethodInvocation useSite,
|
||||||
|
AccessorSummary accessor,
|
||||||
|
Map<IVariableBinding, Expression> fieldBindings,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
int depth) {
|
||||||
|
MethodDeclaration enclosingMethod = findEnclosingMethod(useSite);
|
||||||
|
if (enclosingMethod == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ASTNode defNode = findVariableDefinitionNode(enclosingMethod, receiverName.getIdentifier());
|
||||||
|
if (defNode instanceof SingleVariableDeclaration) {
|
||||||
|
String setterName = "set" + Character.toUpperCase(accessor.fieldName().charAt(0))
|
||||||
|
+ accessor.fieldName().substring(1);
|
||||||
|
Expression setterArg = findLocalSetterArgument(
|
||||||
|
enclosingMethod,
|
||||||
|
receiverName.getIdentifier(),
|
||||||
|
accessor.methodName(),
|
||||||
|
setterName,
|
||||||
|
accessor.fieldName());
|
||||||
|
if (setterArg != null) {
|
||||||
|
IVariableBinding fieldBinding = findFieldVariableBinding(
|
||||||
|
accessor.declaringFqn(), accessor.fieldName(), useSite);
|
||||||
|
if (fieldBinding != null) {
|
||||||
|
List<Expression> resolved = getReachingDefinitions(
|
||||||
|
setterArg, visited, paramBindings, fieldBindings, depth + 1);
|
||||||
|
fieldBindings.put(fieldBinding, resolved.isEmpty() ? setterArg : resolved.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (defNode != null) {
|
||||||
|
applyIntermediateMutations(
|
||||||
|
receiverName, defNode, useSite, enclosingMethod, fieldBindings, paramBindings, visited, depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ASTNode findVariableDefinitionNode(MethodDeclaration methodDeclaration, String varName) {
|
||||||
|
for (Object paramObj : methodDeclaration.parameters()) {
|
||||||
|
if (paramObj instanceof SingleVariableDeclaration svd
|
||||||
|
&& varName.equals(svd.getName().getIdentifier())) {
|
||||||
|
return svd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (methodDeclaration.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final ASTNode[] found = new ASTNode[1];
|
||||||
|
methodDeclaration.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(VariableDeclarationStatement node) {
|
||||||
|
for (Object fragmentObj : node.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
||||||
|
&& varName.equals(fragment.getName().getIdentifier())) {
|
||||||
|
found[0] = node;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return found[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Expression> inlineGetterMethodReturns(
|
||||||
|
AccessorSummary accessor,
|
||||||
|
TypeDeclaration ownerType,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> fieldBindings,
|
||||||
|
int depth) {
|
||||||
|
MethodDeclaration getter = context.findMethodDeclaration(ownerType, accessor.methodName(), true);
|
||||||
|
if (getter == null || getter.getBody() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Expression> results = new ArrayList<>();
|
||||||
|
for (ReturnStatement rs : findReturnStatements(getter.getBody())) {
|
||||||
|
if (rs.getExpression() != null) {
|
||||||
|
results.addAll(getReachingDefinitions(
|
||||||
|
rs.getExpression(), visited, paramBindings, fieldBindings, depth + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<IVariableBinding, Expression> buildTypeDefaultFieldBindings(TypeDeclaration typeDeclaration) {
|
||||||
|
Map<IVariableBinding, Expression> fieldBindings = new HashMap<>();
|
||||||
|
if (typeDeclaration == null) {
|
||||||
|
return fieldBindings;
|
||||||
|
}
|
||||||
|
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
|
||||||
|
for (Object fragmentObj : fieldDeclaration.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
||||||
|
&& fragment.getInitializer() != null) {
|
||||||
|
IVariableBinding binding = fragment.resolveBinding();
|
||||||
|
if (binding != null) {
|
||||||
|
fieldBindings.put(binding, fragment.getInitializer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fieldBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeDeclaration resolveExpressionTypeDeclaration(
|
||||||
|
Expression expr,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> instanceFieldBindings) {
|
||||||
|
if (expr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ITypeBinding typeBinding = expr.resolveTypeBinding();
|
||||||
|
if (typeBinding == null) {
|
||||||
|
IVariableBinding variableBinding = getVariableBinding(expr);
|
||||||
|
if (variableBinding != null && variableBinding.getType() != null) {
|
||||||
|
typeBinding = variableBinding.getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeBinding == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String fqn = typeBinding.getErasure().getQualifiedName();
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(fqn);
|
||||||
|
if (td == null && expr.getRoot() instanceof CompilationUnit cu) {
|
||||||
|
td = context.getTypeDeclaration(fqn, cu);
|
||||||
|
}
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
|
||||||
private void applyFlowSensitiveMutationsBeforeUse(
|
private void applyFlowSensitiveMutationsBeforeUse(
|
||||||
SimpleName receiverName,
|
SimpleName receiverName,
|
||||||
MethodInvocation useSite,
|
MethodInvocation useSite,
|
||||||
@@ -1130,7 +1418,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
if (declaringClassFqn != null && !declaringClassFqn.isEmpty()) {
|
if (declaringClassFqn != null && !declaringClassFqn.isEmpty()) {
|
||||||
// Find all known implementation subclasses in the codebase
|
// Find all known implementation subclasses in the codebase
|
||||||
List<String> implClassFqns = context.getImplementations(declaringClassFqn);
|
List<String> implClassFqns = context.getImplementations(declaringClassFqn);
|
||||||
List<String> currentPath = CURRENT_PATH.get();
|
List<String> currentPath = activeAnalysisPath();
|
||||||
if (currentPath != null && implClassFqns != null && !implClassFqns.isEmpty()) {
|
if (currentPath != null && implClassFqns != null && !implClassFqns.isEmpty()) {
|
||||||
Set<String> contextTypes = getCompatibleContextTypes(currentPath, declaringClassFqn);
|
Set<String> contextTypes = getCompatibleContextTypes(currentPath, declaringClassFqn);
|
||||||
List<String> filteredImpls = new ArrayList<>();
|
List<String> filteredImpls = new ArrayList<>();
|
||||||
@@ -1188,6 +1476,10 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> activeAnalysisPath() {
|
||||||
|
return context.getAnalysisCallPath();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isExpressionWrapping(Expression outer, Expression inner) {
|
private boolean isExpressionWrapping(Expression outer, Expression inner) {
|
||||||
Expression current = outer;
|
Expression current = outer;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
@@ -1380,6 +1672,9 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
if (cfg == null) return;
|
if (cfg == null) return;
|
||||||
|
|
||||||
ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, defNode);
|
ControlFlowGraph.CfgNode defCfgNode = findCfgNode(cfg, defNode);
|
||||||
|
if (defCfgNode == null && defNode instanceof SingleVariableDeclaration) {
|
||||||
|
defCfgNode = cfg.getEntryNode();
|
||||||
|
}
|
||||||
ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useNode);
|
ControlFlowGraph.CfgNode useCfgNode = findCfgNode(cfg, useNode);
|
||||||
|
|
||||||
if (defCfgNode != null && useCfgNode != null) {
|
if (defCfgNode != null && useCfgNode != null) {
|
||||||
@@ -1483,7 +1778,15 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return findMethodDeclarationInType(mb.getDeclaringClass(), mb);
|
return findMethodDeclarationInType(mb.getDeclaringClass(), mb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isSupplierLikeReceiver(Expression receiver) {
|
private static boolean isFunctionalSamInvocation(MethodInvocation mi) {
|
||||||
|
String methodName = mi.getName().getIdentifier();
|
||||||
|
if (!isKnownSamMethodName(methodName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("get".equals(methodName) && !mi.arguments().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Expression receiver = mi.getExpression();
|
||||||
if (receiver == null) {
|
if (receiver == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1491,23 +1794,27 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
if (typeBinding == null) {
|
if (typeBinding == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String fqn = typeBinding.getErasure().getQualifiedName();
|
ITypeBinding erasure = typeBinding.getErasure();
|
||||||
if (fqn == null) {
|
return isKnownFunctionalInterfaceName(erasure.getQualifiedName());
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return fqn.contains("Supplier") || fqn.contains("Function") || fqn.contains("Callable");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isRunnableReceiver(Expression receiver) {
|
private static boolean isKnownSamMethodName(String methodName) {
|
||||||
if (receiver == null) {
|
return switch (methodName) {
|
||||||
|
case "run", "get", "call", "accept", "apply", "test" -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isKnownFunctionalInterfaceName(String fqn) {
|
||||||
|
if (fqn == null || fqn.isBlank()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ITypeBinding typeBinding = receiver.resolveTypeBinding();
|
String simple = fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
|
||||||
if (typeBinding == null) {
|
return switch (simple) {
|
||||||
return false;
|
case "Runnable", "Supplier", "Function", "Callable", "Consumer", "BiConsumer", "Predicate", "BiFunction" ->
|
||||||
}
|
true;
|
||||||
String fqn = typeBinding.getErasure().getQualifiedName();
|
default -> fqn.contains("Supplier") || fqn.contains("Function") || fqn.contains("Callable");
|
||||||
return "java.lang.Runnable".equals(fqn) || fqn.endsWith(".Runnable");
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Expression> resolveFunctionalInterfaceInvocation(
|
private List<Expression> resolveFunctionalInterfaceInvocation(
|
||||||
@@ -1545,6 +1852,11 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
if (bound instanceof LambdaExpression lambda) {
|
if (bound instanceof LambdaExpression lambda) {
|
||||||
ASTNode body = lambda.getBody();
|
ASTNode body = lambda.getBody();
|
||||||
if (body instanceof Expression bodyExpr) {
|
if (body instanceof Expression bodyExpr) {
|
||||||
|
List<Expression> sideEffectArgs = collectVoidCallArgumentDefinitions(
|
||||||
|
bodyExpr, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
|
if (!sideEffectArgs.isEmpty()) {
|
||||||
|
return sideEffectArgs;
|
||||||
|
}
|
||||||
return getReachingDefinitions(bodyExpr, visited, paramBindings, instanceFieldBindings, depth);
|
return getReachingDefinitions(bodyExpr, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
}
|
}
|
||||||
if (body instanceof Block block) {
|
if (body instanceof Block block) {
|
||||||
@@ -1556,6 +1868,28 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return getReachingDefinitions(bound, visited, paramBindings, instanceFieldBindings, depth);
|
return getReachingDefinitions(bound, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Expression> collectVoidCallArgumentDefinitions(
|
||||||
|
Expression bodyExpr,
|
||||||
|
Set<ASTNode> visited,
|
||||||
|
Map<IVariableBinding, Expression> paramBindings,
|
||||||
|
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||||
|
int depth) {
|
||||||
|
if (!(bodyExpr instanceof MethodInvocation mi) || !isVoidMethodInvocation(mi)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Expression> results = new ArrayList<>();
|
||||||
|
for (Object argObj : mi.arguments()) {
|
||||||
|
results.addAll(getReachingDefinitions(
|
||||||
|
(Expression) argObj, visited, paramBindings, instanceFieldBindings, depth + 1));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isVoidMethodInvocation(MethodInvocation mi) {
|
||||||
|
IMethodBinding binding = mi.resolveMethodBinding();
|
||||||
|
return binding != null && binding.getReturnType() != null && "void".equals(binding.getReturnType().getName());
|
||||||
|
}
|
||||||
|
|
||||||
private void collectSideEffectExpressions(
|
private void collectSideEffectExpressions(
|
||||||
Block body,
|
Block body,
|
||||||
List<Expression> results,
|
List<Expression> results,
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ class PipelineRefactorTest {
|
|||||||
void shouldRecognizeReactiveReceiversAndFactoryMethods() {
|
void shouldRecognizeReactiveReceiversAndFactoryMethods() {
|
||||||
assertThat(LibraryUnwrapRegistry.isUnwrapReceiverType("Mono")).isTrue();
|
assertThat(LibraryUnwrapRegistry.isUnwrapReceiverType("Mono")).isTrue();
|
||||||
assertThat(LibraryUnwrapRegistry.isUnwrapMethodName("just")).isTrue();
|
assertThat(LibraryUnwrapRegistry.isUnwrapMethodName("just")).isTrue();
|
||||||
|
assertThat(LibraryUnwrapRegistry.isReactiveFactoryMethod("withPayload")).isTrue();
|
||||||
|
assertThat(LibraryUnwrapRegistry.isReactiveTransformMethod("flatMap")).isTrue();
|
||||||
assertThat(LibraryUnwrapRegistry.shouldStopUnwrap("buildPayload")).isTrue();
|
assertThat(LibraryUnwrapRegistry.shouldStopUnwrap("buildPayload")).isTrue();
|
||||||
assertThat(LibraryUnwrapRegistry.isEventSetterMethodName("eventType")).isTrue();
|
assertThat(LibraryUnwrapRegistry.isEventSetterMethodName("eventType")).isTrue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -647,8 +647,7 @@ public class ConstantResolverTest {
|
|||||||
MethodInvocation getCall = (MethodInvocation) ((VariableDeclarationFragment) fd.fragments().get(0)).getInitializer();
|
MethodInvocation getCall = (MethodInvocation) ((VariableDeclarationFragment) fd.fragments().get(0)).getInitializer();
|
||||||
|
|
||||||
ConstantExtractor extractor = new ConstantExtractor(context, new ConstantResolver(), mi -> null);
|
ConstantExtractor extractor = new ConstantExtractor(context, new ConstantResolver(), mi -> null);
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.setCurrentPath(
|
context.setAnalysisCallPath(List.of("com.example.ApiController.dispatch"));
|
||||||
List.of("com.example.ApiController.dispatch"));
|
|
||||||
try {
|
try {
|
||||||
ConstantResolver resolver = new ConstantResolver();
|
ConstantResolver resolver = new ConstantResolver();
|
||||||
SimpleName routesName = (SimpleName) getCall.getExpression();
|
SimpleName routesName = (SimpleName) getCall.getExpression();
|
||||||
@@ -660,7 +659,7 @@ public class ConstantResolverTest {
|
|||||||
extractor.extractConstantsFromExpression(getCall, constants);
|
extractor.extractConstantsFromExpression(getCall, constants);
|
||||||
assertThat(constants).contains("OrderEvent.PAY", "OrderEvent.SHIP");
|
assertThat(constants).contains("OrderEvent.PAY", "OrderEvent.SHIP");
|
||||||
} finally {
|
} finally {
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.clearCurrentPath();
|
context.clearAnalysisCallPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.LambdaExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
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 java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class CallSiteMatcherTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchCallSiteArgumentToSpecificEdgeWhenMultipleCallsExist(@TempDir Path tempDir) throws IOException {
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), """
|
||||||
|
package com.example;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
public enum OrderCommand { PAY, SHIP }
|
||||||
|
public class OrderController {
|
||||||
|
private final OrderService service;
|
||||||
|
public OrderController(OrderService service) { this.service = service; }
|
||||||
|
public void both() {
|
||||||
|
service.fireEvent(() -> OrderCommand.PAY);
|
||||||
|
service.fireEvent(() -> OrderCommand.SHIP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class OrderService {
|
||||||
|
void fireEvent(Supplier<OrderCommand> provider) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
CallEdge payEdge = new CallEdge(
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
List.of("() -> OrderCommand.PAY"));
|
||||||
|
CallEdge shipEdge = new CallEdge(
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
List.of("() -> OrderCommand.SHIP"));
|
||||||
|
|
||||||
|
MethodInvocation payInvocation = CallSiteMatcher.findMatchingInvocation(
|
||||||
|
"com.example.OrderController.both",
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
payEdge,
|
||||||
|
context);
|
||||||
|
MethodInvocation shipInvocation = CallSiteMatcher.findMatchingInvocation(
|
||||||
|
"com.example.OrderController.both",
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
shipEdge,
|
||||||
|
context);
|
||||||
|
|
||||||
|
assertThat(payInvocation).isNotNull();
|
||||||
|
assertThat(shipInvocation).isNotNull();
|
||||||
|
assertThat(payInvocation).isNotSameAs(shipInvocation);
|
||||||
|
assertThat(payInvocation.arguments().get(0)).isInstanceOf(LambdaExpression.class);
|
||||||
|
assertThat(shipInvocation.arguments().get(0)).isInstanceOf(LambdaExpression.class);
|
||||||
|
|
||||||
|
VariableTracer tracer = new VariableTracer(context, context.getConstantResolver());
|
||||||
|
tracer.setTypeResolver(new TypeResolver(context));
|
||||||
|
tracer.setConstantExtractor(new ConstantExtractor(context, context.getConstantResolver(), null));
|
||||||
|
|
||||||
|
assertThat(tracer.resolveConstantsFromCallSiteArgument(
|
||||||
|
"com.example.OrderController.both",
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
0,
|
||||||
|
payEdge)).containsExactly("OrderCommand.PAY");
|
||||||
|
assertThat(tracer.resolveConstantsFromCallSiteArgument(
|
||||||
|
"com.example.OrderController.both",
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
0,
|
||||||
|
shipEdge)).containsExactly("OrderCommand.SHIP");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSelectEdgeUsingPathBindings(@TempDir Path tempDir) throws IOException {
|
||||||
|
CallEdge payEdge = new CallEdge(
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
List.of("() -> OrderCommand.PAY"));
|
||||||
|
CallEdge shipEdge = new CallEdge(
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
List.of("() -> OrderCommand.SHIP"));
|
||||||
|
|
||||||
|
CallEdge selected = CallSiteMatcher.selectEdge(
|
||||||
|
"com.example.OrderController.both",
|
||||||
|
"com.example.OrderService.fireEvent",
|
||||||
|
List.of(payEdge, shipEdge),
|
||||||
|
Map.of("event", "OrderCommand.SHIP"));
|
||||||
|
|
||||||
|
assertThat(selected).isSameAs(shipEdge);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
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.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proves {@link JdtDataFlowModel} + anchored source AST resolves getter chains that detached
|
||||||
|
* re-parsing cannot, without relying on the call-graph accessor fallback tier.
|
||||||
|
*/
|
||||||
|
class DataFlowGetterEnablesMoreTest {
|
||||||
|
|
||||||
|
private CodebaseContext context;
|
||||||
|
private VariableTracer variableTracer;
|
||||||
|
private ConstantExtractor constantExtractor;
|
||||||
|
private JdtDataFlowModel dataFlowModel;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(@TempDir Path tempDir) throws IOException {
|
||||||
|
context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
ConstantResolver constantResolver = context.getConstantResolver();
|
||||||
|
variableTracer = new VariableTracer(context, constantResolver);
|
||||||
|
constantExtractor = new ConstantExtractor(context, constantResolver, null);
|
||||||
|
variableTracer.setConstantExtractor(constantExtractor);
|
||||||
|
dataFlowModel = new JdtDataFlowModel(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveChainedParameterGetterViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
|
||||||
|
scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
public void processOrderEvent(EventWrapper wrapper) {
|
||||||
|
wrapper.getEvent().getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class EventWrapper {
|
||||||
|
public RichEvent getEvent() { return new RichEvent(); }
|
||||||
|
}
|
||||||
|
class RichEvent {
|
||||||
|
public OrderEvents getType() { return OrderEvents.PAY; }
|
||||||
|
}
|
||||||
|
enum OrderEvents { PAY }
|
||||||
|
""");
|
||||||
|
|
||||||
|
String methodFqn = "com.example.OrderController.processOrderEvent";
|
||||||
|
String expressionText = "wrapper.getEvent().getType()";
|
||||||
|
|
||||||
|
assertThat(resolveViaDataflow(expressionText, methodFqn))
|
||||||
|
.anyMatch(value -> value.contains("PAY"));
|
||||||
|
|
||||||
|
assertThat(resolveViaDetachedParse(expressionText))
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSuperGetterFieldViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
|
||||||
|
scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class ChildController extends ParentController {
|
||||||
|
void process() {
|
||||||
|
super.getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ParentController {
|
||||||
|
private OrderEvent event = OrderEvent.PAY;
|
||||||
|
OrderEvent getEvent() { return event; }
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY, SHIP }
|
||||||
|
""");
|
||||||
|
|
||||||
|
String methodFqn = "com.example.ChildController.process";
|
||||||
|
String expressionText = "super.getEvent()";
|
||||||
|
|
||||||
|
assertThat(resolveViaDataflow(expressionText, methodFqn))
|
||||||
|
.anyMatch(value -> value.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveDeepFieldBackedChainViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
|
||||||
|
scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class CentralDispatcher {
|
||||||
|
void fire(Order order) {
|
||||||
|
order.getPayload().getEvent().getCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Order {
|
||||||
|
private Payload payload = new Payload();
|
||||||
|
Payload getPayload() { return payload; }
|
||||||
|
}
|
||||||
|
class Payload {
|
||||||
|
private Event event = new Event();
|
||||||
|
Event getEvent() { return event; }
|
||||||
|
}
|
||||||
|
class Event {
|
||||||
|
private OrderEvent code = OrderEvent.PAY;
|
||||||
|
OrderEvent getCode() { return code; }
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY, SHIP }
|
||||||
|
""");
|
||||||
|
|
||||||
|
String methodFqn = "com.example.CentralDispatcher.fire";
|
||||||
|
String expressionText = "order.getPayload().getEvent().getCode()";
|
||||||
|
|
||||||
|
assertThat(resolveViaDataflow(expressionText, methodFqn))
|
||||||
|
.anyMatch(value -> value.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveInterfaceMiddleHopViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
|
||||||
|
scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class ApiController {
|
||||||
|
Outer outer;
|
||||||
|
void pay() {
|
||||||
|
outer.getInner().getPayload().getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface Inner {
|
||||||
|
Payload getPayload();
|
||||||
|
}
|
||||||
|
class InnerImpl implements Inner {
|
||||||
|
private Payload payload = new Payload();
|
||||||
|
public Payload getPayload() { return payload; }
|
||||||
|
}
|
||||||
|
class Outer {
|
||||||
|
public Inner getInner() { return new InnerImpl(); }
|
||||||
|
}
|
||||||
|
class Payload {
|
||||||
|
private OrderEvent event = OrderEvent.PAY;
|
||||||
|
public OrderEvent getEvent() { return event; }
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY, SHIP }
|
||||||
|
""");
|
||||||
|
|
||||||
|
String methodFqn = "com.example.ApiController.pay";
|
||||||
|
String expressionText = "outer.getInner().getPayload().getEvent()";
|
||||||
|
|
||||||
|
assertThat(resolveViaDataflow(expressionText, methodFqn))
|
||||||
|
.anyMatch(value -> value.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveAnonymousClassGetterViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
|
||||||
|
scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class Runner {
|
||||||
|
void run() {
|
||||||
|
Handler handler = new Handler() {
|
||||||
|
public OrderEvent getEvent() { return OrderEvent.PAY; }
|
||||||
|
};
|
||||||
|
handler.getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface Handler {
|
||||||
|
OrderEvent getEvent();
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY, SHIP }
|
||||||
|
""");
|
||||||
|
|
||||||
|
String methodFqn = "com.example.Runner.run";
|
||||||
|
String expressionText = "handler.getEvent()";
|
||||||
|
|
||||||
|
assertThat(resolveViaDataflow(expressionText, methodFqn))
|
||||||
|
.anyMatch(value -> value.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSetterBeforeGetterOnParameterViaDataflow(@TempDir Path tempDir) throws IOException {
|
||||||
|
scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class Runner {
|
||||||
|
static class Payload {
|
||||||
|
private String event = "DEFAULT";
|
||||||
|
public void setEvent(String event) { this.event = event; }
|
||||||
|
public String getEvent() { return event; }
|
||||||
|
}
|
||||||
|
void run(Payload payload) {
|
||||||
|
payload.setEvent("PAY");
|
||||||
|
payload.getEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
String methodFqn = "com.example.Runner.run";
|
||||||
|
String expressionText = "payload.getEvent()";
|
||||||
|
|
||||||
|
assertThat(resolveViaDataflow(expressionText, methodFqn))
|
||||||
|
.anyMatch(value -> value.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveChainedGetterViaJdtDataFlowModelOnAnchoredAst(@TempDir Path tempDir) throws IOException {
|
||||||
|
scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
public void run(EventWrapper wrapper) {
|
||||||
|
OrderEvents value = wrapper.getEvent().getType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class EventWrapper {
|
||||||
|
public RichEvent getEvent() { return new RichEvent(); }
|
||||||
|
}
|
||||||
|
class RichEvent {
|
||||||
|
public OrderEvents getType() { return OrderEvents.PAY; }
|
||||||
|
}
|
||||||
|
enum OrderEvents { PAY }
|
||||||
|
""");
|
||||||
|
|
||||||
|
MethodInvocation chain = findMethodInvocation(
|
||||||
|
"com.example.Runner", "run", "wrapper.getEvent().getType()");
|
||||||
|
assertThat(chain).isNotNull();
|
||||||
|
|
||||||
|
String resolved = dataFlowModel.resolveValue(chain, context);
|
||||||
|
assertThat(resolved).contains("PAY");
|
||||||
|
|
||||||
|
Expression orphan = AstUtils.parseExpression("wrapper.getEvent().getType()");
|
||||||
|
assertThat(dataFlowModel.resolveValue(orphan, context))
|
||||||
|
.isEqualTo("wrapper.getEvent().getType()");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveViaDataflow(String expressionText, String methodFqn) {
|
||||||
|
List<String> searchMethods = List.of(methodFqn);
|
||||||
|
List<String> callPath = List.of(methodFqn);
|
||||||
|
return variableTracer.resolveConstantsFromSourceText(expressionText, searchMethods, callPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveViaDetachedParse(String expressionText) {
|
||||||
|
Expression orphan = AstUtils.parseExpression(expressionText);
|
||||||
|
if (orphan == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
for (Expression def : dataFlowModel.getReachingDefinitions(orphan)) {
|
||||||
|
if (def != null && !def.toString().equals(orphan.toString())) {
|
||||||
|
constantExtractor.extractConstantsFromExpression(def, constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodInvocation findMethodInvocation(String classFqn, String methodName, String expressionText) {
|
||||||
|
Expression found = AstUtils.findExpressionInMethod(classFqn + "." + methodName, expressionText, context);
|
||||||
|
return found instanceof MethodInvocation mi ? mi : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scan(Path tempDir, String source) throws IOException {
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), source);
|
||||||
|
context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
ConstantResolver constantResolver = context.getConstantResolver();
|
||||||
|
variableTracer = new VariableTracer(context, constantResolver);
|
||||||
|
constantExtractor = new ConstantExtractor(context, constantResolver, null);
|
||||||
|
variableTracer.setConstantExtractor(constantExtractor);
|
||||||
|
dataFlowModel = new JdtDataFlowModel(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,8 +31,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Regression suite for functional-interface event resolution ({@code Supplier.get()},
|
* Regression suite for functional-interface event resolution ({@code Supplier.get()},
|
||||||
* {@code Runnable.run()}, side-effect lambdas). Must stay on the dataflow pipeline:
|
* {@code Runnable.run()}, {@code Consumer.accept()}, {@code Callable.call()}, side-effect lambdas).
|
||||||
* {@link JdtDataFlowModel} → {@link VariableTracer} → {@link PathBindingEvaluator} → call graph.
|
* Must stay on the dataflow pipeline:
|
||||||
|
* {@link JdtDataFlowModel} → {@link VariableTracer} → {@link PathBindingEvaluator}
|
||||||
|
* → {@link PathBindingExpressionResolver} → call graph.
|
||||||
* Do not reintroduce a parallel {@code CallSiteArgumentResolver} bridge.
|
* Do not reintroduce a parallel {@code CallSiteArgumentResolver} bridge.
|
||||||
*/
|
*/
|
||||||
class FunctionalInterfaceDataFlowRegressionTest {
|
class FunctionalInterfaceDataFlowRegressionTest {
|
||||||
@@ -110,6 +112,87 @@ class FunctionalInterfaceDataFlowRegressionTest {
|
|||||||
Expression initializer = findInitializer(context, "com.example.Runner", "run", "value");
|
Expression initializer = findInitializer(context, "com.example.Runner", "run", "value");
|
||||||
assertThat(model.resolveValue(initializer, context)).isEqualTo("X");
|
assertThat(model.resolveValue(initializer, context)).isEqualTo("X");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEnumFromCallableCallOnLocalLambda(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
public enum OrderCommand { PAY, SHIP }
|
||||||
|
public class Hooks {
|
||||||
|
OrderCommand read() throws Exception {
|
||||||
|
Callable<OrderCommand> callable = () -> OrderCommand.PAY;
|
||||||
|
return callable.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
MethodInvocation call = findReturnMethodInvocation(context, "com.example.Hooks", "read");
|
||||||
|
assertThat(call.getName().getIdentifier()).isEqualTo("call");
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
for (Expression def : model.getReachingDefinitions(call)) {
|
||||||
|
new ConstantExtractor(context, context.getConstantResolver(), null)
|
||||||
|
.extractConstantsFromExpression(def, constants);
|
||||||
|
}
|
||||||
|
assertThat(constants).anyMatch(c -> c.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEnumFromConsumerAcceptSideEffectLambda(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
public enum OrderCommand { PAY, SHIP }
|
||||||
|
public class Hooks {
|
||||||
|
void run() {
|
||||||
|
Consumer<String> handler = ignored -> sendEvent(OrderCommand.PAY);
|
||||||
|
handler.accept("trigger");
|
||||||
|
}
|
||||||
|
void sendEvent(OrderCommand event) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
MethodInvocation acceptCall = findSideEffectCall(context, "com.example.Hooks", "run");
|
||||||
|
assertThat(acceptCall.getName().getIdentifier()).isEqualTo("accept");
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
for (Expression def : model.getReachingDefinitions(acceptCall)) {
|
||||||
|
new ConstantExtractor(context, context.getConstantResolver(), null)
|
||||||
|
.extractConstantsFromExpression(def, constants);
|
||||||
|
}
|
||||||
|
assertThat(constants).anyMatch(c -> c.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEnumThroughReactiveTransformLambdaBinding(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
public enum OrderCommand { PAY, SHIP }
|
||||||
|
public class Hooks {
|
||||||
|
OrderCommand read() {
|
||||||
|
return Mono.just(OrderCommand.PAY).map(x -> x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Mono {
|
||||||
|
static Mono just(Object o) { return new Mono(); }
|
||||||
|
OrderCommand map(java.util.function.Function fn) { return OrderCommand.PAY; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
MethodInvocation mapCall = findReturnMethodInvocation(context, "com.example.Hooks", "read");
|
||||||
|
assertThat(mapCall.getName().getIdentifier()).isEqualTo("map");
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
for (Expression def : model.getReachingDefinitions(mapCall)) {
|
||||||
|
new ConstantExtractor(context, context.getConstantResolver(), null)
|
||||||
|
.extractConstantsFromExpression(def, constants);
|
||||||
|
}
|
||||||
|
assertThat(constants).anyMatch(c -> c.contains("PAY"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@@ -282,6 +365,43 @@ class FunctionalInterfaceDataFlowRegressionTest {
|
|||||||
assertSinglePayEvent(chains(project, "com.example.service.AbstractOrderService", "sendEvent"));
|
assertSinglePayEvent(chains(project, "com.example.service.AbstractOrderService", "sendEvent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLinkConsumerAcceptSideEffectLambda(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
public enum OrderCommand { PAY, SHIP }
|
||||||
|
public class OrderService {
|
||||||
|
private final EventDispatcher dispatcher = new EventDispatcher();
|
||||||
|
public void processOrder() {
|
||||||
|
dispatcher.dispatch("order", ignored -> fireEvent());
|
||||||
|
}
|
||||||
|
public void fireEvent() { sendEvent(OrderCommand.PAY); }
|
||||||
|
void sendEvent(OrderCommand event) {}
|
||||||
|
}
|
||||||
|
class EventDispatcher {
|
||||||
|
public void dispatch(String order, Consumer<String> callback) {
|
||||||
|
callback.accept("trigger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
List<CallChain> chains = engine.findChains(
|
||||||
|
List.of(EntryPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("processOrder")
|
||||||
|
.build()),
|
||||||
|
List.of(TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.event("event")
|
||||||
|
.build()));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("OrderCommand.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReachFireSiteThroughRunnableRunHop(@TempDir Path tempDir) throws IOException {
|
void shouldReachFireSiteThroughRunnableRunHop(@TempDir Path tempDir) throws IOException {
|
||||||
RunnableProject project = RunnableProject.withExecutor(tempDir);
|
RunnableProject project = RunnableProject.withExecutor(tempDir);
|
||||||
@@ -504,4 +624,47 @@ class FunctionalInterfaceDataFlowRegressionTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class GetterChainDataFlowLayer {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveChainedGetterThroughDataflowBeforeAccessorFallback(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
public class OrderController {
|
||||||
|
private OrderService service;
|
||||||
|
public void processOrderEvent(EventWrapper wrapper) {
|
||||||
|
service.updateOrderState(wrapper.getEvent().getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class OrderService {
|
||||||
|
public void updateOrderState(OrderEvents event) {}
|
||||||
|
}
|
||||||
|
class EventWrapper {
|
||||||
|
public RichEvent getEvent() { return new RichEvent(); }
|
||||||
|
}
|
||||||
|
class RichEvent {
|
||||||
|
public OrderEvents getType() { return OrderEvents.PAY; }
|
||||||
|
}
|
||||||
|
enum OrderEvents { PAY }
|
||||||
|
""");
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
List<CallChain> chains = engine.findChains(
|
||||||
|
List.of(EntryPoint.builder()
|
||||||
|
.className("com.example.OrderController")
|
||||||
|
.methodName("processOrderEvent")
|
||||||
|
.build()),
|
||||||
|
List.of(TriggerPoint.builder()
|
||||||
|
.className("com.example.OrderService")
|
||||||
|
.methodName("updateOrderState")
|
||||||
|
.event("event")
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder("OrderEvents.PAY");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,24 @@ class FunctionalInterfaceTypesTest {
|
|||||||
.isTrue();
|
.isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecognizeConsumerSamMethod() {
|
||||||
|
assertThat(FunctionalInterfaceTypes.isFunctionalSamMethod("java.util.function.Consumer.accept"))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRecognizeRunnableSamMethod() {
|
||||||
|
assertThat(FunctionalInterfaceTypes.isFunctionalSamMethod("java.lang.Runnable.run"))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectMapGetAsSamMethod() {
|
||||||
|
assertThat(FunctionalInterfaceTypes.isFunctionalSamMethod("java.util.Map.get"))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAcceptLambdaIntoSupplierParameter() {
|
void shouldAcceptLambdaIntoSupplierParameter() {
|
||||||
assertThat(FunctionalInterfaceTypes.isProvablyResolvedCallSiteArgument(
|
assertThat(FunctionalInterfaceTypes.isProvablyResolvedCallSiteArgument(
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PathBindingExpressionResolverTest {
|
||||||
|
|
||||||
|
private CodebaseContext context;
|
||||||
|
private PathBindingExpressionResolver resolver;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp(@TempDir Path tempDir) throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
enum DomainCommand { ORDER_PAY, ORDER_SHIP }
|
||||||
|
class CommandGateway {
|
||||||
|
private final StringCommandMapper mapper = new StringCommandMapper();
|
||||||
|
void executeViaMapper(String commandKey) {
|
||||||
|
DomainCommand command = mapper.fromString(commandKey);
|
||||||
|
dispatch(command);
|
||||||
|
}
|
||||||
|
void dispatch(DomainCommand command) {}
|
||||||
|
}
|
||||||
|
class StringCommandMapper {
|
||||||
|
DomainCommand fromString(String commandKey) {
|
||||||
|
return switch (commandKey) {
|
||||||
|
case "order.pay" -> DomainCommand.ORDER_PAY;
|
||||||
|
case "order.ship" -> DomainCommand.ORDER_SHIP;
|
||||||
|
default -> throw new IllegalArgumentException(commandKey);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), source);
|
||||||
|
|
||||||
|
context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
VariableTracer variableTracer = new VariableTracer(context, context.getConstantResolver());
|
||||||
|
resolver = new PathBindingExpressionResolver(context, variableTracer, context.getConstantResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveMapperFromStringViaDataflowWithNamedBindings() {
|
||||||
|
String resolved = resolver.resolveMethodCallFromSource(
|
||||||
|
"com.example.CommandGateway.executeViaMapper",
|
||||||
|
"command",
|
||||||
|
Map.of("commandKey", "order.pay"));
|
||||||
|
|
||||||
|
assertThat(resolved).isEqualTo("DomainCommand.ORDER_PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveAlternateSwitchArmViaDataflowNamedBindings() {
|
||||||
|
String resolved = resolver.resolveMethodCallFromSource(
|
||||||
|
"com.example.CommandGateway.executeViaMapper",
|
||||||
|
"command",
|
||||||
|
Map.of("commandKey", "order.ship"));
|
||||||
|
|
||||||
|
assertThat(resolved).isEqualTo("DomainCommand.ORDER_SHIP");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user