Index trivial accessors and inline them in dataflow, constant extraction, and call graph resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 18:17:17 +02:00
parent 0628710106
commit 7f12ed3f3a
19 changed files with 2591 additions and 7 deletions

View File

@@ -0,0 +1,75 @@
package click.kamil.springstatemachineexporter.analysis.index;
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
/**
* Resolves constant values for fields referenced by indexed trivial accessors.
*/
public final class AccessorFieldResolver {
private AccessorFieldResolver() {
}
public static List<String> resolveFieldConstants(
AccessorSummary accessor,
CodebaseContext context,
ConstructorAnalyzer constructorAnalyzer,
CompilationUnit contextCu,
Set<String> visited,
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor) {
if (accessor == null || !accessor.isGetter() || context == null) {
return List.of();
}
TypeDeclaration typeDeclaration = contextCu != null
? context.getTypeDeclaration(accessor.declaringFqn(), contextCu)
: null;
if (typeDeclaration == null) {
typeDeclaration = context.getTypeDeclaration(accessor.declaringFqn());
}
if (typeDeclaration == null) {
return List.of();
}
List<String> constants = new ArrayList<>();
if (constructorAnalyzer != null) {
constants.addAll(constructorAnalyzer.traceFieldInConstructors(
typeDeclaration, accessor.fieldName(), context, visited));
}
if (constants.isEmpty()) {
collectFieldInitializerConstants(
typeDeclaration, accessor.fieldName(), constantExtractor, constants);
}
return constants;
}
private static void collectFieldInitializerConstants(
TypeDeclaration typeDeclaration,
String fieldName,
BiConsumer<org.eclipse.jdt.core.dom.Expression, List<String>> constantExtractor,
List<String> constants) {
if (constantExtractor == null) {
return;
}
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
for (Object fragmentObj : fieldDeclaration.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment
&& fragment.getName().getIdentifier().equals(fieldName)
&& fragment.getInitializer() != null) {
constantExtractor.accept(fragment.getInitializer(), constants);
}
}
}
}
}

View File

@@ -0,0 +1,75 @@
package click.kamil.springstatemachineexporter.analysis.index;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class AccessorIndex {
private static final AccessorIndex EMPTY = new AccessorIndex(Map.of(), 0, 0, 0);
private final Map<String, Map<String, AccessorSummary>> byOwnerAndMethod;
private final int typeCount;
private final int trivialCount;
private final int blockedCount;
public AccessorIndex(
Map<String, Map<String, AccessorSummary>> byOwnerAndMethod,
int typeCount,
int trivialCount,
int blockedCount) {
this.byOwnerAndMethod = Collections.unmodifiableMap(deepCopy(byOwnerAndMethod));
this.typeCount = typeCount;
this.trivialCount = trivialCount;
this.blockedCount = blockedCount;
}
public static AccessorIndex empty() {
return EMPTY;
}
public Optional<AccessorSummary> lookup(String ownerFqn, String methodName) {
if (ownerFqn == null || methodName == null) {
return Optional.empty();
}
Map<String, AccessorSummary> methods = byOwnerAndMethod.get(ownerFqn);
if (methods == null) {
return Optional.empty();
}
return Optional.ofNullable(methods.get(methodName));
}
public List<AccessorSummary> accessorsForType(String ownerFqn) {
Map<String, AccessorSummary> methods = byOwnerAndMethod.get(ownerFqn);
if (methods == null) {
return List.of();
}
return List.copyOf(methods.values());
}
public Map<String, Map<String, AccessorSummary>> asMap() {
return byOwnerAndMethod;
}
public int typeCount() {
return typeCount;
}
public int trivialCount() {
return trivialCount;
}
public int blockedCount() {
return blockedCount;
}
private static Map<String, Map<String, AccessorSummary>> deepCopy(Map<String, Map<String, AccessorSummary>> source) {
Map<String, Map<String, AccessorSummary>> copy = new HashMap<>();
for (Map.Entry<String, Map<String, AccessorSummary>> entry : source.entrySet()) {
copy.put(entry.getKey(), Map.copyOf(entry.getValue()));
}
return copy;
}
}

View File

