Fix abstract getter widening and nested reactive flatMap resolution.

Ensure abstract-type getter resolution merges subclass field values and trace indexed accessors on the requesting type, and extract terminal payloads from chained flatMap sendEvent calls.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 06:02:48 +02:00
parent 613dfffd53
commit c2eff6d7bb
8 changed files with 221 additions and 24 deletions

View File

@@ -22,6 +22,7 @@ public final class AccessorFieldResolver {
public static List<String> resolveFieldConstants(
AccessorSummary accessor,
String requestingTypeFqn,
CodebaseContext context,
ConstructorAnalyzer constructorAnalyzer,
CompilationUnit contextCu,
@@ -35,11 +36,14 @@ public final class AccessorFieldResolver {
return List.of();
}
String traceTypeFqn = requestingTypeFqn != null && !requestingTypeFqn.isBlank()
? requestingTypeFqn
: accessor.declaringFqn();
TypeDeclaration typeDeclaration = contextCu != null
? context.getTypeDeclaration(accessor.declaringFqn(), contextCu)
? context.getTypeDeclaration(traceTypeFqn, contextCu)
: null;
if (typeDeclaration == null) {
typeDeclaration = context.getTypeDeclaration(accessor.declaringFqn());
typeDeclaration = context.getTypeDeclaration(traceTypeFqn);
}
if (typeDeclaration == null) {
return List.of();
@@ -53,7 +57,7 @@ public final class AccessorFieldResolver {
if (constants.isEmpty()) {
collectFieldInitializerConstants(
typeDeclaration, accessor.fieldName(), constantExtractor, constants);
typeDeclaration, accessor.fieldName(), constantExtractor, constants, context);
}
return constants;
}
@@ -62,8 +66,9 @@ public final class AccessorFieldResolver {
TypeDeclaration typeDeclaration,
String fieldName,
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor,
List<String> constants) {
if (constantExtractor == null) {
List<String> constants,
CodebaseContext context) {
if (constantExtractor == null || typeDeclaration == null) {
return;
}
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
@@ -75,5 +80,14 @@ public final class AccessorFieldResolver {
}
}
}
if (constants.isEmpty() && context != null) {
String superFqn = context.getSuperclassFqn(typeDeclaration);
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
if (superType != null) {
collectFieldInitializerConstants(superType, fieldName, constantExtractor, constants, context);
}
}
}
}
}

View File

@@ -262,8 +262,11 @@ public final class AccessorResolver {
if (constantExtractor != null) {
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
String requestingTypeFqn = receiverContext != null && receiverContext.ownerType() != null
? context.getFqn(receiverContext.ownerType())
: accessor.ownerFqn();
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
accessor, contextCu, visited);
accessor, requestingTypeFqn, contextCu, visited);
if (!fieldConstants.isEmpty()) {
return fieldConstants;
}
@@ -335,7 +338,7 @@ public final class AccessorResolver {
}
if (constantExtractor != null) {
List<String> indexedConstants = constantExtractor.resolveIndexedGetterFieldConstants(
indexedAccessor, receiverContext.contextCu(), visited);
indexedAccessor, ownerFqn, receiverContext.contextCu(), visited);
if (!indexedConstants.isEmpty()) {
return indexedConstants;
}

View File

@@ -0,0 +1,80 @@
package click.kamil.springstatemachineexporter.analysis.pipeline;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.Block;
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 java.util.Set;
/**
* Extracts terminal payload literals from reactive factory chains ({@code Mono.just}, nested {@code flatMap}).
*/
public final class ReactiveExpressionSupport {
private static final Set<String> REACTIVE_FACTORY_METHODS = Set.of("just", "withPayload", "success");
private ReactiveExpressionSupport() {
}
public static String extractPayload(Expression expression, 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);
if (fromArgument != null) {
return fromArgument;
}
}
if (REACTIVE_FACTORY_METHODS.contains(methodName) && !mi.arguments().isEmpty()) {
Expression arg = (Expression) mi.arguments().get(0);
if (constantResolver != null && context != null) {
String resolved = constantResolver.resolve(arg, context);
if (resolved != null) {
return resolved;
}
}
return arg.toString();
}
if (mi.getExpression() != null) {
return extractPayload(mi.getExpression(), constantResolver, context);
}
}
return null;
}
private static String extractFlatMapArgumentPayload(
Expression argument,
ConstantResolver constantResolver,
CodebaseContext context) {
if (argument instanceof LambdaExpression lambda) {
Expression bodyExpression = lambdaBodyExpression(lambda);
if (bodyExpression != null) {
return extractPayload(bodyExpression, constantResolver, context);
}
}
return extractPayload(argument, constantResolver, context);
}
private static Expression lambdaBodyExpression(LambdaExpression lambda) {
if (lambda.getBody() instanceof Expression bodyExpression) {
return bodyExpression;
}
if (lambda.getBody() instanceof Block block) {
for (Object statement : block.statements()) {
if (statement instanceof ReturnStatement returnStatement
&& returnStatement.getExpression() != null) {
return returnStatement.getExpression();
}
}
}
return null;
}
}

