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());
|
|| org.eclipse.jdt.core.dom.Modifier.isAbstract(ownerType.getModifiers());
|
||||||
|
|
||||||
if (widenToImplementations) {
|
if (widenToImplementations) {
|
||||||
List<String> widened = new ArrayList<>();
|
List<String> results = new ArrayList<>();
|
||||||
List<String> impls = context.getImplementations(ownerFqn);
|
|
||||||
if (impls != null) {
|
if (receiverContext != null && receiverContext.cic() != null) {
|
||||||
for (String implFqn : impls) {
|
String cicOwnerFqn = resolveCicOwnerFqn(receiverContext);
|
||||||
TypeDeclaration implType = context.getTypeDeclaration(implFqn);
|
if (cicOwnerFqn != null) {
|
||||||
widened.addAll(resolve(
|
TypeDeclaration cicOwnerType = context.getTypeDeclaration(
|
||||||
implFqn,
|
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,
|
methodName,
|
||||||
new ReceiverContext(implType, null,
|
cicReceiverContext,
|
||||||
receiverContext != null ? receiverContext.contextCu() : null,
|
indexedAccessor.orElse(null),
|
||||||
false,
|
visited);
|
||||||
receiverContext != null ? receiverContext.fieldValues() : null),
|
if (!cicValues.isEmpty()) {
|
||||||
budget,
|
return deduplicatePreservingOrder(cicValues);
|
||||||
visited));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!widened.isEmpty()) {
|
return List.of();
|
||||||
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);
|
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, methodName);
|
||||||
@@ -403,6 +420,28 @@ public final class AccessorResolver {
|
|||||||
return accessorOpt.get().fieldName();
|
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) {
|
private static List<String> deduplicatePreservingOrder(List<String> values) {
|
||||||
List<String> deduped = new ArrayList<>();
|
List<String> deduped = new ArrayList<>();
|
||||||
for (String value : values) {
|
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.LambdaExpression;
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
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;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -21,14 +23,129 @@ public final class ReactiveExpressionSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String extractPayload(Expression expression, ConstantResolver constantResolver, CodebaseContext context) {
|
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) {
|
if (expression == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (expression instanceof MethodInvocation mi) {
|
if (expression instanceof MethodInvocation mi) {
|
||||||
String methodName = mi.getName().getIdentifier();
|
String methodName = mi.getName().getIdentifier();
|
||||||
if ("flatMap".equals(methodName) && !mi.arguments().isEmpty()) {
|
if ("flatMap".equals(methodName) && !mi.arguments().isEmpty()) {
|
||||||
String fromArgument = extractFlatMapArgumentPayload((Expression) mi.arguments().get(0),
|
String fromArgument = extractFlatMapArgumentPayload(
|
||||||
constantResolver, context);
|
(Expression) mi.arguments().get(0), mi.getExpression(), constantResolver, context);
|
||||||
if (fromArgument != null) {
|
if (fromArgument != null) {
|
||||||
return fromArgument;
|
return fromArgument;
|
||||||
}
|
}
|
||||||
@@ -44,7 +161,7 @@ public final class ReactiveExpressionSupport {
|
|||||||
return arg.toString();
|
return arg.toString();
|
||||||
}
|
}
|
||||||
if (mi.getExpression() != null) {
|
if (mi.getExpression() != null) {
|
||||||
return extractPayload(mi.getExpression(), constantResolver, context);
|
return extractPayload(mi.getExpression(), flatMapReceiver, constantResolver, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -52,15 +169,131 @@ public final class ReactiveExpressionSupport {
|
|||||||
|
|
||||||
private static String extractFlatMapArgumentPayload(
|
private static String extractFlatMapArgumentPayload(
|
||||||
Expression argument,
|
Expression argument,
|
||||||
|
Expression flatMapReceiver,
|
||||||
ConstantResolver constantResolver,
|
ConstantResolver constantResolver,
|
||||||
CodebaseContext context) {
|
CodebaseContext context) {
|
||||||
if (argument instanceof LambdaExpression lambda) {
|
if (argument instanceof LambdaExpression lambda) {
|
||||||
Expression bodyExpression = lambdaBodyExpression(lambda);
|
Expression bodyExpression = lambdaBodyExpression(lambda);
|
||||||
if (bodyExpression != null) {
|
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) {
|
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--) {
|
for (int i = path.size() - 1; i > 0; i--) {
|
||||||
String target = path.get(i);
|
String target = path.get(i);
|
||||||
String caller = path.get(i - 1);
|
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 (paramIndex < 0) {
|
||||||
if ("this".equals(currentParamName)) {
|
if ("this".equals(currentParamName)) {
|
||||||
String actualReceiver = null;
|
String actualReceiver = null;
|
||||||
@@ -328,7 +329,15 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
String entryMethod = path.get(0);
|
String entryMethod = path.get(0);
|
||||||
|
|
||||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||||
|
String getterExpression = resolvedValue + methodSuffix;
|
||||||
List<String> chainedGetterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
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()) {
|
if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
|
||||||
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
||||||
@@ -421,6 +430,19 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exprNode instanceof Expression expressionNode) {
|
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(
|
String reactivePayload = ReactiveExpressionSupport.extractPayload(
|
||||||
expressionNode, constantResolver, context);
|
expressionNode, constantResolver, context);
|
||||||
if (reactivePayload != null) {
|
if (reactivePayload != null) {
|
||||||
@@ -896,7 +918,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
});
|
});
|
||||||
|
|
||||||
String targetMethod = path.get(path.size() - 1);
|
String targetMethod = path.get(path.size() - 1);
|
||||||
int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event);
|
int eventParamIndex = typeResolver.getParameterIndex(targetMethod, event, true);
|
||||||
if (eventParamIndex < 0) {
|
if (eventParamIndex < 0) {
|
||||||
eventParamIndex = 0;
|
eventParamIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -965,6 +987,29 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
.build();
|
.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;
|
return tp;
|
||||||
} finally {
|
} finally {
|
||||||
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.clearCurrentPath();
|
click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel.clearCurrentPath();
|
||||||
@@ -1326,7 +1371,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
} else if (exprNode instanceof MethodInvocation mi) {
|
} else if (exprNode instanceof MethodInvocation mi) {
|
||||||
String getterName = mi.getName().getIdentifier();
|
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();
|
Expression receiver = mi.getExpression();
|
||||||
String receiverName = null;
|
String receiverName = null;
|
||||||
String receiverType = null;
|
String receiverType = null;
|
||||||
@@ -1379,6 +1426,18 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
receiverType = variableTracer.getVariableDeclaredType(callerMethod, receiverName);
|
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) {
|
if (receiverType != null) {
|
||||||
String simpleReceiverType = receiverType;
|
String simpleReceiverType = receiverType;
|
||||||
if (receiverType.contains("<")) {
|
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);
|
visited.remove(fqn);
|
||||||
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
|
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
|
||||||
|
|||||||
@@ -238,6 +238,11 @@ public class GenericEventDetector {
|
|||||||
} else {
|
} else {
|
||||||
eventValue = context.resolveExpression(eventExpr);
|
eventValue = context.resolveExpression(eventExpr);
|
||||||
}
|
}
|
||||||
|
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetter(
|
||||||
|
eventExpr, constantResolver, context);
|
||||||
|
if (remapped != null) {
|
||||||
|
eventValue = remapped;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MethodDeclaration method = findEnclosingMethod(node);
|
MethodDeclaration method = findEnclosingMethod(node);
|
||||||
|
|||||||
@@ -7,11 +7,21 @@ import java.util.*;
|
|||||||
public class TypeResolver {
|
public class TypeResolver {
|
||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
|
|
||||||
|
private static final Set<String> GENERIC_EVENT_PARAMETER_NAMES = Set.of("e", "ev");
|
||||||
|
|
||||||
public TypeResolver(CodebaseContext context) {
|
public TypeResolver(CodebaseContext context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getParameterIndex(String methodFqn, String paramName) {
|
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;
|
if (methodFqn == null || !methodFqn.contains(".")) return -1;
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
@@ -25,11 +35,20 @@ public class TypeResolver {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (allowSyntheticEventPlaceholder
|
||||||
|
&& md.parameters().size() == 1
|
||||||
|
&& isGenericEventParameterName(paramName)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isGenericEventParameterName(String paramName) {
|
||||||
|
return paramName != null && GENERIC_EVENT_PARAMETER_NAMES.contains(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getParameterNames(String methodFqn) {
|
public List<String> getParameterNames(String methodFqn) {
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -337,19 +337,6 @@ public class VariableTracer {
|
|||||||
if (fieldType != null) return fieldType;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -346,9 +347,11 @@ public class CodebaseContext {
|
|||||||
if (interfaceName != null && interfaceName.contains("<")) {
|
if (interfaceName != null && interfaceName.contains("<")) {
|
||||||
cleanName = interfaceName.substring(0, interfaceName.indexOf('<'));
|
cleanName = interfaceName.substring(0, interfaceName.indexOf('<'));
|
||||||
}
|
}
|
||||||
Set<String> allImpls = new HashSet<>();
|
Set<String> allImpls = new LinkedHashSet<>();
|
||||||
collectImplementations(cleanName, allImpls, new HashSet<>());
|
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) {
|
private void collectImplementations(String typeName, Set<String> results, Set<String> visited) {
|
||||||
|
|||||||
@@ -372,6 +372,11 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
if (concreteAccessor.isPresent()) {
|
if (concreteAccessor.isPresent()) {
|
||||||
accessor = concreteAccessor;
|
accessor = concreteAccessor;
|
||||||
} else if (shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
|
} else if (shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
|
||||||
|
List<Expression> concreteOverride = inlineConcreteMethodOverride(
|
||||||
|
methodName, receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
||||||
|
if (!concreteOverride.isEmpty()) {
|
||||||
|
return concreteOverride;
|
||||||
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,6 +462,42 @@ public class JdtDataFlowModel implements DataFlowModel {
|
|||||||
return false;
|
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) {
|
private String concreteReceiverTypeFqn(Expression receiverDef) {
|
||||||
if (receiverDef instanceof ClassInstanceCreation cic) {
|
if (receiverDef instanceof ClassInstanceCreation cic) {
|
||||||
return AccessorInlining.resolveTypeFqn(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