@@ -0,0 +1,286 @@
package click.kamil.springstatemachineexporter.analysis.index;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
@Slf4j
public class AccessorIndexBuilder {
private final TrivialAccessorDetector detector = new TrivialAccessorDetector();
private CodebaseContext context;
public AccessorIndex build(CodebaseContext context) {
this.context = context;
Map<String, DirectTypeAccessors> direct = new HashMap<>();
for (CompilationUnit cu : context.getCompilationUnits()) {
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
for (Object typeObj : cu.types()) {
if (typeObj instanceof TypeDeclaration td) {
indexTypeDeclaration(td, direct);
} else if (typeObj instanceof RecordDeclaration rd) {
indexRecord(rd, packageName, direct);
} else if (typeObj instanceof EnumDeclaration ed) {
indexEnum(ed, packageName, direct);
}
}
}
int blocked = direct.values().stream().mapToInt(state -> state.blockedMethods.size()).sum();
Map<String, Map<String, AccessorSummary>> effective = new HashMap<>();
int trivial = 0;
for (String ownerFqn : direct.keySet()) {
Map<String, AccessorSummary> merged = mergeEffective(ownerFqn, direct, effective, new HashSet<>());
if (!merged.isEmpty()) {
effective.put(ownerFqn, merged);
trivial += merged.size();
}
}
log.info(
"Built accessor index: {} trivial accessors across {} types ({} blocked overrides)",
trivial,
effective.size(),
blocked);
this.context = null;
return new AccessorIndex(effective, effective.size(), trivial, blocked);
}
private void indexTypeDeclaration(TypeDeclaration td, Map<String, DirectTypeAccessors> direct) {
String ownerFqn = context.getFqn(td);
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
for (MethodDeclaration md : td.getMethods()) {
classifyMethod(ownerFqn, md, fieldName -> findFieldDeclaringFqn(td, fieldName), state);
}
for (TypeDeclaration nested : td.getTypes()) {
indexTypeDeclaration(nested, direct);
}
}
private void indexEnum(EnumDeclaration ed, String parentFqn, Map<String, DirectTypeAccessors> direct) {
String ownerFqn = parentFqn.isEmpty() ? ed.getName().getIdentifier() : parentFqn + "." + ed.getName().getIdentifier();
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
for (Object decl : ed.bodyDeclarations()) {
if (decl instanceof MethodDeclaration md) {
classifyMethod(ownerFqn, md, fieldName -> findEnumFieldDeclaringFqn(ed, ownerFqn, fieldName), state);
} else if (decl instanceof TypeDeclaration nestedTd) {
indexTypeDeclaration(nestedTd, direct);
} else if (decl instanceof RecordDeclaration nestedRd) {
indexRecord(nestedRd, ownerFqn, direct);
}
}
}
private void indexRecord(RecordDeclaration rd, String parentFqn, Map<String, DirectTypeAccessors> direct) {
String ownerFqn = parentFqn.isEmpty() ? rd.getName().getIdentifier() : parentFqn + "." + rd.getName().getIdentifier();
DirectTypeAccessors state = direct.computeIfAbsent(ownerFqn, k -> new DirectTypeAccessors());
for (Object compObj : rd.recordComponents()) {
String componentName = recordComponentName(compObj);
if (componentName == null) {
continue;
}
detector.detectRecordComponent(ownerFqn, componentName).ifPresent(summary ->
state.trivial.put(summary.methodName(), summary));
}
TrivialAccessorDetector.FieldLookup recordFieldLookup =
fieldName -> findRecordFieldDeclaringFqn(rd, ownerFqn, fieldName);
for (Object declObj : rd.bodyDeclarations()) {
if (declObj instanceof MethodDeclaration md) {
String methodName = md.getName().getIdentifier();
AccessorSummary existing = state.trivial.get(methodName);
if (existing != null && existing.kind() == AccessorKind.RECORD_COMPONENT) {
continue;
}
classifyMethod(ownerFqn, md, recordFieldLookup, state);
} else if (declObj instanceof TypeDeclaration nestedTd) {
indexTypeDeclaration(nestedTd, direct);
} else if (declObj instanceof RecordDeclaration nestedRd) {
indexRecord(nestedRd, ownerFqn, direct);
}
}
}
private void classifyMethod(
String ownerFqn,
MethodDeclaration md,
TrivialAccessorDetector.FieldLookup fieldLookup,
DirectTypeAccessors state) {
if (md.getBody() == null) {
return;
}
String methodName = md.getName().getIdentifier();
Optional<AccessorSummary> summary = detector.detect(ownerFqn, md, fieldLookup);
applyClassification(state, methodName, summary);
}
private void applyClassification(DirectTypeAccessors state, String methodName, Optional<AccessorSummary> summary) {
if (summary.isPresent()) {
state.trivial.put(methodName, summary.get());
state.blockedMethods.remove(methodName);
return;
}
if (looksLikeAccessorMethod(methodName)) {
state.blockedMethods.add(methodName);
state.trivial.remove(methodName);
}
}
private boolean looksLikeAccessorMethod(String methodName) {
return methodName.startsWith("get")
|| methodName.startsWith("set")
|| methodName.startsWith("is");
}
private Map<String, AccessorSummary> mergeEffective(
String ownerFqn,
Map<String, DirectTypeAccessors> direct,
Map<String, Map<String, AccessorSummary>> cache,
Set<String> visiting) {
ownerFqn = resolveTypeFqn(ownerFqn);
if (ownerFqn == null) {
return Map.of();
}
if (cache.containsKey(ownerFqn)) {
return cache.get(ownerFqn);
}
if (!visiting.add(ownerFqn)) {
return Map.of();
}
Map<String, AccessorSummary> merged = new HashMap<>();
TypeDeclaration td = context.getTypeDeclaration(ownerFqn);
if (td != null) {
String superFqn = resolveTypeFqn(context.getSuperclassFqn(td));
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
for (Map.Entry<String, AccessorSummary> inherited : mergeEffective(superFqn, direct, cache, visiting).entrySet()) {
merged.put(inherited.getKey(), rebindOwner(inherited.getValue(), ownerFqn));
}
}
}
DirectTypeAccessors own = direct.get(ownerFqn);
if (own != null) {
own.blockedMethods.forEach(merged::remove);
merged.putAll(own.trivial);
}
visiting.remove(ownerFqn);
cache.put(ownerFqn, Map.copyOf(merged));
return merged;
}
private AccessorSummary rebindOwner(AccessorSummary summary, String ownerFqn) {
if (summary.ownerFqn().equals(ownerFqn)) {
return summary;
}
return new AccessorSummary(
ownerFqn,
summary.methodName(),
summary.kind(),
summary.fieldName(),
summary.paramName(),
summary.declaringFqn(),
summary.returnsThis()
);
}
private String resolveTypeFqn(String typeName) {
if (typeName == null || typeName.isEmpty()) {
return null;
}
if (typeName.contains(".")) {
return typeName;
}
TypeDeclaration td = context.getTypeDeclaration(typeName);
return td != null ? context.getFqn(td) : typeName;
}
private static final class DirectTypeAccessors {
private final Map<String, AccessorSummary> trivial = new HashMap<>();
private final Set<String> blockedMethods = new HashSet<>();
}
private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName) {
return findFieldDeclaringFqn(td, fieldName, new HashSet<>());
}
private Optional<String> findFieldDeclaringFqn(TypeDeclaration td, String fieldName, Set<String> visited) {
if (td == null) {
return Optional.empty();
}
String ownerFqn = context.getFqn(td);
if (!visited.add(ownerFqn)) {
return Optional.empty();
}
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(fieldName)) {
return Optional.of(ownerFqn);
}
}
}
String superFqn = context.getSuperclassFqn(td);
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
if (superTd != null) {
return findFieldDeclaringFqn(superTd, fieldName, visited);
}
}
return Optional.empty();
}
private Optional<String> findEnumFieldDeclaringFqn(EnumDeclaration ed, String ownerFqn, String fieldName) {
for (Object decl : ed.bodyDeclarations()) {
if (decl instanceof FieldDeclaration fd) {
for (Object fragObj : fd.fragments()) {
VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj;
if (frag.getName().getIdentifier().equals(fieldName)) {
return Optional.of(ownerFqn);
}
}
}
}
return Optional.empty();
}
private Optional<String> findRecordFieldDeclaringFqn(RecordDeclaration rd, String ownerFqn, String fieldName) {
for (Object compObj : rd.recordComponents()) {
String componentName = recordComponentName(compObj);
if (fieldName.equals(componentName)) {
return Optional.of(ownerFqn);
}
}
return Optional.empty();
}
private String recordComponentName(Object compObj) {
if (compObj instanceof SingleVariableDeclaration svd) {
return svd.getName().getIdentifier();
}
try {
java.lang.reflect.Method getNameMethod = compObj.getClass().getMethod("getName");
Object nameObj = getNameMethod.invoke(compObj);
if (nameObj instanceof SimpleName sn) {
return sn.getIdentifier();
}
} catch (ReflectiveOperationException ignored) {
// fall through
}
return null;
}
}

