Fix CIC accessor resolution, concrete overrides, and reactive flatMap tracing.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -184,25 +184,42 @@ public final class AccessorResolver {
|
||||
|| org.eclipse.jdt.core.dom.Modifier.isAbstract(ownerType.getModifiers());
|
||||
|
||||
if (widenToImplementations) {
|
||||
List<String> widened = new ArrayList<>();
|
||||
List<String> impls = context.getImplementations(ownerFqn);
|
||||
if (impls != null) {
|
||||
for (String implFqn : impls) {
|
||||
TypeDeclaration implType = context.getTypeDeclaration(implFqn);
|
||||
widened.addAll(resolve(
|
||||
implFqn,
|
||||
List<String> results = new ArrayList<>();
|
||||
|
||||
if (receiverContext != null && receiverContext.cic() != null) {
|
||||
String cicOwnerFqn = resolveCicOwnerFqn(receiverContext);
|
||||
if (cicOwnerFqn != null) {
|
||||
TypeDeclaration cicOwnerType = context.getTypeDeclaration(
|
||||
cicOwnerFqn, receiverContext.contextCu());
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(cicOwnerFqn, methodName);
|
||||
ReceiverContext cicReceiverContext = new ReceiverContext(
|
||||
cicOwnerType,
|
||||
receiverContext.cic(),
|
||||
receiverContext.contextCu(),
|
||||
receiverContext.anonymousGetter(),
|
||||
receiverContext.fieldValues());
|
||||
List<String> cicValues = resolveWithCic(
|
||||
cicOwnerFqn,
|
||||
methodName,
|
||||
new ReceiverContext(implType, null,
|
||||
receiverContext != null ? receiverContext.contextCu() : null,
|
||||
false,
|
||||
receiverContext != null ? receiverContext.fieldValues() : null),
|
||||
budget,
|
||||
visited));
|
||||
cicReceiverContext,
|
||||
indexedAccessor.orElse(null),
|
||||
visited);
|
||||
if (!cicValues.isEmpty()) {
|
||||
return deduplicatePreservingOrder(cicValues);
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
if (!widened.isEmpty()) {
|
||||
return deduplicatePreservingOrder(widened);
|
||||
|
||||
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
|
||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
||||
results.addAll(constantExtractor.resolveMethodReturnConstant(
|
||||
ownerFqn, methodName, 0, visited, contextCu, budget));
|
||||
}
|
||||
if (!results.isEmpty()) {
|
||||
return deduplicatePreservingOrder(results);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, methodName);
|
||||
@@ -403,6 +420,28 @@ public final class AccessorResolver {
|
||||
return accessorOpt.get().fieldName();
|
||||
}
|
||||
|
||||
private String resolveCicOwnerFqn(ReceiverContext receiverContext) {
|
||||
if (receiverContext == null || receiverContext.cic() == null) {
|
||||
return null;
|
||||
}
|
||||
ClassInstanceCreation cic = receiverContext.cic();
|
||||
CompilationUnit contextCu = receiverContext.contextCu();
|
||||
String simpleName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType());
|
||||
TypeDeclaration cicType = contextCu != null
|
||||
? context.getTypeDeclaration(simpleName, contextCu)
|
||||
: null;
|
||||
if (cicType == null) {
|
||||
cicType = context.getTypeDeclaration(simpleName);
|
||||
}
|
||||
if (cicType != null) {
|
||||
return context.getFqn(cicType);
|
||||
}
|
||||
if (cic.resolveTypeBinding() != null) {
|
||||
return cic.resolveTypeBinding().getErasure().getQualifiedName();
|
||||
}
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
private static List<String> deduplicatePreservingOrder(List<String> values) {
|
||||
List<String> deduped = new ArrayList<>();
|
||||
for (String value : values) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.LambdaExpression;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.eclipse.jdt.core.dom.ASTNode;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@@ -21,14 +23,129 @@ public final class ReactiveExpressionSupport {
|
||||
}
|
||||
|
||||
public static String extractPayload(Expression expression, ConstantResolver constantResolver, CodebaseContext context) {
|
||||
return extractPayload(expression, null, constantResolver, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites {@code p.getEvent()} inside a {@code flatMap(p -> ...)} lambda to {@code payload.getEvent()}
|
||||
* when {@code p} is fed by {@code Mono.just(payload)} (or a chained reactive source).
|
||||
*/
|
||||
public static String remapLambdaParameterGetter(
|
||||
Expression eventExpr,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (!(eventExpr instanceof MethodInvocation getterCall)) {
|
||||
return null;
|
||||
}
|
||||
if (!(getterCall.getExpression() instanceof SimpleName paramName)) {
|
||||
return null;
|
||||
}
|
||||
LambdaExpression lambda = findEnclosingLambda(getterCall);
|
||||
if (lambda == null) {
|
||||
return null;
|
||||
}
|
||||
MethodInvocation flatMapCall = findEnclosingFlatMap(lambda);
|
||||
if (flatMapCall == null) {
|
||||
return null;
|
||||
}
|
||||
String mappedReceiver = mapLambdaParameterToSource(
|
||||
lambda,
|
||||
paramName.getIdentifier(),
|
||||
flatMapCall.getExpression(),
|
||||
constantResolver,
|
||||
context);
|
||||
if (mappedReceiver == null) {
|
||||
return null;
|
||||
}
|
||||
return mappedReceiver + "." + getterCall.getName().getIdentifier() + "()";
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #remapLambdaParameterGetter(Expression, ConstantResolver, CodebaseContext)} but locates
|
||||
* the getter in a real method body when {@code expressionText} came from a detached parse tree.
|
||||
*/
|
||||
public static String remapLambdaParameterGetterInMethod(
|
||||
String expressionText,
|
||||
String methodFqn,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
MethodInvocation getter = findMethodInvocationInMethod(methodFqn, expressionText, context);
|
||||
if (getter == null) {
|
||||
return null;
|
||||
}
|
||||
String remapped = remapLambdaParameterGetter(getter, constantResolver, context);
|
||||
if (remapped != null) {
|
||||
return remapped;
|
||||
}
|
||||
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) {
|
||||
ASTNode current = node.getParent();
|
||||
while (current != null) {
|
||||
if (current instanceof LambdaExpression lambda) {
|
||||
return lambda;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MethodInvocation findEnclosingFlatMap(ASTNode node) {
|
||||
ASTNode current = node.getParent();
|
||||
while (current != null) {
|
||||
if (current instanceof MethodInvocation mi && "flatMap".equals(mi.getName().getIdentifier())) {
|
||||
return mi;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String extractPayload(
|
||||
Expression expression,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (expression == null) {
|
||||
return null;
|
||||
}
|
||||
if (expression instanceof MethodInvocation mi) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
if ("flatMap".equals(methodName) && !mi.arguments().isEmpty()) {
|
||||
String fromArgument = extractFlatMapArgumentPayload((Expression) mi.arguments().get(0),
|
||||
constantResolver, context);
|
||||
String fromArgument = extractFlatMapArgumentPayload(
|
||||
(Expression) mi.arguments().get(0), mi.getExpression(), constantResolver, context);
|
||||
if (fromArgument != null) {
|
||||
return fromArgument;
|
||||
}
|
||||
@@ -44,7 +161,7 @@ public final class ReactiveExpressionSupport {
|
||||
return arg.toString();
|
||||
}
|
||||
if (mi.getExpression() != null) {
|
||||
return extractPayload(mi.getExpression(), constantResolver, context);
|
||||
return extractPayload(mi.getExpression(), flatMapReceiver, constantResolver, context);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -52,15 +169,131 @@ public final class ReactiveExpressionSupport {
|
||||
|
||||
private static String extractFlatMapArgumentPayload(
|
||||
Expression argument,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (argument instanceof LambdaExpression lambda) {
|
||||
Expression bodyExpression = lambdaBodyExpression(lambda);
|
||||
if (bodyExpression != null) {
|
||||
return extractPayload(bodyExpression, constantResolver, context);
|
||||
String lambdaPayload = extractLambdaBodyPayload(
|
||||
bodyExpression, lambda, flatMapReceiver, constantResolver, context);
|
||||
if (lambdaPayload != null) {
|
||||
return lambdaPayload;
|
||||
}
|
||||
return extractPayload(bodyExpression, flatMapReceiver, constantResolver, context);
|
||||
}
|
||||
}
|
||||
return extractPayload(argument, constantResolver, context);
|
||||
return extractPayload(argument, flatMapReceiver, constantResolver, context);
|
||||
}
|
||||
|
||||
private static String extractLambdaBodyPayload(
|
||||
Expression bodyExpression,
|
||||
LambdaExpression lambda,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (!(bodyExpression instanceof MethodInvocation factoryCall)) {
|
||||
return null;
|
||||
}
|
||||
if (!REACTIVE_FACTORY_METHODS.contains(factoryCall.getName().getIdentifier())
|
||||
|| factoryCall.arguments().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Expression factoryArg = (Expression) factoryCall.arguments().get(0);
|
||||
if (!(factoryArg instanceof MethodInvocation getterCall)) {
|
||||
return null;
|
||||
}
|
||||
String getterSuffix = "." + getterCall.getName().getIdentifier() + "()";
|
||||
Expression getterReceiver = getterCall.getExpression();
|
||||
if (getterReceiver instanceof SimpleName paramName) {
|
||||
String mappedReceiver = mapLambdaParameterToSource(
|
||||
lambda, paramName.getIdentifier(), flatMapReceiver, constantResolver, context);
|
||||
if (mappedReceiver != null) {
|
||||
return mappedReceiver + getterSuffix;
|
||||
}
|
||||
}
|
||||
if (getterReceiver != null) {
|
||||
return getterReceiver.toString() + getterSuffix;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String mapLambdaParameterToSource(
|
||||
LambdaExpression lambda,
|
||||
String paramName,
|
||||
Expression flatMapReceiver,
|
||||
ConstantResolver constantResolver,
|
||||
CodebaseContext context) {
|
||||
if (lambda.parameters().size() != 1) {
|
||||
return null;
|
||||
}
|
||||
Object parameter = lambda.parameters().get(0);
|
||||
if (!(parameter instanceof org.eclipse.jdt.core.dom.VariableDeclaration variableDeclaration)) {
|
||||
return null;
|
||||
}
|
||||
if (!paramName.equals(variableDeclaration.getName().getIdentifier())) {
|
||||
return null;
|
||||
}
|
||||
if (flatMapReceiver instanceof MethodInvocation receiverFlatMap
|
||||
&& "flatMap".equals(receiverFlatMap.getName().getIdentifier())
|
||||
&& !receiverFlatMap.arguments().isEmpty()
|
||||
&& receiverFlatMap.arguments().get(0) instanceof LambdaExpression feederLambda
|
||||
&& feederLambda != lambda) {
|
||||
Expression feederBody = lambdaBodyExpression(feederLambda);
|
||||
if (feederBody != null) {
|
||||
String feederPayload = extractLambdaBodyPayload(
|
||||
feederBody, feederLambda, receiverFlatMap.getExpression(), constantResolver, context);
|
||||
if (feederPayload != null) {
|
||||
return feederPayload;
|
||||
}
|
||||
String extracted = extractPayload(
|
||||
feederBody, receiverFlatMap.getExpression(), constantResolver, context);
|
||||
if (extracted != null) {
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
}
|
||||
Expression source = peelJustArgument(flatMapReceiver);
|
||||
if (source instanceof MethodInvocation getterMi
|
||||
&& getterMi.getExpression() instanceof SimpleName innerParamName) {
|
||||
LambdaExpression outerLambda = findEnclosingLambda(lambda);
|
||||
if (outerLambda != null && outerLambda != lambda) {
|
||||
MethodInvocation outerFlatMap = findEnclosingFlatMap(outerLambda);
|
||||
if (outerFlatMap != null) {
|
||||
String mappedBase = mapLambdaParameterToSource(
|
||||
outerLambda,
|
||||
innerParamName.getIdentifier(),
|
||||
outerFlatMap.getExpression(),
|
||||
constantResolver,
|
||||
context);
|
||||
if (mappedBase != null) {
|
||||
return mappedBase + "." + getterMi.getName().getIdentifier() + "()";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (source != null) {
|
||||
if (constantResolver != null && context != null) {
|
||||
String resolved = constantResolver.resolve(source, context);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return source.toString();
|
||||
}
|
||||
return extractPayload(flatMapReceiver, null, constantResolver, context);
|
||||
}
|
||||
|
||||
private static Expression peelJustArgument(Expression expression) {
|
||||
if (expression instanceof MethodInvocation mi
|
||||
&& REACTIVE_FACTORY_METHODS.contains(mi.getName().getIdentifier())
|
||||
&& !mi.arguments().isEmpty()) {
|
||||
return (Expression) mi.arguments().get(0);
|
||||
}
|
||||
if (expression instanceof MethodInvocation mi && mi.getExpression() != null) {
|
||||
return peelJustArgument(mi.getExpression());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Expression lambdaBodyExpression(LambdaExpression lambda) {
|
||||
|
||||
@@ -201,7 +201,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
for (int i = path.size() - 1; i > 0; i--) {
|
||||
String target = path.get(i);
|
||||
String caller = path.get(i - 1);
|
||||
int paramIndex = typeResolver.getParameterIndex(target, currentParamName);
|
||||
boolean allowSyntheticPlaceholder = i == path.size() - 1;
|
||||
int paramIndex = typeResolver.getParameterIndex(target, currentParamName, allowSyntheticPlaceholder);
|
||||
if (paramIndex < 0) {
|
||||
if ("this".equals(currentParamName)) {
|
||||
String actualReceiver = null;
|
||||
@@ -328,7 +329,15 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
String entryMethod = path.get(0);
|
||||
|
||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||
String getterExpression = resolvedValue + methodSuffix;
|
||||
List<String> chainedGetterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
||||
if (chainedGetterEvents == null || chainedGetterEvents.isEmpty()) {
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
|
||||
getterExpression, entryMethod, constantResolver, context);
|
||||
if (remapped != null) {
|
||||
chainedGetterEvents = evaluateGetterOnReceiver(remapped, "", entryMethod, path, callGraph);
|
||||
}
|
||||
}
|
||||
if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
|
||||
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
||||
@@ -421,6 +430,19 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
|
||||
if (exprNode instanceof Expression expressionNode) {
|
||||
String remappedGetter = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
expressionNode, constantResolver, context);
|
||||
if (remappedGetter == null) {
|
||||
remappedGetter = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
|
||||
resolvedValue, entryMethod, constantResolver, context);
|
||||
}
|
||||
if (remappedGetter != null) {
|
||||
List<String> remappedEvents = evaluateGetterOnReceiver(
|
||||
remappedGetter, "", entryMethod, path, callGraph);
|
||||
if (remappedEvents != null && !remappedEvents.isEmpty()) {
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, remappedEvents);
|
||||
}
|
||||
}
|
||||
String reactivePayload = ReactiveExpressionSupport.extractPayload(
|
||||
expressionNode, constantResolver, context);
|
||||
if (reactivePayload != null) {
|
||||
@@ -896,7 +918,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
});
|
||||
|
||||
String targetMethod = path.get(path.size() - 1);
|
||||
int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event);
|
||||
int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event, true);
|
||||
if (eventParamIndex < 0) {
|
||||
eventParamIndex = 0;
|
||||
}
|
||||
@@ -965,6 +987,29 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
.build();
|
||||
}
|
||||
|
||||
if (polymorphicEvents.isEmpty()) {
|
||||
String[] extracted = extractMethodSuffix(resolvedValue, "", entryMethod);
|
||||
if (expressionAccessClassifier.isTraceableAccessorSuffix(extracted[1])) {
|
||||
String getterExpression = extracted[0] + extracted[1];
|
||||
List<String> getterEvents = evaluateGetterOnReceiver(
|
||||
extracted[0], extracted[1], entryMethod, path, callGraph);
|
||||
if (getterEvents == null || getterEvents.isEmpty()) {
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
|
||||
getterExpression, entryMethod, constantResolver, context);
|
||||
if (remapped != null) {
|
||||
getterEvents = evaluateGetterOnReceiver(remapped, "", entryMethod, path, callGraph);
|
||||
}
|
||||
}
|
||||
if (getterEvents != null && !getterEvents.isEmpty()) {
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, getterEvents);
|
||||
}
|
||||
}
|
||||
if (exprNode instanceof ClassInstanceCreation cic) {
|
||||
polymorphicEvents.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
|
||||
}
|
||||
}
|
||||
|
||||
return tp;
|
||||
} finally {
|
||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.clearCurrentPath();
|
||||
@@ -1326,7 +1371,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
} else if (exprNode instanceof MethodInvocation mi) {
|
||||
String getterName = mi.getName().getIdentifier();
|
||||
String propName = methodSuffix.startsWith(".get") ? methodSuffix.substring(4) : methodSuffix.substring(1);
|
||||
String propName = methodSuffix != null && !methodSuffix.isEmpty()
|
||||
? (methodSuffix.startsWith(".get") ? methodSuffix.substring(4) : methodSuffix.substring(1))
|
||||
: getterName;
|
||||
Expression receiver = mi.getExpression();
|
||||
String receiverName = null;
|
||||
String receiverType = null;
|
||||
@@ -1379,6 +1426,18 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
receiverType = variableTracer.getVariableDeclaredType(callerMethod, receiverName);
|
||||
}
|
||||
|
||||
if (receiverType == null && receiver instanceof SimpleName) {
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
mi, constantResolver, context);
|
||||
if (remapped == null) {
|
||||
remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
|
||||
mi.toString(), scopeMethod, constantResolver, context);
|
||||
}
|
||||
if (remapped != null) {
|
||||
return evaluateGetterOnReceiver(remapped, "", entryMethod, path, callGraph);
|
||||
}
|
||||
}
|
||||
|
||||
if (receiverType != null) {
|
||||
String simpleReceiverType = receiverType;
|
||||
if (receiverType.contains("<")) {
|
||||
|
||||
@@ -447,6 +447,19 @@ public class ConstantExtractor {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (widenToImplementations) {
|
||||
List<String> impls = context.getImplementations(className);
|
||||
if (impls != null) {
|
||||
for (String implName : impls) {
|
||||
List<String> delegationResult = resolveMethodReturnConstant(
|
||||
implName, methodName, depth + 1, visited, contextCu, activeBudget);
|
||||
for (String value : delegationResult) {
|
||||
if (!constants.contains(value)) {
|
||||
constants.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visited.remove(fqn);
|
||||
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
|
||||
|
||||
@@ -238,6 +238,11 @@ public class GenericEventDetector {
|
||||
} else {
|
||||
eventValue = context.resolveExpression(eventExpr);
|
||||
}
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
eventExpr, constantResolver, context);
|
||||
if (remapped != null) {
|
||||
eventValue = remapped;
|
||||
}
|
||||
}
|
||||
|
||||
MethodDeclaration method = findEnclosingMethod(node);
|
||||
|
||||
@@ -7,11 +7,21 @@ import java.util.*;
|
||||
public class TypeResolver {
|
||||
private final CodebaseContext context;
|
||||
|
||||
private static final Set<String> GENERIC_EVENT_PARAMETER_NAMES = Set.of("e", "ev");
|
||||
|
||||
public TypeResolver(CodebaseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public int getParameterIndex(String methodFqn, String paramName) {
|
||||
return getParameterIndex(methodFqn, paramName, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* When {@code allowSyntheticEventPlaceholder} is true, maps generic trigger placeholders
|
||||
* such as {@code e} onto the sole parameter of single-arg methods (e.g. {@code sendEvent}).
|
||||
*/
|
||||
public int getParameterIndex(String methodFqn, String paramName, boolean allowSyntheticEventPlaceholder) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
@@ -25,11 +35,20 @@ public class TypeResolver {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (allowSyntheticEventPlaceholder
|
||||
&& md.parameters().size() == 1
|
||||
&& isGenericEventParameterName(paramName)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean isGenericEventParameterName(String paramName) {
|
||||
return paramName != null && GENERIC_EVENT_PARAMETER_NAMES.contains(paramName);
|
||||
}
|
||||
|
||||
public List<String> getParameterNames(String methodFqn) {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||
return null;
|
||||
|
||||
@@ -336,19 +336,6 @@ public class VariableTracer {
|
||||
String fieldType = getFieldType(td, cleanFieldName, context, new java.util.HashSet<>());
|
||||
if (fieldType != null) return fieldType;
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: ask constantExtractor to resolve it via method return constant
|
||||
if (constantExtractor != null && methodFqn.contains(".")) {
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
CompilationUnit cu = td != null && td.getRoot() instanceof CompilationUnit ? (CompilationUnit) td.getRoot() : null;
|
||||
List<String> values = constantExtractor.resolveMethodReturnConstant(cleanFieldName, methodName, 0, new HashSet<>(), cu);
|
||||
if (values != null && !values.isEmpty()) {
|
||||
// If it resolves to some class constant, we could infer type or return a fallback
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -346,9 +347,11 @@ public class CodebaseContext {
|
||||
if (interfaceName != null && interfaceName.contains("<")) {
|
||||
cleanName = interfaceName.substring(0, interfaceName.indexOf('<'));
|
||||
}
|
||||
Set<String> allImpls = new HashSet<>();
|
||||
Set<String> allImpls = new LinkedHashSet<>();
|
||||
collectImplementations(cleanName, allImpls, new HashSet<>());
|
||||
return new ArrayList<>(allImpls);
|
||||
List<String> sorted = new ArrayList<>(allImpls);
|
||||
Collections.sort(sorted);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
||||
|
||||
@@ -372,6 +372,11 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
if (concreteAccessor.isPresent()) {
|
||||
accessor = concreteAccessor;
|
||||
} else if (shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
|
||||
List<Expression> concreteOverride = inlineConcreteMethodOverride(
|
||||
methodName, receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
||||
if (!concreteOverride.isEmpty()) {
|
||||
return concreteOverride;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -457,6 +462,42 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<Expression> inlineConcreteMethodOverride(
|
||||
String methodName,
|
||||
List<Expression> receiverDefs,
|
||||
Set<ASTNode> visited,
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||
int depth) {
|
||||
List<Expression> results = new ArrayList<>();
|
||||
for (Expression receiverDef : receiverDefs) {
|
||||
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
|
||||
if (concreteFqn == null) {
|
||||
continue;
|
||||
}
|
||||
TypeDeclaration concreteType = context.getTypeDeclaration(concreteFqn);
|
||||
if (concreteType == null) {
|
||||
continue;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(concreteType, methodName, true);
|
||||
if (method == null || method.getBody() == null) {
|
||||
continue;
|
||||
}
|
||||
for (Object statement : method.getBody().statements()) {
|
||||
if (statement instanceof ReturnStatement returnStatement
|
||||
&& returnStatement.getExpression() != null) {
|
||||
results.addAll(getReachingDefinitions(
|
||||
returnStatement.getExpression(),
|
||||
visited,
|
||||
paramBindings,
|
||||
instanceFieldBindings,
|
||||
depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private String concreteReceiverTypeFqn(Expression receiverDef) {
|
||||
if (receiverDef instanceof ClassInstanceCreation cic) {
|
||||
return AccessorInlining.resolveTypeFqn(cic);
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
|
||||
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InheritanceAndNestedResolutionTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
CodebaseContext context;
|
||||
ConstantExtractor constantExtractor;
|
||||
ConstructorAnalyzer constructorAnalyzer;
|
||||
AccessorResolver accessorResolver;
|
||||
VariableTracer variableTracer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
|
||||
ConstantResolver constantResolver = context.getConstantResolver();
|
||||
constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
|
||||
constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
|
||||
variableTracer = new VariableTracer(context, constantResolver);
|
||||
constantExtractor.setVariableTracer(variableTracer);
|
||||
constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
|
||||
constructorAnalyzer.setVariableTracer(variableTracer);
|
||||
constructorAnalyzer.setConstantExtractor(constantExtractor);
|
||||
accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
||||
constantExtractor.setAccessorResolver(accessorResolver);
|
||||
}
|
||||
|
||||
private void writeJava(String relativePath, String source) throws IOException {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, source);
|
||||
}
|
||||
|
||||
private void scan() throws IOException {
|
||||
context.scan(tempDir);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class InterfaceDefaultMethod {
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldIncludeDefaultMethodAndAllImpls() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { DEFAULT, PAY, SHIP }
|
||||
""");
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default Code getCode() { return Code.DEFAULT; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public Code getCode() { return Code.PAY; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public Code getCode() { return Code.SHIP; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
null, null, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.DEFAULT", "Code.PAY", "Code.SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldMergeDefaultMethodWithConcreteOverrides() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default String getCode() { return "DEFAULT"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public String getCode() { return "PAY"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ShipEvent.java", """
|
||||
package com.example;
|
||||
public class ShipEvent implements EventSource {
|
||||
public String getCode() { return "SHIP"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
null, null, false, null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("DEFAULT", "PAY", "SHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessorResolverShouldPreferConcreteCicOverInterfaceDefault() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { DEFAULT, PAY, SHIP }
|
||||
""");
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
default Code getCode() { return Code.DEFAULT; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/PayEvent.java", """
|
||||
package com.example;
|
||||
public class PayEvent implements EventSource {
|
||||
public Code getCode() { return Code.PAY; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
void run() {
|
||||
EventSource src = new PayEvent();
|
||||
}
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) runner.getRoot();
|
||||
final org.eclipse.jdt.core.dom.ClassInstanceCreation[] payEventCic =
|
||||
new org.eclipse.jdt.core.dom.ClassInstanceCreation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(org.eclipse.jdt.core.dom.ClassInstanceCreation node) {
|
||||
if ("PayEvent".equals(node.getType().toString())) {
|
||||
payEventCic[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
List<String> resolved = accessorResolver.resolve(
|
||||
"com.example.EventSource",
|
||||
"getCode",
|
||||
new AccessorResolver.ReceiverContext(
|
||||
context.getTypeDeclaration("com.example.EventSource"),
|
||||
payEventCic[0],
|
||||
cu,
|
||||
false,
|
||||
null),
|
||||
ResolutionBudget.defaults());
|
||||
|
||||
assertThat(resolved).containsExactly("Code.PAY");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class NonIndexedConcreteOverride {
|
||||
|
||||
@Test
|
||||
void dataFlowShouldResolveNonTrivialConcreteOverride() throws IOException {
|
||||
writeJava("com/example/Runner.java", """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
public void run() {
|
||||
Payload payload = new CustomPayload();
|
||||
String event = payload.getEvent();
|
||||
}
|
||||
}
|
||||
interface Payload {
|
||||
String getEvent();
|
||||
}
|
||||
class CustomPayload implements Payload {
|
||||
public String getEvent() {
|
||||
return sideEffect();
|
||||
}
|
||||
private String sideEffect() { return "CUSTOM"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
MethodDeclaration run = context.findMethodDeclaration(runner, "run", true);
|
||||
final MethodInvocation[] getterCall = new MethodInvocation[1];
|
||||
run.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(VariableDeclarationFragment node) {
|
||||
if (node.getInitializer() instanceof MethodInvocation mi) {
|
||||
getterCall[0] = mi;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
DataFlowModel model = new JdtDataFlowModel(context);
|
||||
String resolved = model.resolveValue(getterCall[0], context);
|
||||
assertThat(resolved).isEqualTo("CUSTOM");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GetterChainThroughInterface {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveDeepChainWithInterfaceMiddleHop() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
CentralDispatcher dispatcher;
|
||||
Outer outer;
|
||||
public void pay() {
|
||||
dispatcher.fire(outer.getInner().getPayload().getEvent());
|
||||
}
|
||||
}
|
||||
class CentralDispatcher {
|
||||
public void fire(OrderEvent event) {}
|
||||
}
|
||||
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 }
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("pay")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.CentralDispatcher")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.contains("OrderEvent.PAY");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConcreteSubtypeTrigger {
|
||||
|
||||
@Test
|
||||
void callGraphShouldPreferConcreteSubtypeFieldOverAbstractDefault() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
OrderService service;
|
||||
public void ship() {
|
||||
service.fire(new ShipEvent());
|
||||
}
|
||||
}
|
||||
class OrderService {
|
||||
StateMachine sm;
|
||||
public void fire(BaseEvent event) {
|
||||
sm.sendEvent(event.getCode());
|
||||
}
|
||||
}
|
||||
enum Code { PAY, SHIP }
|
||||
abstract class BaseEvent {
|
||||
protected Code code = Code.PAY;
|
||||
public Code getCode() { return code; }
|
||||
}
|
||||
class ShipEvent extends BaseEvent {
|
||||
public ShipEvent() { this.code = Code.SHIP; }
|
||||
}
|
||||
class StateMachine { void sendEvent(Code event) {} }
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("ship")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("Code.SHIP");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConcreteSubtypeTriggerViaFire {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveSubtypeWhenPassedToBaseTypedFire() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
OrderService service;
|
||||
public void ship() {
|
||||
service.fire(new ShipEvent());
|
||||
}
|
||||
}
|
||||
class OrderService {
|
||||
public void fire(BaseEvent event) {}
|
||||
}
|
||||
enum Code { PAY, SHIP }
|
||||
abstract class BaseEvent {
|
||||
protected Code code = Code.PAY;
|
||||
public Code getCode() { return code; }
|
||||
}
|
||||
class ShipEvent extends BaseEvent {
|
||||
public ShipEvent() { this.code = Code.SHIP; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("ship")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.OrderService")
|
||||
.methodName("fire")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getMethodChain())
|
||||
.containsExactly(
|
||||
"com.example.ApiController.ship",
|
||||
"com.example.OrderService.fire");
|
||||
assertThat(chains.get(0).getTriggerPoint().getEvent())
|
||||
.contains("ShipEvent");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class UnresolvedTypeWidening {
|
||||
|
||||
@Test
|
||||
void constantExtractorShouldWidenWhenTypeDeclarationMissing() throws IOException {
|
||||
writeJava("com/example/Code.java", """
|
||||
package com.example;
|
||||
public enum Code { A, B }
|
||||
""");
|
||||
writeJava("com/example/EventA.java", """
|
||||
package com.example;
|
||||
public class EventA implements RichEvent {
|
||||
public Code getCode() { return Code.A; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/EventB.java", """
|
||||
package com.example;
|
||||
public class EventB implements RichEvent {
|
||||
public Code getCode() { return Code.B; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/RichEvent.java", """
|
||||
package com.example;
|
||||
public interface RichEvent {
|
||||
Code getCode();
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> resolved = constantExtractor.resolveMethodReturnConstant(
|
||||
"RichEvent", "getCode", 0, new HashSet<>(), null);
|
||||
|
||||
assertThat(resolved).containsExactlyInAnyOrder("Code.A", "Code.B");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DeterministicImplementations {
|
||||
|
||||
@Test
|
||||
void getImplementationsShouldReturnSortedStableOrder() throws IOException {
|
||||
writeJava("com/example/EventSource.java", """
|
||||
package com.example;
|
||||
public interface EventSource {
|
||||
String getCode();
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/ZebraEvent.java", """
|
||||
package com.example;
|
||||
public class ZebraEvent implements EventSource {
|
||||
public String getCode() { return "Z"; }
|
||||
}
|
||||
""");
|
||||
writeJava("com/example/AlphaEvent.java", """
|
||||
package com.example;
|
||||
public class AlphaEvent implements EventSource {
|
||||
public String getCode() { return "A"; }
|
||||
}
|
||||
""");
|
||||
scan();
|
||||
|
||||
List<String> first = context.getImplementations("com.example.EventSource");
|
||||
List<String> second = context.getImplementations("com.example.EventSource");
|
||||
|
||||
assertThat(first).containsExactly(
|
||||
"com.example.AlphaEvent",
|
||||
"com.example.ZebraEvent");
|
||||
assertThat(second).isEqualTo(first);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReactiveFlatMapLambdaSendEvent {
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterInsideFlatMapLambda() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload).flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
OrderEvent event = OrderEvent.PAY;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterInsideFlatMapBlockBody() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload).flatMap(p -> {
|
||||
sm.sendEvent(p.getEvent());
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
OrderEvent event = OrderEvent.PAY;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
static Mono empty() { return new Mono(); }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void callGraphShouldResolveGetterThroughNestedFlatMap() throws IOException {
|
||||
writeJava("com/example/Api.java", """
|
||||
package com.example;
|
||||
public class ApiController {
|
||||
StateMachine sm;
|
||||
Payload payload;
|
||||
public void handle() {
|
||||
Mono.just(payload)
|
||||
.flatMap(x -> Mono.just(x.getInner()))
|
||||
.flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload {
|
||||
Inner inner = new Inner();
|
||||
Inner getInner() { return inner; }
|
||||
}
|
||||
class Inner {
|
||||
OrderEvent event = OrderEvent.SHIP;
|
||||
OrderEvent getEvent() { return event; }
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
class StateMachine { void sendEvent(OrderEvent event) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
class OrderStateMachineConfig {}
|
||||
""");
|
||||
scan();
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
EntryPoint entry = EntryPoint.builder()
|
||||
.className("com.example.ApiController")
|
||||
.methodName("handle")
|
||||
.build();
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||
assertThat(chains).hasSize(1);
|
||||
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactly("OrderEvent.SHIP");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ReactiveExpressionSupportTest {
|
||||
|
||||
@Test
|
||||
void shouldMapLambdaParameterToOuterJustPayload(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
Payload payload;
|
||||
void run() {
|
||||
Mono.just(payload).flatMap(p -> Mono.just(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload { String getEvent() { return "X"; } }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
""";
|
||||
Path file = tempDir.resolve("Runner.java");
|
||||
Files.writeString(file, source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
CompilationUnit cu = (CompilationUnit) runner.getRoot();
|
||||
final MethodInvocation[] flatMapCall = new MethodInvocation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ("flatMap".equals(node.getName().getIdentifier()) && flatMapCall[0] == null) {
|
||||
flatMapCall[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
String payload = ReactiveExpressionSupport.extractPayload(
|
||||
flatMapCall[0], context.getConstantResolver(), context);
|
||||
assertThat(payload).isEqualTo("payload.getEvent()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemapLambdaParameterGetter(@TempDir Path tempDir) throws Exception {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class Runner {
|
||||
Payload payload;
|
||||
StateMachine sm;
|
||||
void run() {
|
||||
Mono.just(payload).flatMap(p -> sm.sendEvent(p.getEvent()));
|
||||
}
|
||||
}
|
||||
class Payload { String getEvent() { return "X"; } }
|
||||
class StateMachine { void sendEvent(String e) {} }
|
||||
class Mono {
|
||||
static Mono just(Object o) { return new Mono(); }
|
||||
Mono flatMap(Object fn) { return this; }
|
||||
}
|
||||
""";
|
||||
Path file = tempDir.resolve("Runner.java");
|
||||
Files.writeString(file, source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
|
||||
CompilationUnit cu = (CompilationUnit) runner.getRoot();
|
||||
final MethodInvocation[] sendEventCall = new MethodInvocation[1];
|
||||
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if ("sendEvent".equals(node.getName().getIdentifier()) && sendEventCall[0] == null) {
|
||||
sendEventCall[0] = node;
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
Expression eventExpr = (Expression) sendEventCall[0].arguments().get(0);
|
||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||
eventExpr, context.getConstantResolver(), context);
|
||||
assertThat(remapped).isEqualTo("payload.getEvent()");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TypeResolverTest {
|
||||
|
||||
@Test
|
||||
void shouldMapGenericEventPlaceholderToSingleParameterIndex(@TempDir Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("Machine.java"), """
|
||||
package com.example;
|
||||
class StateMachine {
|
||||
void sendEvent(OrderEvent event) {}
|
||||
}
|
||||
enum OrderEvent { PAY }
|
||||
""");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
TypeResolver resolver = new TypeResolver(context);
|
||||
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "event")).isZero();
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "e", true)).isZero();
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "e")).isEqualTo(-1);
|
||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "payload")).isEqualTo(-1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user