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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.index;
|
||||
|
||||
public enum AccessorKind {
|
||||
GETTER,
|
||||
BOOLEAN_GETTER,
|
||||
RECORD_COMPONENT,
|
||||
SETTER,
|
||||
FLUENT_SETTER
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<>());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user