View File

@@ -0,0 +1,93 @@
package click.kamil.springstatemachineexporter.analysis.index;
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import java.util.List;
import java.util.Optional;
public final class AccessorInlining {
private AccessorInlining() {
}
public static Optional<AccessorSummary> lookupAccessor(
CodebaseContext context,
MethodInvocation mi,
List<Expression> receiverDefinitions) {
if (context == null || mi == null) {
return Optional.empty();
}
String methodName = mi.getName().getIdentifier();
if (isLikelyNonBeanGet(methodName, mi)) {
return Optional.empty();
}
if (receiverDefinitions != null) {
for (Expression receiverDef : receiverDefinitions) {
if (receiverDef instanceof ClassInstanceCreation cic) {
Optional<AccessorSummary> concrete = lookupOnType(context, resolveTypeFqn(cic), methodName);
if (concrete.isPresent()) {
return concrete;
}
}
}
}
IMethodBinding methodBinding = mi.resolveMethodBinding();
if (methodBinding != null && methodBinding.getDeclaringClass() != null) {
ITypeBinding declaringClass = methodBinding.getDeclaringClass().getErasure();
Optional<AccessorSummary> declared = lookupOnType(context, declaringClass.getQualifiedName(), methodName);
if (declared.isPresent()) {
return declared;
}
}
return Optional.empty();
}
public static Optional<AccessorSummary> lookupAccessor(CodebaseContext context, MethodInvocation mi) {
return lookupAccessor(context, mi, List.of());
}
private static Optional<AccessorSummary> lookupOnType(CodebaseContext context, String ownerFqn, String methodName) {
if (ownerFqn == null || ownerFqn.isBlank() || ownerFqn.startsWith("java.")) {
return Optional.empty();
}
return context.getAccessorIndex().lookup(ownerFqn, methodName);
}
public static String resolveTypeFqn(ClassInstanceCreation cic) {
if (cic == null) {
return null;
}
if (cic.resolveConstructorBinding() != null && cic.resolveConstructorBinding().getDeclaringClass() != null) {
return cic.resolveConstructorBinding().getDeclaringClass().getErasure().getQualifiedName();
}
if (cic.resolveTypeBinding() != null) {
return cic.resolveTypeBinding().getErasure().getQualifiedName();
}
return AstUtils.extractSimpleTypeName(cic.getType());
}
private static boolean isLikelyNonBeanGet(String methodName, MethodInvocation mi) {
if (!"get".equals(methodName) && !"getOrDefault".equals(methodName)) {
return false;
}
if (mi.arguments().size() != 1) {
return false;
}
IMethodBinding methodBinding = mi.resolveMethodBinding();
if (methodBinding == null || methodBinding.getDeclaringClass() == null) {
return false;
}
String ownerFqn = methodBinding.getDeclaringClass().getErasure().getQualifiedName();
return ownerFqn != null && ownerFqn.startsWith("java.util");
}
}

View File

@@ -0,0 +1,9 @@
package click.kamil.springstatemachineexporter.analysis.index;
public enum AccessorKind {
GETTER,
BOOLEAN_GETTER,
RECORD_COMPONENT,
SETTER,
FLUENT_SETTER
}

View File

@@ -0,0 +1,21 @@
package click.kamil.springstatemachineexporter.analysis.index;
public record AccessorSummary(
String ownerFqn,
String methodName,
AccessorKind kind,
String fieldName,
String paramName,
String declaringFqn,
boolean returnsThis
) {
public boolean isGetter() {
return kind == AccessorKind.GETTER
|| kind == AccessorKind.BOOLEAN_GETTER
|| kind == AccessorKind.RECORD_COMPONENT;
}
public boolean isSetter() {
return kind == AccessorKind.SETTER || kind == AccessorKind.FLUENT_SETTER;
}
}

View File

