Unify accessor resolution through a shared pipeline and widen interface getters to all implementations.
Introduce AccessorResolver with session caching and ordered index-first fallbacks, replace fragmented unwrap/depth logic with shared utilities, and fix polymorphic event resolution that stopped after the first indexed implementation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,10 @@ public final class AccessorFieldResolver {
|
||||
if (accessor == null || !accessor.isGetter() || context == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (accessor.kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER
|
||||
|| accessor.fieldName() == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
TypeDeclaration typeDeclaration = contextCu != null
|
||||
? context.getTypeDeclaration(accessor.declaringFqn(), contextCu)
|
||||
|
||||
@@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.index;
|
||||
public enum AccessorKind {
|
||||
GETTER,
|
||||
BOOLEAN_GETTER,
|
||||
CONSTANT_GETTER,
|
||||
RECORD_COMPONENT,
|
||||
SETTER,
|
||||
FLUENT_SETTER
|
||||
|
||||
@@ -12,6 +12,7 @@ public record AccessorSummary(
|
||||
public boolean isGetter() {
|
||||
return kind == AccessorKind.GETTER
|
||||
|| kind == AccessorKind.BOOLEAN_GETTER
|
||||
|| kind == AccessorKind.CONSTANT_GETTER
|
||||
|| kind == AccessorKind.RECORD_COMPONENT;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@ public class TrivialAccessorDetector {
|
||||
|
||||
Optional<String> getterField = propertyNameForGetter(methodName, method);
|
||||
if (getterField.isPresent()) {
|
||||
return detectGetter(ownerFqn, method, methodName, getterField.get(), fieldLookup);
|
||||
Optional<AccessorSummary> fieldGetter = detectGetter(ownerFqn, method, methodName, getterField.get(), fieldLookup);
|
||||
if (fieldGetter.isPresent()) {
|
||||
return fieldGetter;
|
||||
}
|
||||
return detectConstantGetter(ownerFqn, method, methodName);
|
||||
}
|
||||
|
||||
Optional<String> setterField = propertyNameForSetter(methodName);
|
||||
@@ -53,6 +57,51 @@ public class TrivialAccessorDetector {
|
||||
));
|
||||
}
|
||||
|
||||
private Optional<AccessorSummary> detectConstantGetter(
|
||||
String ownerFqn,
|
||||
MethodDeclaration method,
|
||||
String methodName) {
|
||||
List<?> statements = method.getBody().statements();
|
||||
if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Expression expr = rs.getExpression();
|
||||
if (expr == null || !isConstantReturnExpression(expr)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new AccessorSummary(
|
||||
ownerFqn,
|
||||
methodName,
|
||||
AccessorKind.CONSTANT_GETTER,
|
||||
null,
|
||||
null,
|
||||
ownerFqn,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
private boolean isConstantReturnExpression(Expression expr) {
|
||||
Expression current = unwrap(expr);
|
||||
if (current instanceof StringLiteral
|
||||
|| current instanceof NumberLiteral
|
||||
|| current instanceof BooleanLiteral
|
||||
|| current instanceof CharacterLiteral) {
|
||||
return true;
|
||||
}
|
||||
if (current instanceof QualifiedName) {
|
||||
return !containsMethodCall(current);
|
||||
}
|
||||
if (current instanceof SimpleName) {
|
||||
return false;
|
||||
}
|
||||
if (current instanceof FieldAccess fa) {
|
||||
Expression receiver = fa.getExpression();
|
||||
return (receiver instanceof SimpleName || receiver instanceof QualifiedName)
|
||||
&& !containsMethodCall(current);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Optional<AccessorSummary> detectGetter(
|
||||
String ownerFqn,
|
||||
MethodDeclaration method,
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
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.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Unified accessor resolution pipeline: index lookup first, then field/CIC/method-body fallbacks.
|
||||
*/
|
||||
public final class AccessorResolver {
|
||||
|
||||
public record GetterChainStep(String typeFqn, ClassInstanceCreation cic) {
|
||||
}
|
||||
|
||||
public record ReceiverContext(
|
||||
TypeDeclaration ownerType,
|
||||
ClassInstanceCreation cic,
|
||||
CompilationUnit contextCu,
|
||||
boolean anonymousGetter,
|
||||
Map<String, String> fieldValues) {
|
||||
}
|
||||
|
||||
private final CodebaseContext context;
|
||||
private final ConstantExtractor constantExtractor;
|
||||
private final ConstructorAnalyzer constructorAnalyzer;
|
||||
private final ConstantResolver constantResolver;
|
||||
private final FieldInitializerFinder fieldInitializerFinder;
|
||||
private final Map<String, List<String>> sessionCache = new HashMap<>();
|
||||
|
||||
public AccessorResolver(
|
||||
CodebaseContext context,
|
||||
ConstantExtractor constantExtractor,
|
||||
ConstructorAnalyzer constructorAnalyzer,
|
||||
ConstantResolver constantResolver) {
|
||||
this.context = context;
|
||||
this.constantExtractor = constantExtractor;
|
||||
this.constructorAnalyzer = constructorAnalyzer;
|
||||
this.constantResolver = constantResolver;
|
||||
this.fieldInitializerFinder = new FieldInitializerFinder(context);
|
||||
}
|
||||
|
||||
public Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName) {
|
||||
if (className == null || className.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
}
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
||||
if (typeDeclaration != null) {
|
||||
direct = context.getAccessorIndex().lookup(context.getFqn(typeDeclaration), methodName);
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
}
|
||||
if (!typeDeclaration.isInterface() && !org.eclipse.jdt.core.dom.Modifier.isAbstract(typeDeclaration.getModifiers())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
List<String> implementations = context.getImplementations(className);
|
||||
if (implementations == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
for (String implementation : implementations) {
|
||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
||||
if (implAccessor.isPresent()) {
|
||||
return implAccessor;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public List<String> resolve(String ownerFqn, String methodName, ReceiverContext receiverContext, ResolutionBudget budget) {
|
||||
return resolve(ownerFqn, methodName, receiverContext, budget, new HashSet<>());
|
||||
}
|
||||
|
||||
public List<String> resolve(
|
||||
String ownerFqn,
|
||||
String methodName,
|
||||
ReceiverContext receiverContext,
|
||||
ResolutionBudget budget,
|
||||
Set<String> visited) {
|
||||
if (ownerFqn == null || methodName == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (budget == null) {
|
||||
budget = ResolutionBudget.defaults();
|
||||
}
|
||||
|
||||
String cacheKey = cacheKey(ownerFqn, methodName, receiverContext);
|
||||
List<String> cached = sessionCache.get(cacheKey);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
List<String> resolved = resolveInternal(ownerFqn, methodName, receiverContext, budget, visited);
|
||||
if (!resolved.isEmpty()) {
|
||||
List<String> cachedCopy = List.copyOf(resolved);
|
||||
sessionCache.put(cacheKey, cachedCopy);
|
||||
return cachedCopy;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public GetterChainStep resolveGetterChainStep(String ownerFqn, String getterName, ResolutionBudget budget) {
|
||||
if (ownerFqn == null || getterName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, getterName);
|
||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
||||
AccessorSummary accessor = indexedAccessor.get();
|
||||
ClassInstanceCreation nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, accessor.fieldName());
|
||||
String nextType = fieldInitializerFinder.resolveFieldTypeName(ownerFqn, accessor.fieldName());
|
||||
if (nextCic != null) {
|
||||
nextType = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType());
|
||||
}
|
||||
if (nextType != null) {
|
||||
return new GetterChainStep(nextType, nextCic);
|
||||
}
|
||||
}
|
||||
|
||||
String simpleType = simplifyTypeName(ownerFqn);
|
||||
ClassInstanceCreation nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
|
||||
if (nextCic == null) {
|
||||
String fieldName = indexedFieldName(ownerFqn, getterName);
|
||||
nextCic = fieldInitializerFinder.findFieldInitializerCic(ownerFqn, fieldName);
|
||||
}
|
||||
if (nextCic != null) {
|
||||
return new GetterChainStep(
|
||||
click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType()),
|
||||
nextCic);
|
||||
}
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration(simpleType);
|
||||
if (td == null) {
|
||||
td = context.getTypeDeclaration(ownerFqn);
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration getter = context.findMethodDeclaration(td, getterName, true);
|
||||
if (getter != null && getter.getReturnType2() != null) {
|
||||
return new GetterChainStep(getter.getReturnType2().toString(), null);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void clearSessionCache() {
|
||||
sessionCache.clear();
|
||||
}
|
||||
|
||||
private List<String> resolveInternal(
|
||||
String ownerFqn,
|
||||
String methodName,
|
||||
ReceiverContext receiverContext,
|
||||
ResolutionBudget budget,
|
||||
Set<String> visited) {
|
||||
TypeDeclaration ownerType = receiverContext != null ? receiverContext.ownerType() : null;
|
||||
if (ownerType == null) {
|
||||
ownerType = context.getTypeDeclaration(ownerFqn);
|
||||
}
|
||||
boolean widenToImplementations = ownerType == null
|
||||
|| ownerType.isInterface()
|
||||
|| 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,
|
||||
methodName,
|
||||
new ReceiverContext(implType, null,
|
||||
receiverContext != null ? receiverContext.contextCu() : null,
|
||||
false,
|
||||
receiverContext != null ? receiverContext.fieldValues() : null),
|
||||
budget,
|
||||
visited));
|
||||
}
|
||||
}
|
||||
if (!widened.isEmpty()) {
|
||||
return deduplicatePreservingOrder(widened);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(ownerFqn, methodName);
|
||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
||||
List<String> indexed = resolveIndexedAccessor(indexedAccessor.get(), receiverContext, visited);
|
||||
if (!indexed.isEmpty()) {
|
||||
return indexed;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiverContext != null && receiverContext.ownerType() != null && receiverContext.cic() != null) {
|
||||
List<String> cicValues = resolveWithCic(
|
||||
ownerFqn, methodName, receiverContext, indexedAccessor.orElse(null), visited);
|
||||
if (!cicValues.isEmpty()) {
|
||||
return cicValues;
|
||||
}
|
||||
}
|
||||
|
||||
if (constantExtractor != null && !budget.isMethodReturnExhausted(0)) {
|
||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
||||
List<String> deep = constantExtractor.resolveMethodReturnConstant(
|
||||
ownerFqn, methodName, 0, visited, contextCu, budget);
|
||||
if (!deep.isEmpty()) {
|
||||
return deep;
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> resolveIndexedAccessor(
|
||||
AccessorSummary accessor,
|
||||
ReceiverContext receiverContext,
|
||||
Set<String> visited) {
|
||||
if (accessor.kind() == AccessorKind.CONSTANT_GETTER) {
|
||||
List<String> constants = extractConstantGetterReturn(accessor.ownerFqn(), accessor.methodName());
|
||||
if (!constants.isEmpty()) {
|
||||
return constants;
|
||||
}
|
||||
if (constantExtractor != null) {
|
||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
||||
return constantExtractor.resolveMethodReturnConstant(
|
||||
accessor.ownerFqn(), accessor.methodName(), 0, new HashSet<>(visited), contextCu);
|
||||
}
|
||||
}
|
||||
|
||||
if (constantExtractor != null) {
|
||||
CompilationUnit contextCu = receiverContext != null ? receiverContext.contextCu() : null;
|
||||
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
||||
accessor, contextCu, visited);
|
||||
if (!fieldConstants.isEmpty()) {
|
||||
return fieldConstants;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiverContext != null
|
||||
&& receiverContext.cic() != null
|
||||
&& receiverContext.fieldValues() != null
|
||||
&& accessor.fieldName() != null) {
|
||||
String fieldValue = receiverContext.fieldValues().get(accessor.fieldName());
|
||||
if (fieldValue != null) {
|
||||
return List.of(fieldValue);
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> resolveWithCic(
|
||||
String ownerFqn,
|
||||
String methodName,
|
||||
ReceiverContext receiverContext,
|
||||
AccessorSummary indexedAccessor,
|
||||
Set<String> visited) {
|
||||
TypeDeclaration td = receiverContext.ownerType();
|
||||
ClassInstanceCreation cic = receiverContext.cic();
|
||||
Map<String, String> fieldValues = receiverContext.fieldValues() != null
|
||||
? receiverContext.fieldValues()
|
||||
: new HashMap<>();
|
||||
|
||||
MethodDeclaration getter = null;
|
||||
if (cic.getAnonymousClassDeclaration() != null) {
|
||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(methodName)) {
|
||||
getter = md;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getter == null) {
|
||||
getter = context.findMethodDeclaration(td, methodName, true);
|
||||
}
|
||||
if (getter != null && getter.getBody() != null) {
|
||||
if (fieldValues.isEmpty() && constructorAnalyzer != null) {
|
||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
|
||||
}
|
||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, visited);
|
||||
if (result != null) {
|
||||
return List.of(qualifyEnumLikeResult(getter, result));
|
||||
}
|
||||
}
|
||||
|
||||
if (indexedAccessor != null && indexedAccessor.isGetter() && !receiverContext.anonymousGetter()) {
|
||||
if (indexedAccessor.kind() == AccessorKind.CONSTANT_GETTER) {
|
||||
List<String> constantValues = extractConstantGetterReturn(ownerFqn, methodName);
|
||||
if (!constantValues.isEmpty()) {
|
||||
return constantValues;
|
||||
}
|
||||
}
|
||||
if (fieldValues.isEmpty() && constructorAnalyzer != null) {
|
||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, null));
|
||||
}
|
||||
if (indexedAccessor.fieldName() != null) {
|
||||
String indexedFieldValue = fieldValues.get(indexedAccessor.fieldName());
|
||||
if (indexedFieldValue != null) {
|
||||
return List.of(indexedFieldValue);
|
||||
}
|
||||
}
|
||||
if (constantExtractor != null) {
|
||||
List<String> indexedConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
||||
indexedAccessor, receiverContext.contextCu(), visited);
|
||||
if (!indexedConstants.isEmpty()) {
|
||||
return indexedConstants;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> extractConstantGetterReturn(String ownerFqn, String methodName) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> constants = new ArrayList<>();
|
||||
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> extractor =
|
||||
constantExtractor != null
|
||||
? constantExtractor::extractConstantsFromExpression
|
||||
: (expr, out) -> {
|
||||
String resolved = constantResolver.resolve(expr, context);
|
||||
if (resolved != null) {
|
||||
out.add(resolved);
|
||||
}
|
||||
};
|
||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
extractor.accept(node.getExpression(), constants);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return constants;
|
||||
}
|
||||
|
||||
private static String qualifyEnumLikeResult(MethodDeclaration getter, String result) {
|
||||
if (result.contains(".")) {
|
||||
return result;
|
||||
}
|
||||
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
||||
if (returnType == null) {
|
||||
return result;
|
||||
}
|
||||
int lastDotRet = returnType.lastIndexOf('.');
|
||||
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
|
||||
if ("String".equals(simpleReturnType) || "Integer".equals(simpleReturnType)
|
||||
|| "Long".equals(simpleReturnType) || "Boolean".equals(simpleReturnType)) {
|
||||
return result;
|
||||
}
|
||||
return simpleReturnType + "." + result;
|
||||
}
|
||||
|
||||
private String indexedFieldName(String ownerFqn, String getterName) {
|
||||
Optional<AccessorSummary> accessorOpt = lookupIndexedGetter(ownerFqn, getterName);
|
||||
if (accessorOpt.isEmpty() || accessorOpt.get().fieldName() == null) {
|
||||
return null;
|
||||
}
|
||||
return accessorOpt.get().fieldName();
|
||||
}
|
||||
|
||||
private static List<String> deduplicatePreservingOrder(List<String> values) {
|
||||
List<String> deduped = new ArrayList<>();
|
||||
for (String value : values) {
|
||||
if (!deduped.contains(value)) {
|
||||
deduped.add(value);
|
||||
}
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
private static String cacheKey(String ownerFqn, String methodName, ReceiverContext receiverContext) {
|
||||
String cicKey = receiverContext != null && receiverContext.cic() != null
|
||||
? receiverContext.cic().toString()
|
||||
: "";
|
||||
return ownerFqn + "#" + methodName + "#" + cicKey;
|
||||
}
|
||||
|
||||
private static String simplifyTypeName(String typeFqn) {
|
||||
if (typeFqn == null) {
|
||||
return null;
|
||||
}
|
||||
String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
|
||||
if (simple.contains("<")) {
|
||||
simple = simple.substring(0, simple.indexOf('<'));
|
||||
}
|
||||
return simple;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Finds {@code new ...()} field initializers, walking the inheritance chain once.
|
||||
*/
|
||||
public final class FieldInitializerFinder {
|
||||
|
||||
private final CodebaseContext context;
|
||||
|
||||
public FieldInitializerFinder(CodebaseContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ClassInstanceCreation findFieldInitializerCic(String ownerFqn, String fieldName) {
|
||||
if (fieldName == null || ownerFqn == null) {
|
||||
return null;
|
||||
}
|
||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
||||
return findFieldInitializerCic(td, fieldName, new HashSet<>());
|
||||
}
|
||||
|
||||
public ClassInstanceCreation findFieldInitializerCic(TypeDeclaration td, String fieldName, Set<String> visited) {
|
||||
if (td == null || fieldName == null) {
|
||||
return null;
|
||||
}
|
||||
String typeFqn = context.getFqn(td);
|
||||
if (typeFqn == null || !visited.add(typeFqn)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
if (fragObj instanceof VariableDeclarationFragment frag
|
||||
&& frag.getName().getIdentifier().equals(fieldName)
|
||||
&& frag.getInitializer() instanceof ClassInstanceCreation cic) {
|
||||
return cic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
ClassInstanceCreation inherited = findFieldInitializerCic(superTd, fieldName, visited);
|
||||
if (inherited != null) {
|
||||
return inherited;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String resolveFieldTypeName(String ownerFqn, String fieldName) {
|
||||
if (ownerFqn == null || fieldName == null) {
|
||||
return null;
|
||||
}
|
||||
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
|
||||
return resolveFieldTypeName(td, fieldName, new HashSet<>());
|
||||
}
|
||||
|
||||
public String resolveFieldTypeName(TypeDeclaration td, String fieldName, Set<String> visited) {
|
||||
if (td == null || fieldName == null) {
|
||||
return null;
|
||||
}
|
||||
String typeFqn = context.getFqn(td);
|
||||
if (typeFqn == null || !visited.add(typeFqn)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (FieldDeclaration fd : td.getFields()) {
|
||||
for (Object fragObj : fd.fragments()) {
|
||||
if (fragObj instanceof VariableDeclarationFragment frag
|
||||
&& frag.getName().getIdentifier().equals(fieldName)) {
|
||||
return fd.getType().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
return resolveFieldTypeName(superTd, fieldName, visited);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Central registry for reactive/factory unwrap and passthrough method names used during constant extraction.
|
||||
*/
|
||||
public final class LibraryUnwrapRegistry {
|
||||
|
||||
private static final Set<String> UNWRAP_RECEIVER_TYPES = Set.of(
|
||||
"Optional", "Stream", "List", "Set", "Arrays", "Objects", "Mono", "Flux");
|
||||
|
||||
private static final Set<String> UNWRAP_METHOD_NAMES = Set.of(
|
||||
"of", "ofEntries", "asList", "entry", "just", "withPayload", "success");
|
||||
|
||||
private static final String[] STOP_UNWRAP_PREFIXES = {
|
||||
"map", "parse", "convert", "to", "resolve", "build", "create", "from",
|
||||
"transform", "translate", "derive", "determine", "calculate", "decode",
|
||||
"extract", "get", "find", "validate"
|
||||
};
|
||||
|
||||
private LibraryUnwrapRegistry() {
|
||||
}
|
||||
|
||||
public static boolean isUnwrapReceiverType(String receiverExpressionText) {
|
||||
return receiverExpressionText != null && UNWRAP_RECEIVER_TYPES.contains(receiverExpressionText);
|
||||
}
|
||||
|
||||
public static boolean isUnwrapMethodName(String methodName) {
|
||||
return methodName != null && UNWRAP_METHOD_NAMES.contains(methodName);
|
||||
}
|
||||
|
||||
public static boolean shouldStopUnwrap(String methodName) {
|
||||
if (methodName == null) {
|
||||
return true;
|
||||
}
|
||||
String lowerName = methodName.toLowerCase(Locale.ROOT);
|
||||
for (String prefix : STOP_UNWRAP_PREFIXES) {
|
||||
if (lowerName.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEventSetterMethodName(String methodName) {
|
||||
if (methodName == null) {
|
||||
return false;
|
||||
}
|
||||
return "event".equalsIgnoreCase(methodName)
|
||||
|| "type".equalsIgnoreCase(methodName)
|
||||
|| "eventType".equalsIgnoreCase(methodName);
|
||||
}
|
||||
|
||||
public static boolean isHeaderSetterMethodName(String methodName) {
|
||||
return "setHeader".equals(methodName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
import org.eclipse.jdt.core.dom.Expression;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Shared {@link MethodInvocation} peeling for factory/reactive wrappers and local passthrough methods.
|
||||
*/
|
||||
public final class MethodInvocationUnwrapper {
|
||||
|
||||
private MethodInvocationUnwrapper() {
|
||||
}
|
||||
|
||||
public static Expression unwrap(
|
||||
MethodInvocation mi,
|
||||
int depth,
|
||||
String methodFqn,
|
||||
CodebaseContext context,
|
||||
Function<MethodInvocation, String> targetMethodFqnResolver) {
|
||||
if (ResolutionBudget.defaults().isUnwrapExhausted(depth)) {
|
||||
return mi;
|
||||
}
|
||||
|
||||
String targetMethodFqn = targetMethodFqnResolver != null
|
||||
? targetMethodFqnResolver.apply(mi)
|
||||
: null;
|
||||
if (targetMethodFqn == null && methodFqn != null && methodFqn.contains(".") && mi.getExpression() == null) {
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
targetMethodFqn = className + "." + mi.getName().getIdentifier();
|
||||
}
|
||||
|
||||
if (targetMethodFqn != null && targetMethodFqn.contains(".")) {
|
||||
String className = targetMethodFqn.substring(0, targetMethodFqn.lastIndexOf('.'));
|
||||
String methodName = targetMethodFqn.substring(targetMethodFqn.lastIndexOf('.') + 1);
|
||||
|
||||
TypeDeclaration currentTd = methodFqn != null && methodFqn.contains(".")
|
||||
? context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.')))
|
||||
: null;
|
||||
org.eclipse.jdt.core.dom.CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof org.eclipse.jdt.core.dom.CompilationUnit compilationUnit
|
||||
? compilationUnit
|
||||
: null;
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration(className, cu);
|
||||
if (td == null) {
|
||||
td = context.getTypeDeclaration(className);
|
||||
}
|
||||
if (td != null) {
|
||||
MethodDeclaration localMd = context.findMethodDeclaration(td, methodName, true);
|
||||
if (localMd != null) {
|
||||
int paramIdx = getReturnedParameterIndex(localMd);
|
||||
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
||||
Expression arg = peelExpression((Expression) mi.arguments().get(paramIdx));
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrap(innerMi, depth + 1, methodFqn, context, targetMethodFqnResolver);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mi.getExpression() != null) {
|
||||
String exprStr = mi.getExpression().toString();
|
||||
if (!LibraryUnwrapRegistry.isUnwrapReceiverType(exprStr)) {
|
||||
return mi;
|
||||
}
|
||||
} else if (targetMethodFqn == null) {
|
||||
return mi;
|
||||
}
|
||||
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
if (LibraryUnwrapRegistry.shouldStopUnwrap(methodName)) {
|
||||
return mi;
|
||||
}
|
||||
|
||||
Expression arg = peelExpression((Expression) mi.arguments().get(0));
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrap(innerMi, depth + 1, methodFqn, context, targetMethodFqnResolver);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
return mi;
|
||||
}
|
||||
|
||||
public static Expression peelExpression(Expression expression) {
|
||||
Expression current = expression;
|
||||
while (current instanceof ParenthesizedExpression pe) {
|
||||
current = pe.getExpression();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public static int getReturnedParameterIndex(MethodDeclaration md) {
|
||||
if (md.getBody() == null) {
|
||||
return -1;
|
||||
}
|
||||
List<?> parameters = md.parameters();
|
||||
if (parameters.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
final List<String> returnedExprs = new ArrayList<>();
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
returnedExprs.add(node.getExpression().toString());
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
if (returnedExprs.size() == 1) {
|
||||
String retStr = returnedExprs.get(0);
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
if (parameters.get(i) instanceof SingleVariableDeclaration svd
|
||||
&& svd.getName().getIdentifier().equals(retStr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||
|
||||
/**
|
||||
* Shared depth and fallback budget for analysis traversals. Replaces scattered magic-number limits.
|
||||
*/
|
||||
public final class ResolutionBudget {
|
||||
|
||||
public static final int DEFAULT_METHOD_RETURN = 20;
|
||||
public static final int DEFAULT_ACCESSOR_INLINE = 25;
|
||||
public static final int DEFAULT_DATAFLOW = 50;
|
||||
public static final int DEFAULT_UNWRAP = 5;
|
||||
public static final int DEFAULT_CONSTRUCTOR = 10;
|
||||
public static final int DEFAULT_RETURNED_CIC = 50;
|
||||
public static final int DEFAULT_GLOBAL_FIELD_SCAN = 10;
|
||||
|
||||
private final int methodReturnDepth;
|
||||
private final int accessorInlineDepth;
|
||||
private final int dataflowDepth;
|
||||
private final int unwrapDepth;
|
||||
private final int constructorDepth;
|
||||
private final int returnedCicDepth;
|
||||
private final int globalFieldScanLimit;
|
||||
private final boolean allowGlobalFieldScan;
|
||||
|
||||
private ResolutionBudget(
|
||||
int methodReturnDepth,
|
||||
int accessorInlineDepth,
|
||||
int dataflowDepth,
|
||||
int unwrapDepth,
|
||||
int constructorDepth,
|
||||
int returnedCicDepth,
|
||||
int globalFieldScanLimit,
|
||||
boolean allowGlobalFieldScan) {
|
||||
this.methodReturnDepth = methodReturnDepth;
|
||||
this.accessorInlineDepth = accessorInlineDepth;
|
||||
this.dataflowDepth = dataflowDepth;
|
||||
this.unwrapDepth = unwrapDepth;
|
||||
this.constructorDepth = constructorDepth;
|
||||
this.returnedCicDepth = returnedCicDepth;
|
||||
this.globalFieldScanLimit = globalFieldScanLimit;
|
||||
this.allowGlobalFieldScan = allowGlobalFieldScan;
|
||||
}
|
||||
|
||||
public static ResolutionBudget defaults() {
|
||||
return new ResolutionBudget(
|
||||
DEFAULT_METHOD_RETURN,
|
||||
DEFAULT_ACCESSOR_INLINE,
|
||||
DEFAULT_DATAFLOW,
|
||||
DEFAULT_UNWRAP,
|
||||
DEFAULT_CONSTRUCTOR,
|
||||
DEFAULT_RETURNED_CIC,
|
||||
DEFAULT_GLOBAL_FIELD_SCAN,
|
||||
false);
|
||||
}
|
||||
|
||||
public static ResolutionBudget withGlobalFieldScan() {
|
||||
return defaults().enableGlobalFieldScan();
|
||||
}
|
||||
|
||||
public ResolutionBudget enableGlobalFieldScan() {
|
||||
return new ResolutionBudget(
|
||||
methodReturnDepth,
|
||||
accessorInlineDepth,
|
||||
dataflowDepth,
|
||||
unwrapDepth,
|
||||
constructorDepth,
|
||||
returnedCicDepth,
|
||||
globalFieldScanLimit,
|
||||
true);
|
||||
}
|
||||
|
||||
public ResolutionBudget child() {
|
||||
return new ResolutionBudget(
|
||||
methodReturnDepth - 1,
|
||||
accessorInlineDepth - 1,
|
||||
dataflowDepth - 1,
|
||||
unwrapDepth,
|
||||
constructorDepth - 1,
|
||||
returnedCicDepth - 1,
|
||||
globalFieldScanLimit,
|
||||
allowGlobalFieldScan);
|
||||
}
|
||||
|
||||
public boolean isMethodReturnExhausted(int depth) {
|
||||
return depth > methodReturnDepth;
|
||||
}
|
||||
|
||||
public boolean isAccessorInlineExhausted(int depth) {
|
||||
return depth > accessorInlineDepth;
|
||||
}
|
||||
|
||||
public boolean isDataflowExhausted(int depth) {
|
||||
return depth > dataflowDepth;
|
||||
}
|
||||
|
||||
public boolean isUnwrapExhausted(int depth) {
|
||||
return depth > unwrapDepth;
|
||||
}
|
||||
|
||||
public boolean isConstructorExhausted(int depth) {
|
||||
return depth > constructorDepth;
|
||||
}
|
||||
|
||||
public boolean isReturnedCicExhausted(int depth) {
|
||||
return depth > returnedCicDepth;
|
||||
}
|
||||
|
||||
public int globalFieldScanLimit() {
|
||||
return globalFieldScanLimit;
|
||||
}
|
||||
|
||||
public boolean allowGlobalFieldScan() {
|
||||
return allowGlobalFieldScan;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
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.ResolutionBudget;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
@@ -23,6 +27,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
protected final ConstantExtractor constantExtractor;
|
||||
protected final ConstructorAnalyzer constructorAnalyzer;
|
||||
protected final ExpressionAccessClassifier expressionAccessClassifier;
|
||||
protected final AccessorResolver accessorResolver;
|
||||
protected final FieldInitializerFinder fieldInitializerFinder;
|
||||
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
||||
|
||||
private ASTNode parseExpressionString(String expr) {
|
||||
@@ -65,7 +71,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|
||||
public AbstractCallGraphEngine(CodebaseContext context) {
|
||||
this.context = context;
|
||||
this.constantResolver = new ConstantResolver();
|
||||
this.constantResolver = context.getConstantResolver();
|
||||
|
||||
this.typeResolver = new TypeResolver(context);
|
||||
this.pathFinder = new CallGraphPathFinder(context);
|
||||
@@ -82,6 +88,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
this.expressionAccessClassifier = new ExpressionAccessClassifier(
|
||||
context, variableTracer, constantResolver, this::parseExpressionString);
|
||||
this.constantExtractor.setExpressionAccessClassifier(this.expressionAccessClassifier);
|
||||
this.accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
|
||||
this.constantExtractor.setAccessorResolver(this.accessorResolver);
|
||||
this.fieldInitializerFinder = new FieldInitializerFinder(context);
|
||||
}
|
||||
|
||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
@@ -297,94 +306,35 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|
||||
String entryMethod = path.get(0);
|
||||
|
||||
if (methodSuffix.startsWith(".get") || methodSuffix.contains(".type") || methodSuffix.contains(".event")) {
|
||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||
List<String> chainedGetterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
||||
if (chainedGetterEvents != null && !chainedGetterEvents.isEmpty()) {
|
||||
log.debug("Early return (chained getter): getterEvents = {}", chainedGetterEvents);
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.sourceModule(tp.getSourceModule())
|
||||
.stateMachineId(tp.getStateMachineId())
|
||||
.sourceState(tp.getSourceState())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(chainedGetterEvents)
|
||||
.constraint(tp.getConstraint())
|
||||
.stateTypeFqn(tp.getStateTypeFqn())
|
||||
.eventTypeFqn(tp.getEventTypeFqn())
|
||||
.external(tp.isExternal())
|
||||
.build();
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, chainedGetterEvents);
|
||||
}
|
||||
|
||||
if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event")
|
||||
|| methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
|
||||
String localMethodName = methodSuffix.substring(1).replace("()", "");
|
||||
String scopeForVar = resolveScopeMethodForVariable(currentParamName, entryMethod, path);
|
||||
Expression localSetterExpr = variableTracer.traceLocalSetter(scopeForVar, currentParamName, localMethodName);
|
||||
if (localSetterExpr != null) {
|
||||
List<String> setterEvents = new ArrayList<>();
|
||||
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
|
||||
for (Expression tracedSetter : tracedSetters) {
|
||||
constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
|
||||
}
|
||||
if (!setterEvents.isEmpty()) {
|
||||
log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.sourceModule(tp.getSourceModule())
|
||||
.stateMachineId(tp.getStateMachineId())
|
||||
.sourceState(tp.getSourceState())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(setterEvents)
|
||||
.constraint(tp.getConstraint())
|
||||
.stateTypeFqn(tp.getStateTypeFqn())
|
||||
.eventTypeFqn(tp.getEventTypeFqn())
|
||||
.external(tp.isExternal())
|
||||
.build();
|
||||
}
|
||||
if (expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
||||
if (!setterEvents.isEmpty()) {
|
||||
log.debug("Early return (scoped setter): setterEvents = {}", setterEvents);
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int entryParamIndex = typeResolver.getParameterIndex(entryMethod, currentParamName);
|
||||
if (entryParamIndex < 0) {
|
||||
if (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) {
|
||||
String localMethodName = methodSuffix.substring(1).replace("()", "");
|
||||
String scopeForVar = resolveScopeMethodForVariable(currentParamName, entryMethod, path);
|
||||
Expression localSetterExpr = variableTracer.traceLocalSetter(scopeForVar, currentParamName, localMethodName);
|
||||
if (localSetterExpr != null) {
|
||||
List<String> setterEvents = new ArrayList<>();
|
||||
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
|
||||
for (Expression tracedSetter : tracedSetters) {
|
||||
constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
|
||||
}
|
||||
if (!setterEvents.isEmpty()) {
|
||||
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.sourceModule(tp.getSourceModule())
|
||||
.stateMachineId(tp.getStateMachineId())
|
||||
.sourceState(tp.getSourceState())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(setterEvents)
|
||||
.constraint(tp.getConstraint())
|
||||
.stateTypeFqn(tp.getStateTypeFqn())
|
||||
.eventTypeFqn(tp.getEventTypeFqn())
|
||||
.external(tp.isExternal())
|
||||
.build();
|
||||
}
|
||||
if (methodSuffix != null && expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)) {
|
||||
List<String> setterEvents = resolveSetterEventsFromSuffix(currentParamName, methodSuffix, entryMethod, path);
|
||||
if (!setterEvents.isEmpty()) {
|
||||
log.debug("Early return 1: setterEvents = {}", setterEvents);
|
||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, setterEvents);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasGetterOnReceiver = (methodSuffix.startsWith(".get") || methodSuffix.equals(".type") || methodSuffix.equals(".event") || methodSuffix.equals(".type()") || methodSuffix.equals(".event()")) && currentParamName != null
|
||||
boolean hasGetterOnReceiver = methodSuffix != null
|
||||
&& expressionAccessClassifier.isTraceableAccessorSuffix(methodSuffix)
|
||||
&& currentParamName != null
|
||||
&& (currentParamName.contains("new ") || (!currentParamName.replaceFirst("^this\\.", "").contains(".") && !currentParamName.contains("(")));
|
||||
if (hasGetterOnReceiver) {
|
||||
List<String> getterEvents = evaluateGetterOnReceiver(resolvedValue, methodSuffix, entryMethod, path, callGraph);
|
||||
@@ -1153,6 +1103,43 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
* Resolves a nested getter prefix such as {@code wrapper.getEvent()} to the declared return type
|
||||
* and, when possible, a {@code new ...()} instance from the getter body.
|
||||
*/
|
||||
private TriggerPoint buildTriggerPointWithPolymorphicEvents(TriggerPoint tp, String resolvedValue, List<String> polymorphicEvents) {
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.sourceModule(tp.getSourceModule())
|
||||
.stateMachineId(tp.getStateMachineId())
|
||||
.sourceState(tp.getSourceState())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.polymorphicEvents(polymorphicEvents)
|
||||
.constraint(tp.getConstraint())
|
||||
.stateTypeFqn(tp.getStateTypeFqn())
|
||||
.eventTypeFqn(tp.getEventTypeFqn())
|
||||
.external(tp.isExternal())
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<String> resolveSetterEventsFromSuffix(
|
||||
String currentParamName,
|
||||
String methodSuffix,
|
||||
String entryMethod,
|
||||
List<String> path) {
|
||||
String localMethodName = methodSuffix.substring(1).replace("()", "");
|
||||
String scopeForVar = resolveScopeMethodForVariable(currentParamName, entryMethod, path);
|
||||
Expression localSetterExpr = variableTracer.traceLocalSetter(scopeForVar, currentParamName, localMethodName);
|
||||
if (localSetterExpr == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> setterEvents = new ArrayList<>();
|
||||
List<Expression> tracedSetters = variableTracer.traceVariableAll(localSetterExpr);
|
||||
for (Expression tracedSetter : tracedSetters) {
|
||||
constantExtractor.extractConstantsFromExpression(tracedSetter, setterEvents);
|
||||
}
|
||||
return setterEvents;
|
||||
}
|
||||
|
||||
protected GetterChainReceiver resolveGetterChainToReceiver(Expression expr, String entryMethod) {
|
||||
if (expr instanceof SimpleName sn) {
|
||||
String type = variableTracer.getVariableDeclaredType(entryMethod, sn.getIdentifier());
|
||||
@@ -1187,10 +1174,15 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|
||||
String getterName = mi.getName().getIdentifier();
|
||||
String simpleType = simplifyTypeName(base.typeFqn());
|
||||
ClassInstanceCreation nextCic = base.cic();
|
||||
if (nextCic == null) {
|
||||
nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
|
||||
String ownerFqn = resolveOwnerFqn(simpleType, base.typeFqn());
|
||||
|
||||
AccessorResolver.GetterChainStep indexedStep =
|
||||
accessorResolver.resolveGetterChainStep(ownerFqn, getterName, ResolutionBudget.defaults());
|
||||
if (indexedStep != null) {
|
||||
return new GetterChainReceiver(indexedStep.typeFqn(), indexedStep.cic());
|
||||
}
|
||||
|
||||
ClassInstanceCreation nextCic = constructorAnalyzer.findReturnedCIC(simpleType, getterName, context);
|
||||
if (nextCic != null) {
|
||||
return new GetterChainReceiver(
|
||||
click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(nextCic.getType()),
|
||||
@@ -1210,6 +1202,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveOwnerFqn(String simpleType, String typeFqn) {
|
||||
TypeDeclaration td = context.getTypeDeclaration(simpleType);
|
||||
if (td == null && typeFqn != null) {
|
||||
td = context.getTypeDeclaration(typeFqn);
|
||||
}
|
||||
return td != null ? context.getFqn(td) : typeFqn;
|
||||
}
|
||||
|
||||
private static String simplifyTypeName(String typeFqn) {
|
||||
if (typeFqn == null) {
|
||||
return null;
|
||||
@@ -1232,6 +1232,31 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return entryMethod;
|
||||
}
|
||||
|
||||
private String resolveClassForSuperCall(String entryMethod, List<String> path) {
|
||||
if (path != null) {
|
||||
for (String methodFqn : path) {
|
||||
if (methodFqn == null) {
|
||||
continue;
|
||||
}
|
||||
int lastDot = methodFqn.lastIndexOf('.');
|
||||
if (lastDot <= 0) {
|
||||
continue;
|
||||
}
|
||||
String className = methodFqn.substring(0, lastDot);
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
||||
if (typeDeclaration == null) {
|
||||
continue;
|
||||
}
|
||||
String superFqn = context.getSuperclassFqn(typeDeclaration);
|
||||
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
|
||||
return className;
|
||||
}
|
||||
}
|
||||
}
|
||||
int lastDot = entryMethod.lastIndexOf('.');
|
||||
return lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
|
||||
}
|
||||
|
||||
protected List<String> evaluateGetterOnReceiver(String resolvedValue, String methodSuffix, String entryMethod, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
ASTNode exprNode = parseExpressionString(resolvedValue);
|
||||
while (exprNode instanceof ParenthesizedExpression pe) {
|
||||
@@ -1251,12 +1276,31 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
|
||||
if (exprNode instanceof SuperMethodInvocation smi) {
|
||||
String getterName = smi.getName().getIdentifier();
|
||||
int lastDot = entryMethod.lastIndexOf('.');
|
||||
String className = lastDot > 0 ? entryMethod.substring(0, lastDot) : entryMethod;
|
||||
String className = resolveClassForSuperCall(entryMethod, path);
|
||||
TypeDeclaration currentTd = context.getTypeDeclaration(className);
|
||||
if (currentTd != null) {
|
||||
String receiverType = context.getSuperclassFqn(currentTd);
|
||||
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), null);
|
||||
if (receiverType != null) {
|
||||
Optional<AccessorSummary> indexedSuperGetter =
|
||||
context.getAccessorIndex().lookup(receiverType, getterName);
|
||||
if (indexedSuperGetter.isPresent() && indexedSuperGetter.get().isGetter()) {
|
||||
CompilationUnit contextCu = currentTd.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||
if (indexedSuperGetter.get().kind()
|
||||
== click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
||||
List<String> constants = constantExtractor.resolveMethodReturnConstant(
|
||||
receiverType, getterName, 0, new HashSet<>(), contextCu);
|
||||
if (!constants.isEmpty()) {
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
List<String> fieldConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
||||
indexedSuperGetter.get(), contextCu, new HashSet<>());
|
||||
if (!fieldConstants.isEmpty()) {
|
||||
return fieldConstants;
|
||||
}
|
||||
}
|
||||
return constantExtractor.resolveMethodReturnConstant(receiverType, getterName, 0, new HashSet<>(), null);
|
||||
}
|
||||
}
|
||||
} else if (exprNode instanceof MethodInvocation mi) {
|
||||
String getterName = mi.getName().getIdentifier();
|
||||
@@ -1301,9 +1345,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (currentTd != null) {
|
||||
receiverType = context.getSuperclassFqn(currentTd);
|
||||
}
|
||||
} else if (receiver instanceof FieldAccess fa) {
|
||||
receiverName = fa.getName().getIdentifier();
|
||||
if (fa.resolveFieldBinding() != null && fa.resolveFieldBinding().getType() != null) {
|
||||
receiverType = fa.resolveFieldBinding().getType().getErasure().getQualifiedName();
|
||||
}
|
||||
}
|
||||
|
||||
if (receiverType == null && path.size() > 1) {
|
||||
if (receiverType == null && receiverName != null && path.size() > 1) {
|
||||
String callerMethod = path.get(1);
|
||||
receiverType = variableTracer.getVariableDeclaredType(callerMethod, receiverName);
|
||||
}
|
||||
@@ -1350,53 +1399,55 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
cic = (ClassInstanceCreation) initExpr;
|
||||
}
|
||||
}
|
||||
if (cic == null) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration getter = null;
|
||||
if (cic.getAnonymousClassDeclaration() != null) {
|
||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) {
|
||||
getter = md;
|
||||
break;
|
||||
}
|
||||
boolean anonymousGetter = false;
|
||||
if (cic == null) {
|
||||
String ownerFqn = context.getFqn(td);
|
||||
List<String> resolvedWithoutCic = accessorResolver.resolve(
|
||||
ownerFqn,
|
||||
getterName,
|
||||
new AccessorResolver.ReceiverContext(td, null, contextCu, false, fieldValues),
|
||||
ResolutionBudget.defaults());
|
||||
if (!resolvedWithoutCic.isEmpty()) {
|
||||
return resolvedWithoutCic;
|
||||
}
|
||||
}
|
||||
if (getter == null) {
|
||||
getter = context.findMethodDeclaration(td, getterName, true);
|
||||
}
|
||||
if ((getterName.startsWith("get") && getterName.length() > 3)
|
||||
|| (getterName.startsWith("is") && getterName.length() > 2)
|
||||
|| context.getAccessorIndex().lookup(context.getFqn(td), getterName).isPresent()) {
|
||||
Optional<AccessorSummary> indexedGetter =
|
||||
context.getAccessorIndex().lookup(context.getFqn(td), getterName);
|
||||
if (indexedGetter.isPresent() && indexedGetter.get().isGetter()) {
|
||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(
|
||||
td, cic, context, this::evaluateMethodCallInConstructor));
|
||||
String indexedFieldValue = fieldValues.get(indexedGetter.get().fieldName());
|
||||
if (indexedFieldValue != null) {
|
||||
return Collections.singletonList(indexedFieldValue);
|
||||
}
|
||||
List<String> indexedConstants = constantExtractor.resolveIndexedGetterFieldConstants(
|
||||
indexedGetter.get(), contextCu, new HashSet<>());
|
||||
if (!indexedConstants.isEmpty()) {
|
||||
return indexedConstants;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getter != null && getter.getBody() != null) {
|
||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(td, cic, context, this::evaluateMethodCallInConstructor));
|
||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
|
||||
if (result != null) {
|
||||
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
||||
if (returnType != null && !result.contains(".")) {
|
||||
int lastDotRet = returnType.lastIndexOf('.');
|
||||
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
|
||||
if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer") && !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) {
|
||||
result = simpleReturnType + "." + result;
|
||||
} else {
|
||||
if (cic.getAnonymousClassDeclaration() != null) {
|
||||
for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) {
|
||||
if (declObj instanceof MethodDeclaration md && md.getName().getIdentifier().equals(getterName)) {
|
||||
getter = md;
|
||||
anonymousGetter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Collections.singletonList(result);
|
||||
}
|
||||
if (getter == null) {
|
||||
getter = context.findMethodDeclaration(td, getterName, true);
|
||||
}
|
||||
if (getter != null && getter.getBody() != null) {
|
||||
fieldValues.putAll(constructorAnalyzer.buildFieldValuesFromCIC(
|
||||
td, cic, context, this::evaluateMethodCallInConstructor));
|
||||
String result = constantResolver.evaluateMethodBodyWithLocals(getter, fieldValues, context, new HashSet<>());
|
||||
if (result != null) {
|
||||
String returnType = getter.getReturnType2() != null ? getter.getReturnType2().toString() : null;
|
||||
if (returnType != null && !result.contains(".")) {
|
||||
int lastDotRet = returnType.lastIndexOf('.');
|
||||
String simpleReturnType = lastDotRet > 0 ? returnType.substring(lastDotRet + 1) : returnType;
|
||||
if (!simpleReturnType.equals("String") && !simpleReturnType.equals("Integer")
|
||||
&& !simpleReturnType.equals("Long") && !simpleReturnType.equals("Boolean")) {
|
||||
result = simpleReturnType + "." + result;
|
||||
}
|
||||
}
|
||||
return Collections.singletonList(result);
|
||||
}
|
||||
}
|
||||
List<String> resolvedWithCic = accessorResolver.resolve(
|
||||
context.getFqn(td),
|
||||
getterName,
|
||||
new AccessorResolver.ReceiverContext(td, cic, contextCu, anonymousGetter, fieldValues),
|
||||
ResolutionBudget.defaults());
|
||||
if (!resolvedWithCic.isEmpty()) {
|
||||
return resolvedWithCic;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1484,90 +1535,21 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return new String[] { paramName, currentSuffix };
|
||||
}
|
||||
|
||||
private int getReturnedParameterIndex(MethodDeclaration md) {
|
||||
if (md.getBody() == null) return -1;
|
||||
List<?> parameters = md.parameters();
|
||||
if (parameters.isEmpty()) return -1;
|
||||
|
||||
final List<String> returnedExprs = new ArrayList<>();
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
returnedExprs.add(node.getExpression().toString());
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
if (returnedExprs.size() == 1) {
|
||||
String retStr = returnedExprs.get(0);
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
if (parameters.get(i) instanceof SingleVariableDeclaration svd) {
|
||||
if (svd.getName().getIdentifier().equals(retStr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth) {
|
||||
return unwrapMethodInvocation(mi, depth, null);
|
||||
}
|
||||
|
||||
protected Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
||||
if (depth > 5) return mi;
|
||||
if (mi.getExpression() != null) {
|
||||
String exprStr = mi.getExpression().toString();
|
||||
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
|
||||
return mi;
|
||||
return MethodInvocationUnwrapper.unwrap(mi, depth, methodFqn, context, miArg -> {
|
||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (methodFqn != null && methodFqn.contains(".")) {
|
||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||
if (td != null) {
|
||||
MethodDeclaration localMd = context.findMethodDeclaration(td, mi.getName().getIdentifier(), true);
|
||||
if (localMd != null) {
|
||||
int paramIdx = getReturnedParameterIndex(localMd);
|
||||
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
||||
Expression arg = (Expression) mi.arguments().get(paramIdx);
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
String currentClass = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||
if (miArg.getExpression() == null || miArg.getExpression().toString().equals("this")) {
|
||||
return currentClass + "." + miArg.getName().getIdentifier();
|
||||
}
|
||||
return mi;
|
||||
}
|
||||
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
String mName = mi.getName().getIdentifier();
|
||||
String lowerName = mName.toLowerCase();
|
||||
|
||||
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
|
||||
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
|
||||
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
|
||||
lowerName.startsWith("create") || lowerName.startsWith("from") ||
|
||||
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
|
||||
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
|
||||
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
|
||||
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
|
||||
lowerName.startsWith("find")) {
|
||||
return mi;
|
||||
}
|
||||
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
return mi;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public List<String> resolveClassConstantReturns(String className, CodebaseContext context, CompilationUnit contextCu) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorFieldResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.LibraryUnwrapRegistry;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
@@ -17,6 +20,7 @@ public class ConstantExtractor {
|
||||
private VariableTracer variableTracer;
|
||||
private ConstructorAnalyzer constructorAnalyzer;
|
||||
private ExpressionAccessClassifier expressionAccessClassifier;
|
||||
private AccessorResolver accessorResolver;
|
||||
|
||||
public ConstantExtractor(CodebaseContext context, ConstantResolver constantResolver, Function<MethodInvocation, String> calledMethodResolver) {
|
||||
this.context = context;
|
||||
@@ -36,6 +40,10 @@ public class ConstantExtractor {
|
||||
this.expressionAccessClassifier = expressionAccessClassifier;
|
||||
}
|
||||
|
||||
public void setAccessorResolver(AccessorResolver accessorResolver) {
|
||||
this.accessorResolver = accessorResolver;
|
||||
}
|
||||
|
||||
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
||||
if (constantResolver != null) {
|
||||
String resolved = constantResolver.resolve(expr, context);
|
||||
@@ -98,8 +106,7 @@ public class ConstantExtractor {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (methodName.equals("of") || methodName.equals("ofEntries") || methodName.equals("asList") || methodName.equals("entry") ||
|
||||
methodName.equals("just") || methodName.equals("withPayload") || methodName.equals("success")) {
|
||||
if (LibraryUnwrapRegistry.isUnwrapMethodName(methodName)) {
|
||||
for (Object argObj : mi.arguments()) {
|
||||
extractConstantsFromExpression((Expression) argObj, constants);
|
||||
}
|
||||
@@ -210,9 +217,9 @@ public class ConstantExtractor {
|
||||
Expression currentMi = mi;
|
||||
while (currentMi instanceof MethodInvocation chainMi) {
|
||||
String chainName = chainMi.getName().getIdentifier();
|
||||
if (chainName.equals("setHeader") && chainMi.arguments().size() == 2) {
|
||||
if (LibraryUnwrapRegistry.isHeaderSetterMethodName(chainName) && chainMi.arguments().size() == 2) {
|
||||
extractConstantsFromExpression((Expression) chainMi.arguments().get(1), constants);
|
||||
} else if ((chainName.equalsIgnoreCase("event") || chainName.equalsIgnoreCase("type") || chainName.equalsIgnoreCase("eventType")) && chainMi.arguments().size() == 1) {
|
||||
} else if (LibraryUnwrapRegistry.isEventSetterMethodName(chainName) && chainMi.arguments().size() == 1) {
|
||||
extractConstantsFromExpression((Expression) chainMi.arguments().get(0), constants);
|
||||
}
|
||||
currentMi = chainMi.getExpression();
|
||||
@@ -277,9 +284,22 @@ public class ConstantExtractor {
|
||||
}
|
||||
|
||||
public List<String> resolveMethodReturnConstant(String className, String methodName, int depth, Set<String> visited, CompilationUnit contextCu) {
|
||||
return resolveMethodReturnConstant(className, methodName, depth, visited, contextCu, ResolutionBudget.defaults());
|
||||
}
|
||||
|
||||
public List<String> resolveMethodReturnConstant(
|
||||
String className,
|
||||
String methodName,
|
||||
int depth,
|
||||
Set<String> visited,
|
||||
CompilationUnit contextCu,
|
||||
ResolutionBudget budget) {
|
||||
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
|
||||
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
|
||||
if (depth > 20) {
|
||||
if (budget == null) {
|
||||
budget = ResolutionBudget.defaults();
|
||||
}
|
||||
if (budget.isMethodReturnExhausted(depth)) {
|
||||
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -289,8 +309,25 @@ public class ConstantExtractor {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Optional<AccessorSummary> indexedAccessor = context.getAccessorIndex().lookup(className, methodName);
|
||||
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
|
||||
Optional<AccessorSummary> indexedAccessor = lookupIndexedGetter(className, methodName, budget);
|
||||
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||
if (tempTd == null) {
|
||||
tempTd = context.getTypeDeclaration(className);
|
||||
}
|
||||
final TypeDeclaration td = tempTd;
|
||||
boolean widenToImplementations = td == null
|
||||
|| 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> indexedConstants = resolveIndexedGetterFieldConstants(
|
||||
indexedAccessor.get(), contextCu, visited);
|
||||
if (!indexedConstants.isEmpty()) {
|
||||
@@ -300,11 +337,6 @@ public class ConstantExtractor {
|
||||
}
|
||||
|
||||
List<String> constants = new ArrayList<>();
|
||||
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
|
||||
if (tempTd == null) {
|
||||
tempTd = context.getTypeDeclaration(className);
|
||||
}
|
||||
final TypeDeclaration td = tempTd;
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null && md.getBody() != null) {
|
||||
@@ -387,11 +419,12 @@ public class ConstantExtractor {
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
} 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);
|
||||
List<String> delegationResult = resolveMethodReturnConstant(
|
||||
implName, methodName, depth + 1, visited, contextCu, budget);
|
||||
constants.addAll(delegationResult);
|
||||
}
|
||||
}
|
||||
@@ -416,6 +449,75 @@ public class ConstantExtractor {
|
||||
this::extractConstantsFromExpression);
|
||||
}
|
||||
|
||||
public Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName) {
|
||||
return lookupIndexedGetter(className, methodName, ResolutionBudget.defaults());
|
||||
}
|
||||
|
||||
private Optional<AccessorSummary> lookupIndexedGetter(String className, String methodName, ResolutionBudget budget) {
|
||||
if (accessorResolver != null) {
|
||||
return accessorResolver.lookupIndexedGetter(className, methodName);
|
||||
}
|
||||
if (className == null || className.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<AccessorSummary> direct = context.getAccessorIndex().lookup(className, methodName);
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
}
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
|
||||
if (typeDeclaration != null) {
|
||||
direct = context.getAccessorIndex().lookup(context.getFqn(typeDeclaration), methodName);
|
||||
if (direct.isPresent()) {
|
||||
return direct;
|
||||
}
|
||||
if (!typeDeclaration.isInterface() && !Modifier.isAbstract(typeDeclaration.getModifiers())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
List<String> implementations = context.getImplementations(className);
|
||||
if (implementations == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
for (String implementation : implementations) {
|
||||
Optional<AccessorSummary> implAccessor = context.getAccessorIndex().lookup(implementation, methodName);
|
||||
if (implAccessor.isPresent()) {
|
||||
return implAccessor;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private List<String> extractConstantGetterReturn(
|
||||
String ownerFqn,
|
||||
String methodName,
|
||||
CompilationUnit contextCu,
|
||||
Set<String> visited) {
|
||||
TypeDeclaration typeDeclaration = contextCu != null
|
||||
? context.getTypeDeclaration(ownerFqn, contextCu)
|
||||
: null;
|
||||
if (typeDeclaration == null) {
|
||||
typeDeclaration = context.getTypeDeclaration(ownerFqn);
|
||||
}
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, methodName, true);
|
||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> constants = new ArrayList<>();
|
||||
methodDeclaration.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
extractConstantsFromExpression(node.getExpression(), constants);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return constants;
|
||||
}
|
||||
|
||||
public String parseEnumSetElement(String eVal) {
|
||||
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
@@ -33,6 +34,15 @@ public class ConstructorAnalyzer {
|
||||
}
|
||||
|
||||
public List<String> traceFieldInConstructors(TypeDeclaration td, String fieldName, CodebaseContext context, Set<String> visited) {
|
||||
return traceFieldInConstructors(td, fieldName, context, visited, ResolutionBudget.defaults());
|
||||
}
|
||||
|
||||
public List<String> traceFieldInConstructors(
|
||||
TypeDeclaration td,
|
||||
String fieldName,
|
||||
CodebaseContext context,
|
||||
Set<String> visited,
|
||||
ResolutionBudget budget) {
|
||||
if (td == null || fieldName == null) return Collections.emptyList();
|
||||
String fqn = context.getFqn(td);
|
||||
if (fqn == null) return Collections.emptyList();
|
||||
@@ -154,7 +164,7 @@ public class ConstructorAnalyzer {
|
||||
if (superFqn != null && !superFqn.equals("java.lang.Object")) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited));
|
||||
results.addAll(traceFieldInConstructors(superTd, fieldName, context, visited, budget));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,23 +176,23 @@ public class ConstructorAnalyzer {
|
||||
for (String implFqn : impls) {
|
||||
TypeDeclaration implTd = context.getTypeDeclaration(implFqn);
|
||||
if (implTd != null) {
|
||||
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited));
|
||||
results.addAll(traceFieldInConstructors(implTd, fieldName, context, visited, budget));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (results.isEmpty()) {
|
||||
// Global Fallback: Search all parsed classes in the workspace
|
||||
if (results.isEmpty() && budget != null && budget.allowGlobalFieldScan()) {
|
||||
// Global Fallback: Search all parsed classes in the workspace (explicit opt-in only)
|
||||
Collection<TypeDeclaration> allTypes = context.getTypeDeclarations();
|
||||
if (allTypes != null) {
|
||||
int matchedCount = 0;
|
||||
for (TypeDeclaration typeDecl : allTypes) {
|
||||
if (typeDecl == null) continue;
|
||||
if (context.hasField(typeDecl, fieldName)) {
|
||||
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited));
|
||||
results.addAll(traceFieldInConstructors(typeDecl, fieldName, context, visited, budget));
|
||||
matchedCount++;
|
||||
if (matchedCount >= 10) {
|
||||
if (matchedCount >= budget.globalFieldScanLimit()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -465,7 +475,7 @@ public class ConstructorAnalyzer {
|
||||
private ClassInstanceCreation findReturnedCIC(
|
||||
String className, String methodName, CodebaseContext context,
|
||||
Set<String> visited, int depth) {
|
||||
if (depth > 50 || !visited.add(className + "#" + methodName)) {
|
||||
if (ResolutionBudget.defaults().isReturnedCicExhausted(depth) || !visited.add(className + "#" + methodName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,28 @@ public final class ExpressionAccessClassifier {
|
||||
return classifyMethodInvocation(mi, scopeMethod) == AccessKind.KEYED_LOOKUP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a trailing suffix like {@code .getEvent()} or {@code .type()} should be resolved via accessor tracing.
|
||||
*/
|
||||
public boolean isTraceableAccessorSuffix(String suffix) {
|
||||
if (suffix == null || !suffix.startsWith(".")) {
|
||||
return false;
|
||||
}
|
||||
String methodPart = suffix.substring(1);
|
||||
if (methodPart.endsWith("()")) {
|
||||
methodPart = methodPart.substring(0, methodPart.length() - 2);
|
||||
}
|
||||
return isBeanAccessorName(methodPart) || isRecordStyleAccessor(methodPart);
|
||||
}
|
||||
|
||||
public boolean isTraceableAccessorExpression(String expression, String scopeMethod) {
|
||||
if (expression == null || expression.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
AccessKind kind = classifyTrailingAccess(expression, scopeMethod);
|
||||
return kind == AccessKind.TRACEABLE_ACCESSOR;
|
||||
}
|
||||
|
||||
private boolean receiverExpressionIsMapLiteral(Expression receiver) {
|
||||
if (constantResolver == null || context == null || receiver == null) {
|
||||
return false;
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.Map;
|
||||
public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
private final CodebaseContext context;
|
||||
private final Path rootDir;
|
||||
private final ConstantResolver constantResolver = new ConstantResolver();
|
||||
private final ConstantResolver constantResolver;
|
||||
private final GenericEventDetector eventDetector;
|
||||
private final SpringMvcDetector mvcDetector;
|
||||
private final MessagingDetector messagingDetector;
|
||||
@@ -35,6 +35,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
|
||||
public JdtIntelligenceProvider(CodebaseContext context, Path rootDir) {
|
||||
this.context = context;
|
||||
this.rootDir = rootDir;
|
||||
this.constantResolver = context.getConstantResolver();
|
||||
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
|
||||
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
|
||||
this.messagingDetector = new MessagingDetector(context);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.MethodInvocationUnwrapper;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
|
||||
@@ -720,62 +722,7 @@ public class VariableTracer {
|
||||
}
|
||||
|
||||
private Expression unwrapMethodInvocation(MethodInvocation mi, int depth, String methodFqn) {
|
||||
if (depth > 5) return mi;
|
||||
|
||||
String targetMethodFqn = resolveTargetMethodFqn(mi, methodFqn);
|
||||
if (targetMethodFqn != null && targetMethodFqn.contains(".")) {
|
||||
String className = targetMethodFqn.substring(0, targetMethodFqn.lastIndexOf('.'));
|
||||
String methodName = targetMethodFqn.substring(targetMethodFqn.lastIndexOf('.') + 1);
|
||||
|
||||
TypeDeclaration currentTd = methodFqn != null && methodFqn.contains(".") ? context.getTypeDeclaration(methodFqn.substring(0, methodFqn.lastIndexOf('.'))) : null;
|
||||
CompilationUnit cu = currentTd != null && currentTd.getRoot() instanceof CompilationUnit ? (CompilationUnit) currentTd.getRoot() : null;
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration(className, cu);
|
||||
if (td != null) {
|
||||
MethodDeclaration localMd = context.findMethodDeclaration(td, methodName, true);
|
||||
if (localMd != null) {
|
||||
int paramIdx = getReturnedParameterIndex(localMd);
|
||||
if (paramIdx >= 0 && paramIdx < mi.arguments().size()) {
|
||||
Expression arg = peelExpression((Expression) mi.arguments().get(paramIdx));
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mi.getExpression() != null) {
|
||||
String exprStr = mi.getExpression().toString();
|
||||
if (!exprStr.equals("Optional") && !exprStr.equals("Stream") && !exprStr.equals("List") && !exprStr.equals("Set") && !exprStr.equals("Arrays") && !exprStr.equals("Objects") && !exprStr.equals("Mono") && !exprStr.equals("Flux")) {
|
||||
return mi;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mi.arguments().isEmpty()) {
|
||||
String mName = mi.getName().getIdentifier();
|
||||
String lowerName = mName.toLowerCase();
|
||||
|
||||
if (lowerName.startsWith("map") || lowerName.startsWith("parse") ||
|
||||
lowerName.startsWith("convert") || lowerName.startsWith("to") ||
|
||||
lowerName.startsWith("resolve") || lowerName.startsWith("build") ||
|
||||
lowerName.startsWith("create") || lowerName.startsWith("from") ||
|
||||
lowerName.startsWith("transform") || lowerName.startsWith("translate") ||
|
||||
lowerName.startsWith("derive") || lowerName.startsWith("determine") ||
|
||||
lowerName.startsWith("calculate") || lowerName.startsWith("decode") ||
|
||||
lowerName.startsWith("extract") || lowerName.startsWith("get") ||
|
||||
lowerName.startsWith("find") || lowerName.startsWith("validate")) {
|
||||
return mi;
|
||||
}
|
||||
|
||||
Expression arg = peelExpression((Expression) mi.arguments().get(0));
|
||||
if (arg instanceof MethodInvocation innerMi) {
|
||||
return unwrapMethodInvocation(innerMi, depth + 1, methodFqn);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
return mi;
|
||||
return MethodInvocationUnwrapper.unwrap(mi, depth, methodFqn, context, miArg -> resolveTargetMethodFqn(miArg, methodFqn));
|
||||
}
|
||||
|
||||
private String resolveTargetMethodFqn(MethodInvocation mi, String currentMethodFqn) {
|
||||
@@ -797,33 +744,4 @@ public class VariableTracer {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getReturnedParameterIndex(MethodDeclaration md) {
|
||||
if (md.getBody() == null) return -1;
|
||||
List<?> parameters = md.parameters();
|
||||
if (parameters.isEmpty()) return -1;
|
||||
|
||||
final List<String> returnedExprs = new ArrayList<>();
|
||||
md.getBody().accept(new ASTVisitor() {
|
||||
@Override
|
||||
public boolean visit(ReturnStatement node) {
|
||||
if (node.getExpression() != null) {
|
||||
returnedExprs.add(node.getExpression().toString());
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
});
|
||||
|
||||
if (returnedExprs.size() == 1) {
|
||||
String retStr = returnedExprs.get(0);
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
if (parameters.get(i) instanceof SingleVariableDeclaration svd) {
|
||||
if (svd.getName().getIdentifier().equals(retStr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package click.kamil.springstatemachineexporter.ast.common;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorInlining;
|
||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ResolutionBudget;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import java.util.*;
|
||||
|
||||
@@ -62,7 +63,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||
int depth) {
|
||||
if (expr == null || depth > 50 || !visited.add(expr)) {
|
||||
if (expr == null || ResolutionBudget.defaults().isDataflowExhausted(depth) || !visited.add(expr)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@@ -269,6 +270,19 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
// Handle Super Method Invocations
|
||||
if (expr instanceof SuperMethodInvocation smi) {
|
||||
IMethodBinding mb = smi.resolveMethodBinding();
|
||||
if (mb != null && mb.getDeclaringClass() != null) {
|
||||
String superFqn = mb.getDeclaringClass().getErasure().getQualifiedName();
|
||||
String methodName = smi.getName().getIdentifier();
|
||||
Optional<AccessorSummary> accessor = context.getAccessorIndex().lookup(superFqn, methodName);
|
||||
if (accessor.isPresent() && accessor.get().isGetter()) {
|
||||
Expression fieldValue = readFieldValue(accessor.get(), instanceFieldBindings, smi);
|
||||
if (fieldValue != null) {
|
||||
visited.remove(expr);
|
||||
return getReachingDefinitions(
|
||||
fieldValue, visited, paramBindings, instanceFieldBindings, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mb != null) {
|
||||
MethodDeclaration md = findMethodDeclaration(mb);
|
||||
if (md != null && md.getBody() != null) {
|
||||
@@ -309,7 +323,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||
int depth) {
|
||||
if (depth > 25) {
|
||||
if (ResolutionBudget.defaults().isAccessorInlineExhausted(depth)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@@ -359,6 +373,10 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (accessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.CONSTANT_GETTER) {
|
||||
return inlineConstantGetter(accessor.get(), visited, paramBindings, instanceFieldBindings, depth);
|
||||
}
|
||||
|
||||
if (accessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.RECORD_COMPONENT) {
|
||||
return inlineRecordComponentRead(mi, accessor.get(), receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
||||
}
|
||||
@@ -366,6 +384,34 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
return inlineAccessorGetter(mi, accessor.get(), receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
|
||||
}
|
||||
|
||||
private List<Expression> inlineConstantGetter(
|
||||
AccessorSummary accessor,
|
||||
Set<ASTNode> visited,
|
||||
Map<IVariableBinding, Expression> paramBindings,
|
||||
Map<IVariableBinding, Expression> instanceFieldBindings,
|
||||
int depth) {
|
||||
TypeDeclaration typeDeclaration = context.getTypeDeclaration(accessor.ownerFqn());
|
||||
if (typeDeclaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
MethodDeclaration methodDeclaration = context.findMethodDeclaration(typeDeclaration, accessor.methodName(), true);
|
||||
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<Expression> results = new ArrayList<>();
|
||||
for (Object statementObj : methodDeclaration.getBody().statements()) {
|
||||
if (statementObj instanceof ReturnStatement returnStatement && returnStatement.getExpression() != null) {
|
||||
results.addAll(getReachingDefinitions(
|
||||
returnStatement.getExpression(),
|
||||
visited,
|
||||
paramBindings,
|
||||
instanceFieldBindings,
|
||||
depth + 1));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private boolean shouldSkipAccessorDueToConcreteOverride(
|
||||
String declaringFqn,
|
||||
String methodName,
|
||||
@@ -375,9 +421,6 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
if (concreteFqn == null || concreteFqn.equals(declaringFqn)) {
|
||||
continue;
|
||||
}
|
||||
if (context.getAccessorIndex().lookup(concreteFqn, methodName).isPresent()) {
|
||||
continue;
|
||||
}
|
||||
TypeDeclaration concreteType = context.getTypeDeclaration(concreteFqn);
|
||||
if (concreteType != null && context.hasMethod(concreteType, methodName)) {
|
||||
return true;
|
||||
@@ -864,7 +907,7 @@ public class JdtDataFlowModel implements DataFlowModel {
|
||||
Map<IVariableBinding, Expression> callerParamValues,
|
||||
Map<IVariableBinding, Expression> fieldBindings,
|
||||
int depth) {
|
||||
if (cb == null || depth > 10) return;
|
||||
if (cb == null || ResolutionBudget.defaults().isConstructorExhausted(depth)) return;
|
||||
MethodDeclaration cd = findMethodDeclaration(cb);
|
||||
if (cd == null || cd.getBody() == null) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user