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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user