@@ -0,0 +1,256 @@
package click.kamil.springstatemachineexporter.analysis.index;
import org.eclipse.jdt.core.dom.*;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
/**
* Detects trivial JavaBean-style getters and setters whose bodies only read or write a single field.
*/
public class TrivialAccessorDetector {
@FunctionalInterface
public interface FieldLookup extends Function<String, Optional<String>> {
}
public Optional<AccessorSummary> detect(String ownerFqn, MethodDeclaration method, FieldLookup fieldLookup) {
if (ownerFqn == null || method == null || method.getBody() == null || fieldLookup == null) {
return Optional.empty();
}
String methodName = method.getName().getIdentifier();
if (method.isConstructor()) {
return Optional.empty();
}
Optional<String> getterField = propertyNameForGetter(methodName, method);
if (getterField.isPresent()) {
return detectGetter(ownerFqn, method, methodName, getterField.get(), fieldLookup);
}
Optional<String> setterField = propertyNameForSetter(methodName);
if (setterField.isPresent() && !method.parameters().isEmpty()) {
return detectSetter(ownerFqn, method, methodName, setterField.get(), fieldLookup);
}
return Optional.empty();
}
public Optional<AccessorSummary> detectRecordComponent(String ownerFqn, String componentName) {
if (ownerFqn == null || componentName == null || componentName.isEmpty()) {
return Optional.empty();
}
return Optional.of(new AccessorSummary(
ownerFqn,
componentName,
AccessorKind.RECORD_COMPONENT,
componentName,
null,
ownerFqn,
false
));
}
private Optional<AccessorSummary> detectGetter(
String ownerFqn,
MethodDeclaration method,
String methodName,
String fieldName,
FieldLookup fieldLookup) {
if (!isTrivialGetterBody(method.getBody(), fieldName)) {
return Optional.empty();
}
Optional<String> declaringFqn = fieldLookup.apply(fieldName);
if (declaringFqn.isEmpty()) {
return Optional.empty();
}
AccessorKind kind = methodName.startsWith("is") ? AccessorKind.BOOLEAN_GETTER : AccessorKind.GETTER;
return Optional.of(new AccessorSummary(
ownerFqn,
methodName,
kind,
fieldName,
null,
declaringFqn.get(),
false
));
}
private Optional<AccessorSummary> detectSetter(
String ownerFqn,
MethodDeclaration method,
String methodName,
String fieldName,
FieldLookup fieldLookup) {
SingleVariableDeclaration param = (SingleVariableDeclaration) method.parameters().get(0);
String paramName = param.getName().getIdentifier();
boolean fluent = isTrivialFluentSetterBody(method.getBody(), fieldName, paramName);
boolean voidSetter = isVoidReturn(method) && isTrivialVoidSetterBody(method.getBody(), fieldName, paramName);
if (!fluent && !voidSetter) {
return Optional.empty();
}
Optional<String> declaringFqn = fieldLookup.apply(fieldName);
if (declaringFqn.isEmpty()) {
return Optional.empty();
}
AccessorKind kind = fluent ? AccessorKind.FLUENT_SETTER : AccessorKind.SETTER;
return Optional.of(new AccessorSummary(
ownerFqn,
methodName,
kind,
fieldName,
paramName,
declaringFqn.get(),
fluent
));
}
private Optional<String> propertyNameForGetter(String methodName, MethodDeclaration method) {
if (methodName.startsWith("get") && methodName.length() > 3) {
return Optional.of(decapitalize(methodName.substring(3)));
}
if (methodName.startsWith("is") && methodName.length() > 2 && isBooleanReturn(method)) {
return Optional.of(decapitalize(methodName.substring(2)));
}
return Optional.empty();
}
private Optional<String> propertyNameForSetter(String methodName) {
if (methodName.startsWith("set") && methodName.length() > 3) {
return Optional.of(decapitalize(methodName.substring(3)));
}
return Optional.empty();
}
private boolean isBooleanReturn(MethodDeclaration method) {
return "boolean".equals(method.getReturnType2().toString())
|| "Boolean".equals(method.getReturnType2().toString());
}
private boolean isVoidReturn(MethodDeclaration method) {
return method.getReturnType2() instanceof PrimitiveType pt && pt.getPrimitiveTypeCode() == PrimitiveType.VOID;
}
public static String decapitalize(String name) {
if (name == null || name.isEmpty()) {
return name;
}
if (name.length() > 1 && Character.isUpperCase(name.charAt(1))) {
return name;
}
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}
private boolean isTrivialGetterBody(Block body, String fieldName) {
List<?> statements = body.statements();
if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
return false;
}
Expression expr = rs.getExpression();
if (expr == null) {
return false;
}
return isDirectFieldRead(unwrap(expr), fieldName);
}
private boolean isTrivialVoidSetterBody(Block body, String fieldName, String paramName) {
List<?> statements = body.statements();
if (statements.size() != 1) {
return false;
}
return isAssignmentStatement(statements.get(0), fieldName, paramName);
}
private boolean isTrivialFluentSetterBody(Block body, String fieldName, String paramName) {
List<?> statements = body.statements();
if (statements.size() != 2) {
return false;
}
if (!isAssignmentStatement(statements.get(0), fieldName, paramName)) {
return false;
}
if (!(statements.get(1) instanceof ReturnStatement rs)) {
return false;
}
Expression ret = unwrap(rs.getExpression());
return ret instanceof ThisExpression;
}
private boolean isAssignmentStatement(Object statement, String fieldName, String paramName) {
if (!(statement instanceof ExpressionStatement es)) {
return false;
}
if (!(es.getExpression() instanceof Assignment assignment)) {
return false;
}
if (assignment.getOperator() != Assignment.Operator.ASSIGN) {
return false;
}
if (!isDirectFieldWrite(assignment.getLeftHandSide(), fieldName)) {
return false;
}
Expression rhs = unwrap(assignment.getRightHandSide());
return rhs instanceof SimpleName sn && sn.getIdentifier().equals(paramName);
}
private boolean isDirectFieldRead(Expression expr, String fieldName) {
if (containsMethodCall(expr)) {
return false;
}
if (expr instanceof SimpleName sn) {
return sn.getIdentifier().equals(fieldName);
}
if (expr instanceof FieldAccess fa) {
Expression receiver = fa.getExpression();
if (receiver == null || receiver instanceof ThisExpression) {
return fa.getName().getIdentifier().equals(fieldName);
}
}
return false;
}
private boolean isDirectFieldWrite(Expression expr, String fieldName) {
if (expr instanceof SimpleName sn) {
return sn.getIdentifier().equals(fieldName);
}
if (expr instanceof FieldAccess fa) {
Expression receiver = fa.getExpression();
if (receiver == null || receiver instanceof ThisExpression) {
return fa.getName().getIdentifier().equals(fieldName);
}
}
return false;
}
private boolean containsMethodCall(Expression expr) {
if (expr == null) {
return false;
}
final boolean[] found = {false};
expr.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
found[0] = true;
return false;
}
});
return found[0];
}
private Expression unwrap(Expression expr) {
Expression current = expr;
while (current instanceof ParenthesizedExpression pe) {
current = pe.getExpression();
}
while (current instanceof CastExpression ce) {
current = ce.getExpression();
}
return current;
}
}