View File

@@ -5,6 +5,7 @@ import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
@@ -418,6 +419,15 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
while (exprNode instanceof ParenthesizedExpression pe) {
exprNode = pe.getExpression();
}
if (exprNode instanceof Expression expressionNode) {
String reactivePayload = ReactiveExpressionSupport.extractPayload(
expressionNode, constantResolver, context);
if (reactivePayload != null) {
polymorphicEvents.add(reactivePayload);
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
}
}
String varName = null;
String methodName = null;

View File

@@ -320,24 +320,34 @@ public class ConstantExtractor {
|| td.isInterface()
|| Modifier.isAbstract(td.getModifiers());
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter() && !widenToImplementations) {
if (indexedAccessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
List<String> constantGetterValues = extractConstantGetterReturn(
indexedAccessor.get().ownerFqn(), methodName, contextCu, visited);
if (!constantGetterValues.isEmpty()) {
visited.remove(fqn);
return constantGetterValues;
List<String> constants = new ArrayList<>();
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
boolean indexedOnRequestedType = className.equals(indexedAccessor.get().ownerFqn());
if (!widenToImplementations || indexedOnRequestedType) {
if (indexedAccessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
List<String> constantGetterValues = extractConstantGetterReturn(
indexedAccessor.get().ownerFqn(), methodName, contextCu, visited);
if (!constantGetterValues.isEmpty()) {
if (!widenToImplementations) {
visited.remove(fqn);
return constantGetterValues;
}
constants.addAll(constantGetterValues);
}
}
if (constants.isEmpty()) {
List<String> indexedConstants = resolveIndexedGetterFieldConstants(
indexedAccessor.get(), className, contextCu, visited);
if (!indexedConstants.isEmpty()) {
if (!widenToImplementations) {
visited.remove(fqn);
return indexedConstants;
}
constants.addAll(indexedConstants);
}
}
}
List<String> indexedConstants = resolveIndexedGetterFieldConstants(
indexedAccessor.get(), contextCu, visited);
if (!indexedConstants.isEmpty()) {
visited.remove(fqn);
return indexedConstants;
}
}
List<String> constants = new ArrayList<>();
if (td != null) {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
@@ -423,13 +433,17 @@ public class ConstantExtractor {
}
});
}
if (constants.isEmpty() && widenToImplementations) {
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);
constants.addAll(delegationResult);
for (String value : delegationResult) {
if (!constants.contains(value)) {
constants.add(value);
}
}
}
}
}
@@ -442,10 +456,12 @@ public class ConstantExtractor {
public List<String> resolveIndexedGetterFieldConstants(
AccessorSummary accessor,
String requestingTypeFqn,
CompilationUnit contextCu,
Set<String> visited) {
return AccessorFieldResolver.resolveFieldConstants(
accessor,
requestingTypeFqn,
context,
constructorAnalyzer,
contextCu,

View File

@@ -2,6 +2,7 @@ package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.RequiredArgsConstructor;
@@ -107,6 +108,10 @@ public class GenericEventDetector {
}
private String extractEventFromReceiver(Expression receiver) {
String reactivePayload = ReactiveExpressionSupport.extractPayload(receiver, constantResolver, context);
if (reactivePayload != null) {
return reactivePayload;
}
if (receiver == null) return null;
if (receiver instanceof MethodInvocation mi) {
String mName = mi.getName().getIdentifier();