View File

@@ -1,6 +1,7 @@
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.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
@@ -1364,6 +1365,25 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
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<>());

View File

@@ -1,6 +1,8 @@
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.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
@@ -287,6 +289,16 @@ public class ConstantExtractor {
return Collections.emptyList();
}
Optional<AccessorSummary> indexedAccessor = context.getAccessorIndex().lookup(className, methodName);
if (indexedAccessor.isPresent() && indexedAccessor.get().isGetter()) {
List<String> indexedConstants = resolveIndexedGetterFieldConstants(
indexedAccessor.get(), contextCu, visited);
if (!indexedConstants.isEmpty()) {
visited.remove(fqn);
return indexedConstants;
}
}
List<String> constants = new ArrayList<>();
TypeDeclaration tempTd = contextCu != null ? context.getTypeDeclaration(className, contextCu) : null;
if (tempTd == null) {
@@ -391,6 +403,19 @@ public class ConstantExtractor {
return constants;
}
public List<String> resolveIndexedGetterFieldConstants(
AccessorSummary accessor,
CompilationUnit contextCu,
Set<String> visited) {
return AccessorFieldResolver.resolveFieldConstants(
accessor,
context,
constructorAnalyzer,
contextCu,
visited,
this::extractConstantsFromExpression);
}
public String parseEnumSetElement(String eVal) {
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
}

View File

@@ -2,6 +2,8 @@ package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
import click.kamil.springstatemachineexporter.analysis.index.TrivialAccessorDetector;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
@@ -55,11 +57,47 @@ public class VariableTracer {
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
if (md != null && md.getBody() != null) {
final Expression[] setterArg = new Expression[1];
String propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
String propName;
String setterMethodName;
String indexedFieldName;
if (isBeanStyleAccessorName(getterName)) {
propName = propertyNameFromAccessor(getterName);
setterMethodName = "set" + capitalizeProperty(propName);
indexedFieldName = propName;
String varTypeName = getVariableDeclaredType(methodFqn, varName);
if (varTypeName != null) {
CompilationUnit contextCu = md.getRoot() instanceof CompilationUnit cu ? cu : null;
TypeDeclaration varType = contextCu != null
? context.getTypeDeclaration(varTypeName, contextCu)
: null;
if (varType == null) {
varType = context.getTypeDeclaration(varTypeName);
}
if (varType != null) {
Optional<AccessorSummary> indexedSetter = context.getAccessorIndex()
.lookup(context.getFqn(varType), setterMethodName);
if (indexedSetter.isPresent() && indexedSetter.get().isSetter()) {
setterMethodName = indexedSetter.get().methodName();
indexedFieldName = indexedSetter.get().fieldName();
}
}
}
} else {
propName = getterName.startsWith("get") ? getterName.substring(3) : getterName;
setterMethodName = "set" + propName;
indexedFieldName = propName;
}
final String resolvedSetterName = setterMethodName;
final String resolvedFieldName = indexedFieldName;
md.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if (node.getName().getIdentifier().equalsIgnoreCase("set" + propName) || node.getName().getIdentifier().equalsIgnoreCase(propName)) {
if (node.getName().getIdentifier().equals(resolvedSetterName)
|| node.getName().getIdentifier().equalsIgnoreCase("set" + resolvedFieldName)
|| node.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
if (node.getExpression() instanceof SimpleName sn && sn.getIdentifier().equals(varName)) {
if (!node.arguments().isEmpty()) {
setterArg[0] = (Expression) node.arguments().get(0);
@@ -72,13 +110,13 @@ public class VariableTracer {
public boolean visit(Assignment node) {
if (node.getLeftHandSide() instanceof FieldAccess fa) {
if (fa.getExpression() instanceof SimpleName faSn && faSn.getIdentifier().equals(varName)) {
if (fa.getName().getIdentifier().equalsIgnoreCase(propName)) {
if (fa.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
setterArg[0] = node.getRightHandSide();
}
}
} else if (node.getLeftHandSide() instanceof QualifiedName qqn) {
if (qqn.getQualifier().getFullyQualifiedName().equals(varName)) {
if (qqn.getName().getIdentifier().equalsIgnoreCase(propName)) {
if (qqn.getName().getIdentifier().equalsIgnoreCase(resolvedFieldName)) {
setterArg[0] = node.getRightHandSide();
}
}
@@ -92,6 +130,32 @@ public class VariableTracer {
return null;
}
private static boolean isBeanStyleAccessorName(String accessorName) {
return (accessorName.startsWith("get") && accessorName.length() > 3)
|| (accessorName.startsWith("is") && accessorName.length() > 2)
|| (accessorName.startsWith("set") && accessorName.length() > 3);
}
private static String propertyNameFromAccessor(String getterName) {
if (getterName.startsWith("get") && getterName.length() > 3) {
return TrivialAccessorDetector.decapitalize(getterName.substring(3));
}
if (getterName.startsWith("is") && getterName.length() > 2) {
return TrivialAccessorDetector.decapitalize(getterName.substring(2));
}
return getterName;
}
private static String capitalizeProperty(String propertyName) {
if (propertyName == null || propertyName.isEmpty()) {
return propertyName;
}
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
return propertyName;
}
return Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
}
private String getFieldType(TypeDeclaration td, String fieldName, CodebaseContext context, java.util.Set<String> visited) {
if (td == null) return null;
String typeFqn = context.getFqn(td);

View File

@@ -1,5 +1,7 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndex;
import click.kamil.springstatemachineexporter.analysis.index.AccessorIndexBuilder;
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
@@ -42,11 +44,16 @@ public class CodebaseContext {
private final ObjectMapper objectMapper = new ObjectMapper();
private Path projectRoot;
private final Map<String, Object> cache = new HashMap<>();
private AccessorIndex accessorIndex = AccessorIndex.empty();
public Map<String, Object> getCache() {
return cache;
}
public AccessorIndex getAccessorIndex() {
return accessorIndex;
}
public void setProjectRoot(Path root) {
this.projectRoot = root;
}
@@ -239,6 +246,8 @@ public class CodebaseContext {
}
}
}
this.accessorIndex = new AccessorIndexBuilder().build(this);
}
private void indexType(TypeDeclaration td, String parentFqn, Path javaFile) {

View File

@@ -1,5 +1,7 @@
package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.analysis.index.AccessorInlining;
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
import org.eclipse.jdt.core.dom.*;
import java.util.*;
@@ -199,6 +201,14 @@ public class JdtDataFlowModel implements DataFlowModel {
visited.remove(expr);
return resolved;
}
List<Expression> inlined = tryInlineAccessorInvocation(
mi, new HashSet<>(visited), paramBindings, instanceFieldBindings, depth);
if (!inlined.isEmpty()) {
visited.remove(expr);
return inlined;
}
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, instanceFieldBindings, depth);
@@ -293,6 +303,433 @@ public class JdtDataFlowModel implements DataFlowModel {
return List.of(expr);
}
private List<Expression> tryInlineAccessorInvocation(
MethodInvocation mi,
Set<ASTNode> visited,
Map<IVariableBinding, Expression> paramBindings,
Map<IVariableBinding, Expression> instanceFieldBindings,
int depth) {
if (depth > 25) {
return List.of();
}
String methodName = mi.getName().getIdentifier();
IMethodBinding methodBinding = mi.resolveMethodBinding();
if (methodBinding == null || methodBinding.getDeclaringClass() == null) {
return List.of();
}
String declaringFqn = methodBinding.getDeclaringClass().getErasure().getQualifiedName();
Optional<AccessorSummary> accessor = context.getAccessorIndex().lookup(declaringFqn, methodName);
if (accessor.isEmpty() && !looksLikeIndexedAccessorName(methodName)) {
return List.of();
}
List<Expression> receiverDefs = List.of();
if (accessor.isEmpty()) {
if (!methodBinding.getDeclaringClass().isInterface()) {
return List.of();
}
Expression receiver = mi.getExpression();
if (receiver == null) {
return List.of();
}
receiverDefs = getReachingDefinitions(receiver, visited, paramBindings, instanceFieldBindings, depth + 1);
accessor = AccessorInlining.lookupAccessor(context, mi, receiverDefs);
if (accessor.isEmpty()) {
return List.of();
}
}
if (receiverDefs.isEmpty() && mi.getExpression() != null) {
receiverDefs = getReachingDefinitions(
mi.getExpression(), visited, paramBindings, instanceFieldBindings, depth + 1);
}
if (accessor.isPresent()
&& shouldSkipAccessorDueToConcreteOverride(declaringFqn, methodName, receiverDefs)) {
return List.of();
}
if (accessor.isEmpty()) {
return List.of();
}
if (!accessor.get().isGetter()) {
return List.of();
}
if (accessor.get().kind() == click.kamil.springstatemachineexporter.analysis.index.AccessorKind.RECORD_COMPONENT) {
return inlineRecordComponentRead(mi, accessor.get(), receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
}
return inlineAccessorGetter(mi, accessor.get(), receiverDefs, visited, paramBindings, instanceFieldBindings, depth);
}
private boolean shouldSkipAccessorDueToConcreteOverride(
String declaringFqn,
String methodName,
List<Expression> receiverDefs) {
for (Expression receiverDef : receiverDefs) {
String concreteFqn = concreteReceiverTypeFqn(receiverDef);
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;
}
}
return false;
}
private String concreteReceiverTypeFqn(Expression receiverDef) {
if (receiverDef instanceof ClassInstanceCreation cic) {
return AccessorInlining.resolveTypeFqn(cic);
}
ITypeBinding typeBinding = receiverDef.resolveTypeBinding();
if (typeBinding != null) {
return typeBinding.getErasure().getQualifiedName();
}
return null;
}
private boolean looksLikeIndexedAccessorName(String methodName) {
return (methodName.startsWith("get") && methodName.length() > 3)
|| (methodName.startsWith("set") && methodName.length() > 3)
|| (methodName.startsWith("is") && methodName.length() > 2);
}
private List<Expression> inlineRecordComponentRead(
MethodInvocation mi,
AccessorSummary accessor,
List<Expression> receiverDefs,
Set<ASTNode> visited,
Map<IVariableBinding, Expression> paramBindings,
Map<IVariableBinding, Expression> instanceFieldBindings,
int depth) {
Expression receiver = mi.getExpression();
List<Expression> candidates = receiverDefs.isEmpty() && receiver != null ? List.of(receiver) : receiverDefs;
List<Expression> results = new ArrayList<>();
for (Expression candidate : candidates) {
if (candidate instanceof ClassInstanceCreation cic) {
Expression componentValue = readRecordComponentValue(cic, accessor.fieldName());
if (componentValue != null) {
results.addAll(getReachingDefinitions(componentValue, visited, paramBindings, instanceFieldBindings, depth + 1));
}
continue;
}
if (candidate instanceof MethodInvocation receiverCall) {
List<Expression> nested = tryInlineAccessorInvocation(
receiverCall, new HashSet<>(visited), paramBindings, instanceFieldBindings, depth + 1);
for (Expression nestedValue : nested) {
if (nestedValue instanceof ClassInstanceCreation cic) {
Expression componentValue = readRecordComponentValue(cic, accessor.fieldName());
if (componentValue != null) {
results.addAll(getReachingDefinitions(
componentValue, visited, paramBindings, instanceFieldBindings, depth + 1));
}
} else {
results.addAll(getReachingDefinitions(nestedValue, visited, paramBindings, instanceFieldBindings, depth + 1));
}
}
}
}
return results;
}
private Expression readRecordComponentValue(ClassInstanceCreation cic, String componentName) {
org.eclipse.jdt.core.dom.RecordDeclaration record = findRecordDeclaration(AccessorInlining.resolveTypeFqn(cic));
if (record == null) {
return null;
}
int componentIndex = -1;
List<?> components = record.recordComponents();
for (int i = 0; i < components.size(); i++) {
Object componentObj = components.get(i);
String name = recordComponentName(componentObj);
if (componentName.equals(name)) {
componentIndex = i;
break;
}
}
if (componentIndex < 0 || componentIndex >= cic.arguments().size()) {
return null;
}
return (Expression) cic.arguments().get(componentIndex);
}
private String recordComponentName(Object componentObj) {
if (componentObj instanceof SingleVariableDeclaration svd) {
return svd.getName().getIdentifier();
}
try {
java.lang.reflect.Method getNameMethod = componentObj.getClass().getMethod("getName");
Object nameObj = getNameMethod.invoke(componentObj);
if (nameObj instanceof SimpleName sn) {
return sn.getIdentifier();
}
} catch (ReflectiveOperationException ignored) {
// fall through
}
return null;
}
private List<Expression> inlineAccessorGetter(
MethodInvocation mi,
AccessorSummary accessor,
List<Expression> receiverDefs,
Set<ASTNode> visited,
Map<IVariableBinding, Expression> paramBindings,
Map<IVariableBinding, Expression> instanceFieldBindings,
int depth) {
Expression receiver = mi.getExpression();
List<Expression> results = new ArrayList<>();
if (receiver == null || receiver instanceof ThisExpression) {
Expression fieldValue = readFieldValue(accessor, instanceFieldBindings, mi);
if (fieldValue != null) {
results.addAll(getReachingDefinitions(fieldValue, visited, paramBindings, instanceFieldBindings, depth + 1));
}
return results;
}
List<Expression> candidates = receiverDefs.isEmpty() ? List.of(receiver) : receiverDefs;
for (Expression candidate : candidates) {
if (candidate instanceof ClassInstanceCreation cic) {
Map<IVariableBinding, Expression> fieldBindings =
getOrCreateFieldBindings(cic, paramBindings, instanceFieldBindings);
if (receiver instanceof SimpleName sn) {
applyFlowSensitiveMutationsBeforeUse(sn, mi, cic, fieldBindings, paramBindings, visited, depth);
} else if (receiver instanceof FieldAccess fa) {
applyFlowSensitiveFieldMutationsBeforeUse(fa, mi, fieldBindings, paramBindings, visited, depth);
}
Expression fieldValue = readFieldValue(accessor, fieldBindings, mi);
if (fieldValue != null) {
results.addAll(getReachingDefinitions(fieldValue, visited, paramBindings, fieldBindings, depth + 1));
}
}
}
return results;
}
private void applyFlowSensitiveMutationsBeforeUse(
SimpleName receiverName,
MethodInvocation useSite,
ClassInstanceCreation creationSite,
Map<IVariableBinding, Expression> fieldBindings,
Map<IVariableBinding, Expression> paramBindings,
Set<ASTNode> visited,
int depth) {
MethodDeclaration enclosingMethod = findEnclosingMethod(useSite);
if (enclosingMethod == null) {
return;
}
ReachingDefinitions rd = getReachingDefinitionsForMethod(enclosingMethod);
if (rd == null) {
return;
}
Set<ASTNode> defs = rd.getReachingDefinitions(receiverName, receiverName.getIdentifier());
for (ASTNode def : defs) {
Expression defExpr = ReachingDefinitions.getDefinitionExpression(def);
if (defExpr == creationSite || isExpressionWrapping(defExpr, creationSite)) {
applyIntermediateMutations(receiverName, def, useSite, enclosingMethod, fieldBindings, paramBindings, visited, depth);
}
}
}
private void applyFlowSensitiveFieldMutationsBeforeUse(
FieldAccess fieldReceiver,
MethodInvocation useSite,
Map<IVariableBinding, Expression> fieldBindings,
Map<IVariableBinding, Expression> paramBindings,
Set<ASTNode> visited,
int depth) {
Expression qualifier = fieldReceiver.getExpression();
if (qualifier != null && !(qualifier instanceof ThisExpression)) {
return;
}
MethodDeclaration enclosingMethod = findEnclosingMethod(useSite);
if (enclosingMethod == null || enclosingMethod.getBody() == null) {
return;
}
int useStart = useSite.getStartPosition();
String targetFieldName = fieldReceiver.getName().getIdentifier();
enclosingMethod.getBody().accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation node) {
if (node.getStartPosition() >= useStart) {
return false;
}
if (!isThisFieldAccess(node.getExpression(), targetFieldName)) {
return super.visit(node);
}
List<Expression> receiverDefs = getReachingDefinitions(
node.getExpression(), visited, paramBindings, fieldBindings, depth + 1);
Optional<AccessorSummary> accessor = AccessorInlining.lookupAccessor(context, node, receiverDefs);
if (accessor.isPresent() && accessor.get().isSetter()) {
inlineAccessorSetter(node, accessor.get(), fieldBindings, paramBindings, visited, depth);
return super.visit(node);
}
IMethodBinding mb = node.resolveMethodBinding();
if (mb != null) {
List<TargetMethod> targets = resolveTargets(node, mb, visited, paramBindings, fieldBindings, depth + 1);
for (TargetMethod target : targets) {
if (target.declaration != null) {
evaluateMethodMutations(target.declaration, node.arguments(), fieldBindings);
}
}
}
return super.visit(node);
}
});
}
private boolean isThisFieldAccess(Expression expr, String fieldName) {
if (!(expr instanceof FieldAccess fa)) {
return false;
}
Expression receiver = fa.getExpression();
return fa.getName().getIdentifier().equals(fieldName)
&& (receiver == null || receiver instanceof ThisExpression);
}
private Expression readFieldValue(
AccessorSummary accessor,
Map<IVariableBinding, Expression> fieldBindings,
ASTNode contextNode) {
IVariableBinding fieldBinding = findFieldVariableBinding(accessor.declaringFqn(), accessor.fieldName(), contextNode);
if (fieldBinding != null && fieldBindings.containsKey(fieldBinding)) {
return fieldBindings.get(fieldBinding);
}
if (fieldBinding != null) {
return findFieldInitializer(fieldBinding, contextNode);
}
return null;
}
private IVariableBinding findFieldVariableBinding(String declaringFqn, String fieldName, ASTNode contextNode) {
if (declaringFqn == null || fieldName == null) {
return null;
}
TypeDeclaration typeDeclaration = context.getTypeDeclaration(declaringFqn);
if (typeDeclaration != null) {
IVariableBinding binding = findFieldOnType(typeDeclaration, fieldName, new HashSet<>());
if (binding != null) {
return binding;
}
}
org.eclipse.jdt.core.dom.RecordDeclaration recordDeclaration = findRecordDeclaration(declaringFqn);
if (recordDeclaration != null) {
for (Object componentObj : recordDeclaration.recordComponents()) {
if (componentObj instanceof SingleVariableDeclaration component
&& component.getName().getIdentifier().equals(fieldName)) {
return component.resolveBinding();
}
}
}
return null;
}
private IVariableBinding findFieldOnType(TypeDeclaration typeDeclaration, String fieldName, Set<String> visitedTypes) {
if (typeDeclaration == null) {
return null;
}
String typeFqn = context.getFqn(typeDeclaration);
if (!visitedTypes.add(typeFqn)) {
return null;
}
for (FieldDeclaration fieldDeclaration : typeDeclaration.getFields()) {
for (Object fragmentObj : fieldDeclaration.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment
&& fragment.getName().getIdentifier().equals(fieldName)) {
IVariableBinding binding = fragment.resolveBinding();
if (binding != null) {
return binding;
}
}
}
}
String superFqn = context.getSuperclassFqn(typeDeclaration);
if (superFqn != null && !"java.lang.Object".equals(superFqn)) {
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
if (superType != null) {
return findFieldOnType(superType, fieldName, visitedTypes);
}
}
return null;
}
private org.eclipse.jdt.core.dom.RecordDeclaration findRecordDeclaration(String fqn) {
for (CompilationUnit cu : context.getCompilationUnits()) {
for (Object typeObj : cu.types()) {
org.eclipse.jdt.core.dom.RecordDeclaration found = findRecordInType(fqn, typeObj);
if (found != null) {
return found;
}
}
}
return null;
}
private org.eclipse.jdt.core.dom.RecordDeclaration findRecordInType(String fqn, Object typeObj) {
if (typeObj instanceof org.eclipse.jdt.core.dom.RecordDeclaration recordDeclaration) {
if (context.getFqn(recordDeclaration).equals(fqn)) {
return recordDeclaration;
}
for (Object decl : recordDeclaration.bodyDeclarations()) {
if (decl instanceof org.eclipse.jdt.core.dom.RecordDeclaration nested) {
org.eclipse.jdt.core.dom.RecordDeclaration found = findRecordInType(fqn, nested);
if (found != null) {
return found;
}
}
}
}
if (typeObj instanceof TypeDeclaration typeDeclaration) {
for (TypeDeclaration nested : typeDeclaration.getTypes()) {
org.eclipse.jdt.core.dom.RecordDeclaration found = findRecordInType(fqn, nested);
if (found != null) {
return found;
}
}
}
return null;
}
private void inlineAccessorSetter(
MethodInvocation mi,
AccessorSummary accessor,
Map<IVariableBinding, Expression> fieldBindings,
Map<IVariableBinding, Expression> paramBindings,
Set<ASTNode> visited,
int depth) {
if (mi.arguments().isEmpty()) {
return;
}
Expression argument = (Expression) mi.arguments().get(0);
List<Expression> resolvedArgument = getReachingDefinitions(argument, visited, paramBindings, fieldBindings, depth + 1);
Expression value = resolvedArgument.isEmpty() ? argument : resolvedArgument.get(0);
IVariableBinding fieldBinding = findFieldVariableBinding(accessor.declaringFqn(), accessor.fieldName(), mi);
if (fieldBinding != null) {
fieldBindings.put(fieldBinding, value);
}
}
private IVariableBinding getVariableBinding(Expression expr) {
if (expr instanceof SimpleName sn) {
IBinding b = sn.resolveBinding();
@@ -704,6 +1141,22 @@ public class JdtDataFlowModel implements DataFlowModel {
Map<IVariableBinding, Expression> fieldBindings) {
if (md == null || md.getBody() == null) return;
ASTNode parent = md.getParent();
if (parent instanceof AbstractTypeDeclaration typeDeclaration) {
String ownerFqn = context.getFqn(typeDeclaration);
Optional<AccessorSummary> accessor = context.getAccessorIndex().lookup(ownerFqn, md.getName().getIdentifier());
if (accessor.isPresent() && accessor.get().isSetter() && !arguments.isEmpty()) {
IVariableBinding fieldBinding = findFieldVariableBinding(
accessor.get().declaringFqn(),
accessor.get().fieldName(),
md);
if (fieldBinding != null) {
fieldBindings.put(fieldBinding, (Expression) arguments.get(0));
return;
}
}
}
Map<IVariableBinding, Expression> currentParamValues = new HashMap<>();
List<?> parameters = md.parameters();
int count = Math.min(parameters.size(), arguments.size());
@@ -790,6 +1243,14 @@ public class JdtDataFlowModel implements DataFlowModel {
public boolean visit(MethodInvocation mi) {
Expression receiver = mi.getExpression();
if (receiver != null && isTargetVariable(receiver, targetVar, targetName)) {
List<Expression> receiverDefs = getReachingDefinitions(
receiver, visited, paramBindings, fieldBindings, depth + 1);
Optional<AccessorSummary> accessor = AccessorInlining.lookupAccessor(context, mi, receiverDefs);
if (accessor.isPresent() && accessor.get().isSetter()) {
inlineAccessorSetter(mi, accessor.get(), fieldBindings, paramBindings, visited, depth);
return super.visit(mi);
}
IMethodBinding mb = mi.resolveMethodBinding();
if (mb != null) {
List<TargetMethod> targets = resolveTargets(mi, mb, visited, paramBindings, fieldBindings, depth + 1);