E
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.flow;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared binder: {@link CallEdge} argument text → unique AST argument expression in the caller body.
|
||||||
|
* Fail-closed when zero or multiple sites match (no pick-any).
|
||||||
|
* Used by allocation facts and event-identity string forwarding.
|
||||||
|
*/
|
||||||
|
public final class CallSiteArgBinder {
|
||||||
|
|
||||||
|
private CallSiteArgBinder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the unique caller AST argument for {@code edge}'s {@code paramIndex}, or empty
|
||||||
|
*/
|
||||||
|
public static Optional<Expression> findUniqueArgument(
|
||||||
|
MethodDeclaration callerBody,
|
||||||
|
CallEdge edge,
|
||||||
|
int paramIndex) {
|
||||||
|
if (callerBody == null || callerBody.getBody() == null || edge == null || edge.getTargetMethod() == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (paramIndex < 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String targetSimple = methodNameOf(edge.getTargetMethod());
|
||||||
|
if (targetSimple == null || targetSimple.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String edgeArgText = null;
|
||||||
|
if (edge.getArguments() != null && paramIndex < edge.getArguments().size()) {
|
||||||
|
edgeArgText = normalizeExprText(edge.getArguments().get(paramIndex));
|
||||||
|
}
|
||||||
|
List<Expression> matches = new ArrayList<>();
|
||||||
|
callerBody.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (!targetSimple.equals(node.getName().getIdentifier())) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
if (paramIndex >= node.arguments().size()) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
matches.add((Expression) node.arguments().get(paramIndex));
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (matches.size() == 1) {
|
||||||
|
return Optional.of(matches.get(0));
|
||||||
|
}
|
||||||
|
if (edgeArgText == null || edgeArgText.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
List<Expression> textHits = new ArrayList<>();
|
||||||
|
for (Expression candidate : matches) {
|
||||||
|
if (edgeArgText.equals(normalizeExprText(candidate.toString()))) {
|
||||||
|
textHits.add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (textHits.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
// Identical arg text at several sites is equivalent — pick the first.
|
||||||
|
// Differing texts that somehow normalize equal still share one edge string.
|
||||||
|
return Optional.of(textHits.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether two method FQNs refer to the same simple method (path hop matching).
|
||||||
|
*/
|
||||||
|
public static boolean methodsMatch(String left, String right) {
|
||||||
|
if (left == null || right == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (left.equals(right)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String leftSimple = methodNameOf(left);
|
||||||
|
String rightSimple = methodNameOf(right);
|
||||||
|
return leftSimple != null && leftSimple.equals(rightSimple)
|
||||||
|
&& (left.endsWith("." + rightSimple) || right.endsWith("." + leftSimple));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String methodNameOf(String methodFqn) {
|
||||||
|
if (methodFqn == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int lastDot = methodFqn.lastIndexOf('.');
|
||||||
|
return lastDot >= 0 ? methodFqn.substring(lastDot + 1) : methodFqn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeExprText(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return text.replaceAll("\\s+", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeExprText(Expression expression) {
|
||||||
|
if (expression == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeExprText(expression.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.flow.alloc;
|
package click.kamil.springstatemachineexporter.analysis.flow.alloc;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.flow.CallSiteArgBinder;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
|
||||||
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.ASTNode;
|
import org.eclipse.jdt.core.dom.ASTNode;
|
||||||
@@ -13,16 +15,21 @@ import org.eclipse.jdt.core.dom.CastExpression;
|
|||||||
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
import org.eclipse.jdt.core.dom.FieldAccess;
|
import org.eclipse.jdt.core.dom.FieldAccess;
|
||||||
|
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.IBinding;
|
||||||
import org.eclipse.jdt.core.dom.IMethodBinding;
|
import org.eclipse.jdt.core.dom.IMethodBinding;
|
||||||
import org.eclipse.jdt.core.dom.ITypeBinding;
|
import org.eclipse.jdt.core.dom.ITypeBinding;
|
||||||
|
import org.eclipse.jdt.core.dom.IVariableBinding;
|
||||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
import org.eclipse.jdt.core.dom.MethodInvocation;
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
import org.eclipse.jdt.core.dom.Modifier;
|
||||||
import org.eclipse.jdt.core.dom.Name;
|
import org.eclipse.jdt.core.dom.Name;
|
||||||
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
import org.eclipse.jdt.core.dom.SimpleName;
|
||||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
||||||
import org.eclipse.jdt.core.dom.ThisExpression;
|
import org.eclipse.jdt.core.dom.ThisExpression;
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -33,6 +40,8 @@ import java.util.Set;
|
|||||||
/**
|
/**
|
||||||
* Path-scoped allocation fact propagator: CIC → locals → callee parameters via call-edge args,
|
* Path-scoped allocation fact propagator: CIC → locals → callee parameters via call-edge args,
|
||||||
* factory {@code return new T()}, and trivial getter→field→ctor-arg peels.
|
* factory {@code return new T()}, and trivial getter→field→ctor-arg peels.
|
||||||
|
* Optionally treats a unique Spring {@code @Bean}/component as an alloc substitute when
|
||||||
|
* {@link InjectionPointAnalyzer} is wired (multi-bean / qualifier conflict → empty).
|
||||||
* Fail-closed when facts are empty or ambiguous (multi-site).
|
* Fail-closed when facts are empty or ambiguous (multi-site).
|
||||||
*/
|
*/
|
||||||
public final class AllocationFactPropagator {
|
public final class AllocationFactPropagator {
|
||||||
@@ -42,6 +51,7 @@ public final class AllocationFactPropagator {
|
|||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
private final VariableTracer variableTracer;
|
private final VariableTracer variableTracer;
|
||||||
private final ConstructorAnalyzer constructorAnalyzer;
|
private final ConstructorAnalyzer constructorAnalyzer;
|
||||||
|
private InjectionPointAnalyzer injectionAnalyzer;
|
||||||
|
|
||||||
public AllocationFactPropagator(CodebaseContext context, VariableTracer variableTracer) {
|
public AllocationFactPropagator(CodebaseContext context, VariableTracer variableTracer) {
|
||||||
this(context, variableTracer, null);
|
this(context, variableTracer, null);
|
||||||
@@ -56,19 +66,25 @@ public final class AllocationFactPropagator {
|
|||||||
this.constructorAnalyzer = constructorAnalyzer;
|
this.constructorAnalyzer = constructorAnalyzer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setInjectionAnalyzer(InjectionPointAnalyzer injectionAnalyzer) {
|
||||||
|
this.injectionAnalyzer = injectionAnalyzer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allocation facts for {@code expression} within {@code enclosingMethod}, optionally
|
* Allocation facts for {@code expression} within {@code enclosingMethod}, optionally
|
||||||
* refined by walking {@code methodPath} call edges backward for parameter receivers.
|
* refined by walking {@code methodPath} call edges backward for parameter receivers.
|
||||||
|
* Dominating {@code instanceof} / pattern narrowing is merged via {@link InstanceofNarrower}.
|
||||||
*/
|
*/
|
||||||
public ReceiverFacts factsAt(
|
public ReceiverFacts factsAt(
|
||||||
Expression expression,
|
Expression expression,
|
||||||
MethodDeclaration enclosingMethod,
|
MethodDeclaration enclosingMethod,
|
||||||
List<String> methodPath,
|
List<String> methodPath,
|
||||||
Map<String, List<CallEdge>> callGraph) {
|
Map<String, List<CallEdge>> callGraph) {
|
||||||
return factsAt(expression, enclosingMethod, methodPath, callGraph, 0, new LinkedHashSet<>());
|
ReceiverFacts base = factsAtRaw(expression, enclosingMethod, methodPath, callGraph, 0, new LinkedHashSet<>());
|
||||||
|
return InstanceofNarrower.refine(base, expression, expression, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReceiverFacts factsAt(
|
private ReceiverFacts factsAtRaw(
|
||||||
Expression expression,
|
Expression expression,
|
||||||
MethodDeclaration enclosingMethod,
|
MethodDeclaration enclosingMethod,
|
||||||
List<String> methodPath,
|
List<String> methodPath,
|
||||||
@@ -116,7 +132,7 @@ public final class AllocationFactPropagator {
|
|||||||
// Follow aliases / casts / factories: RichEvent carrier = e; e = (RichEvent) o;
|
// Follow aliases / casts / factories: RichEvent carrier = e; e = (RichEvent) o;
|
||||||
String defKey = "alias:" + System.identityHashCode(defPeeled);
|
String defKey = "alias:" + System.identityHashCode(defPeeled);
|
||||||
if (visited.add(defKey)) {
|
if (visited.add(defKey)) {
|
||||||
ReceiverFacts nested = factsAt(
|
ReceiverFacts nested = factsAtRaw(
|
||||||
defPeeled, enclosingMethod, methodPath, callGraph, depth + 1, visited);
|
defPeeled, enclosingMethod, methodPath, callGraph, depth + 1, visited);
|
||||||
sites.addAll(nested.sites());
|
sites.addAll(nested.sites());
|
||||||
}
|
}
|
||||||
@@ -146,11 +162,62 @@ public final class AllocationFactPropagator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instance field: unique path-scoped write this.f = provenParam/CIC (or setF(...)).
|
||||||
|
ReceiverFacts fromField = factsFromInstanceFieldStore(
|
||||||
|
peeled, enclosingMethod, methodPath, callGraph, depth, visited);
|
||||||
|
if (!fromField.isEmpty()) {
|
||||||
|
return fromField;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unique Spring bean injected into an interface/abstract field (when analyzer is wired).
|
||||||
|
ReceiverFacts fromBean = factsFromUniqueInjectedBean(peeled);
|
||||||
|
if (!fromBean.isEmpty()) {
|
||||||
|
return fromBean;
|
||||||
|
}
|
||||||
|
|
||||||
return ReceiverFacts.empty();
|
return ReceiverFacts.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ReceiverFacts factsFromUniqueInjectedBean(Expression expression) {
|
||||||
|
if (injectionAnalyzer == null || expression == null) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
IVariableBinding varBinding = null;
|
||||||
|
Expression peeled = peel(expression);
|
||||||
|
if (peeled instanceof SimpleName sn) {
|
||||||
|
IBinding binding = sn.resolveBinding();
|
||||||
|
if (binding instanceof IVariableBinding vb) {
|
||||||
|
varBinding = vb;
|
||||||
|
}
|
||||||
|
} else if (peeled instanceof FieldAccess fa) {
|
||||||
|
varBinding = fa.resolveFieldBinding();
|
||||||
|
}
|
||||||
|
if (varBinding == null) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
String concreteFqn = injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||||
|
if (concreteFqn == null || concreteFqn.isBlank()) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(concreteFqn);
|
||||||
|
if (td == null) {
|
||||||
|
String simple = concreteFqn.contains(".")
|
||||||
|
? concreteFqn.substring(concreteFqn.lastIndexOf('.') + 1)
|
||||||
|
: concreteFqn;
|
||||||
|
td = context.getTypeDeclaration(simple);
|
||||||
|
if (td != null) {
|
||||||
|
concreteFqn = context.getFqn(td);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (td == null || td.isInterface() || Modifier.isAbstract(td.getModifiers())) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
return ReceiverFacts.of(new AllocationSite(concreteFqn));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Facts for the receiver of a virtual accessor call ({@code e.getType()}).
|
* Facts for the receiver of a virtual accessor call ({@code e.getType()}).
|
||||||
|
* Uses the accessor invocation as instanceof use-site so then-branch narrowing applies.
|
||||||
*/
|
*/
|
||||||
public ReceiverFacts factsForAccessorReceiver(
|
public ReceiverFacts factsForAccessorReceiver(
|
||||||
MethodInvocation accessorCall,
|
MethodInvocation accessorCall,
|
||||||
@@ -164,7 +231,9 @@ public final class AllocationFactPropagator {
|
|||||||
return ReceiverFacts.empty();
|
return ReceiverFacts.empty();
|
||||||
}
|
}
|
||||||
MethodDeclaration enclosing = findEnclosingMethod(accessorCall);
|
MethodDeclaration enclosing = findEnclosingMethod(accessorCall);
|
||||||
return factsAt(receiver, enclosing, methodPath, callGraph);
|
ReceiverFacts base = factsAtRaw(
|
||||||
|
receiver, enclosing, methodPath, callGraph, 0, new LinkedHashSet<>());
|
||||||
|
return InstanceofNarrower.refine(base, receiver, accessorCall, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReceiverFacts factsFromFactoryReturn(MethodInvocation mi, int depth, Set<String> visited) {
|
private ReceiverFacts factsFromFactoryReturn(MethodInvocation mi, int depth, Set<String> visited) {
|
||||||
@@ -207,22 +276,238 @@ public final class AllocationFactPropagator {
|
|||||||
return ReceiverFacts.empty();
|
return ReceiverFacts.empty();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ReceiverFacts receiverFacts = factsAt(
|
ReceiverFacts receiverFacts = factsAtRaw(
|
||||||
mi.getExpression(), enclosingMethod, methodPath, callGraph, depth + 1, visited);
|
mi.getExpression(), enclosingMethod, methodPath, callGraph, depth + 1, visited);
|
||||||
AllocationSite unique = receiverFacts.uniqueOrNull();
|
AllocationSite unique = receiverFacts.uniqueOrNull();
|
||||||
if (unique == null || unique.cic() == null) {
|
if (unique != null && unique.cic() != null) {
|
||||||
return ReceiverFacts.empty();
|
|
||||||
}
|
|
||||||
Expression ctorArg = resolveGetterToCtorArg(unique, mi.getName().getIdentifier());
|
Expression ctorArg = resolveGetterToCtorArg(unique, mi.getName().getIdentifier());
|
||||||
if (ctorArg == null) {
|
if (ctorArg != null) {
|
||||||
return ReceiverFacts.empty();
|
return factsAtRaw(ctorArg, enclosingMethod, methodPath, callGraph, depth + 1, visited);
|
||||||
}
|
}
|
||||||
return factsAt(ctorArg, enclosingMethod, methodPath, callGraph, depth + 1, visited);
|
}
|
||||||
|
// Trivial setter peel: w.setEvent(new T()); … w.getEvent()
|
||||||
|
String fieldName = javaBeansFieldName(mi.getName().getIdentifier());
|
||||||
|
ReceiverFacts fromSetter = factsFromUniqueSetterWrite(
|
||||||
|
mi.getExpression(),
|
||||||
|
fieldName,
|
||||||
|
mi.getName().getIdentifier(),
|
||||||
|
enclosingMethod,
|
||||||
|
methodPath,
|
||||||
|
callGraph,
|
||||||
|
depth,
|
||||||
|
visited);
|
||||||
|
if (!fromSetter.isEmpty()) {
|
||||||
|
return fromSetter;
|
||||||
|
}
|
||||||
|
return ReceiverFacts.empty();
|
||||||
} finally {
|
} finally {
|
||||||
visited.remove(peelKey);
|
visited.remove(peelKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path-scoped instance field store: {@code this.carrier = e} or {@code setCarrier(e)} with a
|
||||||
|
* unique proven RHS on methods appearing on {@code methodPath}.
|
||||||
|
*/
|
||||||
|
private ReceiverFacts factsFromInstanceFieldStore(
|
||||||
|
Expression peeled,
|
||||||
|
MethodDeclaration enclosingMethod,
|
||||||
|
List<String> methodPath,
|
||||||
|
Map<String, List<CallEdge>> callGraph,
|
||||||
|
int depth,
|
||||||
|
Set<String> visited) {
|
||||||
|
if (enclosingMethod == null || methodPath == null || depth > MAX_DEPTH) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
String fieldName = instanceFieldName(peeled);
|
||||||
|
if (fieldName == null) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
TypeDeclaration owner = findEnclosingType(enclosingMethod);
|
||||||
|
if (owner == null || !ownerDeclaresField(owner, fieldName)) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
String visitKey = "fieldStore:" + context.getFqn(owner) + "#" + fieldName;
|
||||||
|
if (!visited.add(visitKey)) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
LinkedHashSet<AllocationSite> sites = new LinkedHashSet<>();
|
||||||
|
int writeCount = 0;
|
||||||
|
for (String hop : methodPath) {
|
||||||
|
if (hop == null || !hopContainsType(hop, owner)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MethodDeclaration md = findMethodDeclaration(hop);
|
||||||
|
if (md == null || md.getBody() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Expression> rhsList = collectFieldWrites(md, fieldName);
|
||||||
|
for (Expression rhs : rhsList) {
|
||||||
|
writeCount++;
|
||||||
|
ReceiverFacts nested = factsAtRaw(rhs, md, methodPath, callGraph, depth + 1, visited);
|
||||||
|
sites.addAll(nested.sites());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (writeCount == 0 || sites.isEmpty()) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
// Ambiguous: multiple writes that disagree → fail closed via ofAll size; also reject
|
||||||
|
// multiple writes even if they agree on type (safer path-scoped uniqueness).
|
||||||
|
if (writeCount != 1) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
return ReceiverFacts.ofAll(sites);
|
||||||
|
} finally {
|
||||||
|
visited.remove(visitKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique {@code receiver.setX(arg)} / {@code setX(arg)} on path whose arg has allocation facts.
|
||||||
|
*/
|
||||||
|
private ReceiverFacts factsFromUniqueSetterWrite(
|
||||||
|
Expression receiver,
|
||||||
|
String fieldName,
|
||||||
|
String getterName,
|
||||||
|
MethodDeclaration enclosingMethod,
|
||||||
|
List<String> methodPath,
|
||||||
|
Map<String, List<CallEdge>> callGraph,
|
||||||
|
int depth,
|
||||||
|
Set<String> visited) {
|
||||||
|
if (receiver == null || methodPath == null || fieldName == null || fieldName.isBlank()) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
String setterName = "set" + Character.toUpperCase(fieldName.charAt(0))
|
||||||
|
+ (fieldName.length() > 1 ? fieldName.substring(1) : "");
|
||||||
|
String receiverText = CallSiteArgBinder.normalizeExprText(receiver);
|
||||||
|
LinkedHashSet<AllocationSite> sites = new LinkedHashSet<>();
|
||||||
|
int matchCount = 0;
|
||||||
|
Set<Integer> seenBodies = new LinkedHashSet<>();
|
||||||
|
for (String hop : methodPath) {
|
||||||
|
MethodDeclaration md = findMethodDeclaration(hop);
|
||||||
|
if (md == null || md.getBody() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int bodyId = System.identityHashCode(md);
|
||||||
|
if (!seenBodies.add(bodyId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Expression> setterArgs = collectSetterArgs(md, setterName, receiverText);
|
||||||
|
for (Expression arg : setterArgs) {
|
||||||
|
matchCount++;
|
||||||
|
ReceiverFacts nested = factsAtRaw(arg, md, methodPath, callGraph, depth + 1, visited);
|
||||||
|
sites.addAll(nested.sites());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Same-method setter before getter (path may not re-list enclosing).
|
||||||
|
if (enclosingMethod != null && enclosingMethod.getBody() != null
|
||||||
|
&& seenBodies.add(System.identityHashCode(enclosingMethod))) {
|
||||||
|
List<Expression> local = collectSetterArgs(enclosingMethod, setterName, receiverText);
|
||||||
|
for (Expression arg : local) {
|
||||||
|
matchCount++;
|
||||||
|
ReceiverFacts nested = factsAtRaw(
|
||||||
|
arg, enclosingMethod, methodPath, callGraph, depth + 1, visited);
|
||||||
|
sites.addAll(nested.sites());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchCount != 1 || sites.isEmpty()) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
return ReceiverFacts.ofAll(sites);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String instanceFieldName(Expression peeled) {
|
||||||
|
if (peeled instanceof FieldAccess fa
|
||||||
|
&& (fa.getExpression() instanceof ThisExpression || fa.getExpression() == null)) {
|
||||||
|
return fa.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (peeled instanceof SimpleName sn) {
|
||||||
|
return sn.getIdentifier();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean ownerDeclaresField(TypeDeclaration owner, String fieldName) {
|
||||||
|
if (owner == null || fieldName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (FieldDeclaration fd : owner.getFields()) {
|
||||||
|
for (Object fragObj : fd.fragments()) {
|
||||||
|
if (fragObj instanceof VariableDeclarationFragment frag
|
||||||
|
&& fieldName.equals(frag.getName().getIdentifier())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hopContainsType(String methodFqn, TypeDeclaration owner) {
|
||||||
|
if (methodFqn == null || owner == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String typeFqn = context.getFqn(owner);
|
||||||
|
if (typeFqn != null && methodFqn.startsWith(typeFqn + ".")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String simple = owner.getName().getIdentifier();
|
||||||
|
return methodFqn.contains("." + simple + ".") || methodFqn.startsWith(simple + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Expression> collectFieldWrites(MethodDeclaration method, String fieldName) {
|
||||||
|
List<Expression> writes = new java.util.ArrayList<>();
|
||||||
|
method.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(Assignment node) {
|
||||||
|
Expression lhs = node.getLeftHandSide();
|
||||||
|
if (lhs instanceof SimpleName sn && fieldName.equals(sn.getIdentifier())) {
|
||||||
|
writes.add(node.getRightHandSide());
|
||||||
|
} else if (lhs instanceof FieldAccess fa
|
||||||
|
&& fieldName.equals(fa.getName().getIdentifier())
|
||||||
|
&& (fa.getExpression() instanceof ThisExpression || fa.getExpression() == null)) {
|
||||||
|
writes.add(node.getRightHandSide());
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
String name = node.getName().getIdentifier();
|
||||||
|
if (name != null && name.startsWith("set") && name.length() > 3
|
||||||
|
&& javaBeansFieldName(name).equals(fieldName)
|
||||||
|
&& node.arguments().size() == 1
|
||||||
|
&& (node.getExpression() == null || node.getExpression() instanceof ThisExpression)) {
|
||||||
|
writes.add((Expression) node.arguments().get(0));
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return writes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Expression> collectSetterArgs(
|
||||||
|
MethodDeclaration method, String setterName, String receiverText) {
|
||||||
|
List<Expression> args = new java.util.ArrayList<>();
|
||||||
|
method.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (!setterName.equals(node.getName().getIdentifier()) || node.arguments().size() != 1) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
Expression recv = node.getExpression();
|
||||||
|
if (recv == null) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
if (receiverText != null && receiverText.equals(CallSiteArgBinder.normalizeExprText(recv))) {
|
||||||
|
args.add((Expression) node.arguments().get(0));
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
private Expression resolveGetterToCtorArg(AllocationSite site, String getterName) {
|
private Expression resolveGetterToCtorArg(AllocationSite site, String getterName) {
|
||||||
ClassInstanceCreation cic = site.cic();
|
ClassInstanceCreation cic = site.cic();
|
||||||
if (cic == null || getterName == null) {
|
if (cic == null || getterName == null) {
|
||||||
@@ -435,7 +720,7 @@ public final class AllocationFactPropagator {
|
|||||||
if (edge == null || edge.getTargetMethod() == null) {
|
if (edge == null || edge.getTargetMethod() == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!methodsMatch(edge.getTargetMethod(), calleeMethodFqn)) {
|
if (!CallSiteArgBinder.methodsMatch(edge.getTargetMethod(), calleeMethodFqn)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
List<String> args = edge.getArguments();
|
List<String> args = edge.getArguments();
|
||||||
@@ -443,8 +728,14 @@ public final class AllocationFactPropagator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Prefer AST so nested CIC args stay attached for getter peels.
|
// Prefer AST so nested CIC args stay attached for getter peels.
|
||||||
ReceiverFacts nested = factsFromCallerAstArgument(
|
MethodDeclaration callerMd = findMethodDeclaration(callerMethodFqn);
|
||||||
callerMethodFqn, edge.getTargetMethod(), paramIndex, methodPath, callGraph, depth, visited);
|
ReceiverFacts nested = ReceiverFacts.empty();
|
||||||
|
if (callerMd != null) {
|
||||||
|
var bound = CallSiteArgBinder.findUniqueArgument(callerMd, edge, paramIndex);
|
||||||
|
if (bound.isPresent()) {
|
||||||
|
nested = factsAtRaw(bound.get(), callerMd, methodPath, callGraph, depth + 1, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!nested.isEmpty()) {
|
if (!nested.isEmpty()) {
|
||||||
sites.addAll(nested.sites());
|
sites.addAll(nested.sites());
|
||||||
continue;
|
continue;
|
||||||
@@ -457,38 +748,6 @@ public final class AllocationFactPropagator {
|
|||||||
return ReceiverFacts.ofAll(sites);
|
return ReceiverFacts.ofAll(sites);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReceiverFacts factsFromCallerAstArgument(
|
|
||||||
String callerMethodFqn,
|
|
||||||
String targetMethodFqn,
|
|
||||||
int paramIndex,
|
|
||||||
List<String> methodPath,
|
|
||||||
Map<String, List<CallEdge>> callGraph,
|
|
||||||
int depth,
|
|
||||||
Set<String> visited) {
|
|
||||||
MethodDeclaration callerMd = findMethodDeclaration(callerMethodFqn);
|
|
||||||
if (callerMd == null || callerMd.getBody() == null) {
|
|
||||||
return ReceiverFacts.empty();
|
|
||||||
}
|
|
||||||
String targetSimple = methodNameOf(targetMethodFqn);
|
|
||||||
LinkedHashSet<AllocationSite> sites = new LinkedHashSet<>();
|
|
||||||
callerMd.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
if (!targetSimple.equals(node.getName().getIdentifier())) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
if (paramIndex >= node.arguments().size()) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
Expression arg = (Expression) node.arguments().get(paramIndex);
|
|
||||||
ReceiverFacts nested = factsAt(arg, callerMd, methodPath, callGraph, depth + 1, visited);
|
|
||||||
sites.addAll(nested.sites());
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return ReceiverFacts.ofAll(sites);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AllocationSite siteFromCic(ClassInstanceCreation cic) {
|
private AllocationSite siteFromCic(ClassInstanceCreation cic) {
|
||||||
if (cic == null) {
|
if (cic == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -649,29 +908,18 @@ public final class AllocationFactPropagator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String methodNameOf(String methodFqn) {
|
private static String methodNameOf(String methodFqn) {
|
||||||
if (methodFqn == null) {
|
return CallSiteArgBinder.methodNameOf(methodFqn);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int lastDot = methodFqn.lastIndexOf('.');
|
|
||||||
return lastDot >= 0 ? methodFqn.substring(lastDot + 1) : methodFqn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean methodsMatch(String left, String right) {
|
private static boolean methodsMatch(String left, String right) {
|
||||||
if (left == null || right == null) {
|
return CallSiteArgBinder.methodsMatch(left, right);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (left.equals(right)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return methodNameOf(left).equals(methodNameOf(right))
|
|
||||||
&& (left.endsWith("." + methodNameOf(right)) || right.endsWith("." + methodNameOf(left)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int indexOfMethodOnPath(List<String> methodPath, String methodFqn) {
|
private static int indexOfMethodOnPath(List<String> methodPath, String methodFqn) {
|
||||||
if (methodPath == null || methodFqn == null) {
|
if (methodPath == null || methodFqn == null) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
String simple = methodNameOf(methodFqn);
|
String simple = CallSiteArgBinder.methodNameOf(methodFqn);
|
||||||
for (int i = methodPath.size() - 1; i >= 0; i--) {
|
for (int i = methodPath.size() - 1; i >= 0; i--) {
|
||||||
String hop = methodPath.get(i);
|
String hop = methodPath.get(i);
|
||||||
if (hop != null && (hop.equals(methodFqn) || hop.endsWith("." + simple))) {
|
if (hop != null && (hop.equals(methodFqn) || hop.endsWith("." + simple))) {
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.flow.alloc;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTNode;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
|
import org.eclipse.jdt.core.dom.CastExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.IfStatement;
|
||||||
|
import org.eclipse.jdt.core.dom.InstanceofExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.Modifier;
|
||||||
|
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.PatternInstanceofExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.SimpleName;
|
||||||
|
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.Statement;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.TypePattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dominating {@code instanceof} / pattern narrowing → unique concrete {@link AllocationSite}.
|
||||||
|
* Shared by {@link AllocationFactPropagator}; fail-closed for else-branch, abstract targets,
|
||||||
|
* and sends outside the narrow.
|
||||||
|
*/
|
||||||
|
public final class InstanceofNarrower {
|
||||||
|
|
||||||
|
private InstanceofNarrower() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facts when {@code useSite} is dominated by {@code recv instanceof T} (then-branch)
|
||||||
|
* or {@code recv} is pattern variable {@code T p}.
|
||||||
|
*/
|
||||||
|
public static ReceiverFacts factsAt(
|
||||||
|
Expression receiver,
|
||||||
|
ASTNode useSite,
|
||||||
|
CodebaseContext context) {
|
||||||
|
if (context == null || receiver == null || useSite == null) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
Expression peeledRecv = peel(receiver);
|
||||||
|
if (!(peeledRecv instanceof SimpleName recvSn)) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
String recvName = recvSn.getIdentifier();
|
||||||
|
MethodDeclaration enclosing = findEnclosingMethod(useSite);
|
||||||
|
String patternType = resolvePatternVariableType(recvName, enclosing);
|
||||||
|
if (patternType != null) {
|
||||||
|
return factsFromConcreteTypeName(patternType, context);
|
||||||
|
}
|
||||||
|
ASTNode current = useSite;
|
||||||
|
while (current != null && !(current instanceof MethodDeclaration)) {
|
||||||
|
ASTNode parent = current.getParent();
|
||||||
|
if (parent instanceof IfStatement ifStmt) {
|
||||||
|
if (isNodeUnder(ifStmt.getThenStatement(), current)) {
|
||||||
|
ReceiverFacts fromCond = extractInstanceofFacts(
|
||||||
|
ifStmt.getExpression(), recvName, context);
|
||||||
|
if (!fromCond.isEmpty()) {
|
||||||
|
return fromCond;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge base AFP facts with dominating instanceof.
|
||||||
|
* <ul>
|
||||||
|
* <li>instanceof empty → base unchanged</li>
|
||||||
|
* <li>base empty → instanceof</li>
|
||||||
|
* <li>base unique is Object / missing TD / interface / abstract → refine to instanceof</li>
|
||||||
|
* <li>base unique concrete equals instanceof → that site</li>
|
||||||
|
* <li>base unique concrete conflicts with instanceof → empty (fail-closed)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public static ReceiverFacts refine(
|
||||||
|
ReceiverFacts base,
|
||||||
|
Expression receiver,
|
||||||
|
ASTNode useSite,
|
||||||
|
CodebaseContext context) {
|
||||||
|
ReceiverFacts narrowed = factsAt(receiver, useSite, context);
|
||||||
|
if (narrowed.isEmpty()) {
|
||||||
|
return base == null ? ReceiverFacts.empty() : base;
|
||||||
|
}
|
||||||
|
if (base == null || base.isEmpty()) {
|
||||||
|
return narrowed;
|
||||||
|
}
|
||||||
|
AllocationSite baseUnique = base.uniqueOrNull();
|
||||||
|
AllocationSite narrowUnique = narrowed.uniqueOrNull();
|
||||||
|
if (baseUnique == null || narrowUnique == null) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
if (isWeakAllocSite(baseUnique, context)) {
|
||||||
|
return narrowed;
|
||||||
|
}
|
||||||
|
if (sameType(baseUnique.typeFqn(), narrowUnique.typeFqn())) {
|
||||||
|
return ReceiverFacts.of(baseUnique.preferRich(narrowUnique));
|
||||||
|
}
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String resolvePatternVariableType(String varName, MethodDeclaration enclosing) {
|
||||||
|
if (varName == null || enclosing == null || enclosing.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] found = {null};
|
||||||
|
enclosing.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(PatternInstanceofExpression node) {
|
||||||
|
SingleVariableDeclaration svd = patternVariableOf(node);
|
||||||
|
if (svd != null
|
||||||
|
&& varName.equals(svd.getName().getIdentifier())
|
||||||
|
&& svd.getType() != null) {
|
||||||
|
found[0] = svd.getType().toString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (found[0] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String type = found[0];
|
||||||
|
int generic = type.indexOf('<');
|
||||||
|
return generic >= 0 ? type.substring(0, generic) : type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isWeakAllocSite(AllocationSite site, CodebaseContext context) {
|
||||||
|
if (site == null || site.typeFqn() == null || site.typeFqn().isBlank()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String fqn = site.typeFqn();
|
||||||
|
if ("java.lang.Object".equals(fqn) || "Object".equals(fqn)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
TypeDeclaration td = resolveTypeDeclaration(fqn, context);
|
||||||
|
if (td == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return td.isInterface() || Modifier.isAbstract(td.getModifiers());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameType(String a, String b) {
|
||||||
|
if (a == null || b == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (a.equals(b)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String simpleA = a.contains(".") ? a.substring(a.lastIndexOf('.') + 1) : a;
|
||||||
|
String simpleB = b.contains(".") ? b.substring(b.lastIndexOf('.') + 1) : b;
|
||||||
|
return simpleA.equals(simpleB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReceiverFacts extractInstanceofFacts(
|
||||||
|
Expression condition,
|
||||||
|
String recvName,
|
||||||
|
CodebaseContext context) {
|
||||||
|
Expression cond = peel(condition);
|
||||||
|
if (cond instanceof InstanceofExpression io) {
|
||||||
|
Expression left = peel(io.getLeftOperand());
|
||||||
|
if (left instanceof SimpleName sn && recvName.equals(sn.getIdentifier())
|
||||||
|
&& io.getRightOperand() != null) {
|
||||||
|
return factsFromConcreteTypeName(io.getRightOperand().toString(), context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cond instanceof PatternInstanceofExpression pio) {
|
||||||
|
Expression left = peel(pio.getLeftOperand());
|
||||||
|
if (left instanceof SimpleName sn && recvName.equals(sn.getIdentifier())) {
|
||||||
|
SingleVariableDeclaration svd = patternVariableOf(pio);
|
||||||
|
if (svd != null && svd.getType() != null) {
|
||||||
|
return factsFromConcreteTypeName(svd.getType().toString(), context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReceiverFacts factsFromConcreteTypeName(String typeFqn, CodebaseContext context) {
|
||||||
|
if (typeFqn == null || typeFqn.isBlank()) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
int generic = typeFqn.indexOf('<');
|
||||||
|
if (generic >= 0) {
|
||||||
|
typeFqn = typeFqn.substring(0, generic);
|
||||||
|
}
|
||||||
|
TypeDeclaration td = resolveTypeDeclaration(typeFqn, context);
|
||||||
|
if (td == null || td.isInterface() || Modifier.isAbstract(td.getModifiers())) {
|
||||||
|
return ReceiverFacts.empty();
|
||||||
|
}
|
||||||
|
String fqn = context.getFqn(td);
|
||||||
|
return ReceiverFacts.of(new AllocationSite(fqn != null ? fqn : typeFqn));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypeDeclaration resolveTypeDeclaration(String typeFqn, CodebaseContext context) {
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration(typeFqn);
|
||||||
|
if (td == null) {
|
||||||
|
String simple = typeFqn.contains(".") ? typeFqn.substring(typeFqn.lastIndexOf('.') + 1) : typeFqn;
|
||||||
|
td = context.getTypeDeclaration(simple);
|
||||||
|
}
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SingleVariableDeclaration patternVariableOf(PatternInstanceofExpression node) {
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (node.getPattern() instanceof TypePattern tp) {
|
||||||
|
SingleVariableDeclaration pv = tp.getPatternVariable();
|
||||||
|
if (pv != null) {
|
||||||
|
return pv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// API level may expose rightOperand instead of pattern
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return node.getRightOperand();
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNodeUnder(Statement root, ASTNode node) {
|
||||||
|
if (root == null || node == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current == root) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current instanceof MethodDeclaration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||||
|
ASTNode current = node;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof MethodDeclaration md) {
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression peel(Expression expression) {
|
||||||
|
Expression current = expression;
|
||||||
|
while (current instanceof ParenthesizedExpression pe) {
|
||||||
|
current = pe.getExpression();
|
||||||
|
}
|
||||||
|
if (current instanceof CastExpression ce) {
|
||||||
|
return peel(ce.getExpression());
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,7 +146,7 @@ public final class VirtualAccessorConstResolver {
|
|||||||
/**
|
/**
|
||||||
* Drop symbolic / enum-set / blank; keep FQN or bare constant identifiers.
|
* Drop symbolic / enum-set / blank; keep FQN or bare constant identifiers.
|
||||||
*/
|
*/
|
||||||
static String normalizeConstant(String value) {
|
public static String normalizeConstant(String value) {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.flow.eventid;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.flow.alloc.ReceiverFacts;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proven virtual accessor on a receiver with allocation (or concrete-type) facts.
|
||||||
|
*/
|
||||||
|
public final class AccessProof {
|
||||||
|
|
||||||
|
private final ReceiverFacts receiverFacts;
|
||||||
|
private final String accessorName;
|
||||||
|
private final MethodInvocation accessorCall;
|
||||||
|
private final MethodDeclaration enclosingMethod;
|
||||||
|
|
||||||
|
public AccessProof(
|
||||||
|
ReceiverFacts receiverFacts,
|
||||||
|
String accessorName,
|
||||||
|
MethodInvocation accessorCall,
|
||||||
|
MethodDeclaration enclosingMethod) {
|
||||||
|
this.receiverFacts = receiverFacts == null ? ReceiverFacts.empty() : receiverFacts;
|
||||||
|
this.accessorName = accessorName;
|
||||||
|
this.accessorCall = accessorCall;
|
||||||
|
this.enclosingMethod = enclosingMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReceiverFacts receiverFacts() {
|
||||||
|
return receiverFacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String accessorName() {
|
||||||
|
return accessorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodInvocation accessorCall() {
|
||||||
|
return accessorCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodDeclaration enclosingMethod() {
|
||||||
|
return enclosingMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return accessorName != null
|
||||||
|
&& !accessorName.isBlank()
|
||||||
|
&& !receiverFacts.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import java.util.Set;
|
|||||||
public final class LibraryUnwrapRegistry {
|
public final class LibraryUnwrapRegistry {
|
||||||
|
|
||||||
private static final Set<String> UNWRAP_RECEIVER_TYPES = Set.of(
|
private static final Set<String> UNWRAP_RECEIVER_TYPES = Set.of(
|
||||||
"Optional", "Stream", "List", "Set", "Arrays", "Objects", "Mono", "Flux");
|
"Optional", "Stream", "List", "Set", "Arrays", "Objects", "Mono", "Flux", "MessageBuilder");
|
||||||
|
|
||||||
private static final Set<String> UNWRAP_METHOD_NAMES = Set.of(
|
private static final Set<String> UNWRAP_METHOD_NAMES = Set.of(
|
||||||
"of", "ofEntries", "asList", "entry", "just", "withPayload", "success");
|
"of", "ofEntries", "asList", "entry", "just", "withPayload", "success");
|
||||||
@@ -18,6 +18,9 @@ public final class LibraryUnwrapRegistry {
|
|||||||
|
|
||||||
private static final Set<String> REACTIVE_TRANSFORM_METHODS = Set.of("map", "flatMap", "switchMap", "concatMap");
|
private static final Set<String> REACTIVE_TRANSFORM_METHODS = Set.of("map", "flatMap", "switchMap", "concatMap");
|
||||||
|
|
||||||
|
/** Fixed header keys that may carry a machine event (free-form / dynamic keys stay fail-closed). */
|
||||||
|
private static final Set<String> EVENT_HEADER_KEYS = Set.of("event", "smEvent");
|
||||||
|
|
||||||
private static final String[] STOP_UNWRAP_PREFIXES = {
|
private static final String[] STOP_UNWRAP_PREFIXES = {
|
||||||
"map", "parse", "convert", "to", "resolve", "build", "create", "from",
|
"map", "parse", "convert", "to", "resolve", "build", "create", "from",
|
||||||
"transform", "translate", "derive", "determine", "calculate", "decode",
|
"transform", "translate", "derive", "determine", "calculate", "decode",
|
||||||
@@ -68,4 +71,20 @@ public final class LibraryUnwrapRegistry {
|
|||||||
public static boolean isHeaderSetterMethodName(String methodName) {
|
public static boolean isHeaderSetterMethodName(String methodName) {
|
||||||
return "setHeader".equals(methodName);
|
return "setHeader".equals(methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isEventHeaderKey(String headerKey) {
|
||||||
|
return headerKey != null && EVENT_HEADER_KEYS.contains(headerKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGenericMessageType(String typeName) {
|
||||||
|
if (typeName == null || typeName.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String simple = typeName.contains(".") ? typeName.substring(typeName.lastIndexOf('.') + 1) : typeName;
|
||||||
|
int generic = simple.indexOf('<');
|
||||||
|
if (generic >= 0) {
|
||||||
|
simple = simple.substring(0, generic);
|
||||||
|
}
|
||||||
|
return "GenericMessage".equals(simple);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.core.dom.CastExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.StringLiteral;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared MessageBuilder / GenericMessage / allowlisted-header peels for event identity.
|
||||||
|
* Used by {@code EventIdentityResolver} and call-graph unwrap paths.
|
||||||
|
*/
|
||||||
|
public final class MessageCarrierSupport {
|
||||||
|
|
||||||
|
private MessageCarrierSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer allowlisted {@code setHeader("event"|"smEvent", …)} on a MessageBuilder chain;
|
||||||
|
* else peel {@code withPayload}/{@code just}/{@code success}; else GenericMessage ctor arg.
|
||||||
|
*
|
||||||
|
* @return the event-bearing expression, or null when nothing is proven
|
||||||
|
*/
|
||||||
|
public static Expression peelEventFromMessageExpr(Expression expression) {
|
||||||
|
if (expression == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Expression peeled = peel(expression);
|
||||||
|
Expression headerEvent = findSetHeaderValueOnChain(peeled, null);
|
||||||
|
if (headerEvent != null) {
|
||||||
|
return peel(headerEvent);
|
||||||
|
}
|
||||||
|
Expression payload = ReactiveExpressionSupport.peelFactoryPayloadExpression(peeled);
|
||||||
|
if (payload != null) {
|
||||||
|
return peel(payload);
|
||||||
|
}
|
||||||
|
return peelGenericMessageCtor(peeled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression peelGenericMessageCtor(Expression expression) {
|
||||||
|
Expression peeled = peel(expression);
|
||||||
|
if (peeled instanceof ClassInstanceCreation cic
|
||||||
|
&& LibraryUnwrapRegistry.isGenericMessageType(cic.getType().toString())
|
||||||
|
&& !cic.arguments().isEmpty()
|
||||||
|
&& cic.arguments().get(0) instanceof Expression arg) {
|
||||||
|
return peel(arg);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk a MessageBuilder chain for {@code setHeader(key, value)}. When {@code requiredKey}
|
||||||
|
* is null, returns the first allowlisted event-header value on the chain.
|
||||||
|
*/
|
||||||
|
public static Expression findSetHeaderValueOnChain(Expression expression, String requiredKey) {
|
||||||
|
Expression current = peel(expression);
|
||||||
|
while (current instanceof MethodInvocation mi) {
|
||||||
|
if (LibraryUnwrapRegistry.isHeaderSetterMethodName(mi.getName().getIdentifier())
|
||||||
|
&& mi.arguments().size() >= 2) {
|
||||||
|
String key = stringLiteralValue(mi.arguments().get(0));
|
||||||
|
if (requiredKey != null) {
|
||||||
|
if (requiredKey.equals(key) && mi.arguments().get(1) instanceof Expression value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} else if (LibraryUnwrapRegistry.isEventHeaderKey(key)
|
||||||
|
&& mi.arguments().get(1) instanceof Expression value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("build".equals(mi.getName().getIdentifier()) && mi.arguments().isEmpty()
|
||||||
|
&& mi.getExpression() != null) {
|
||||||
|
current = peel(mi.getExpression());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mi.getExpression() instanceof MethodInvocation next) {
|
||||||
|
current = next;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stringLiteralValue(Object arg) {
|
||||||
|
if (arg instanceof StringLiteral sl) {
|
||||||
|
return sl.getLiteralValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression peel(Expression expression) {
|
||||||
|
Expression current = expression;
|
||||||
|
while (current instanceof ParenthesizedExpression pe) {
|
||||||
|
current = pe.getExpression();
|
||||||
|
}
|
||||||
|
if (current instanceof CastExpression ce) {
|
||||||
|
return peel(ce.getExpression());
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
|||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.flow.alloc.AllocationFactPropagator;
|
import click.kamil.springstatemachineexporter.analysis.flow.alloc.AllocationFactPropagator;
|
||||||
import click.kamil.springstatemachineexporter.analysis.flow.alloc.ReceiverFacts;
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.flow.alloc.VirtualAccessorConstResolver;
|
import click.kamil.springstatemachineexporter.analysis.flow.alloc.VirtualAccessorConstResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.flow.eventid.EventIdentityResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
|
import click.kamil.springstatemachineexporter.analysis.pipeline.FieldInitializerFinder;
|
||||||
@@ -39,6 +39,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
protected final PathBindingEvaluator pathBindingEvaluator;
|
protected final PathBindingEvaluator pathBindingEvaluator;
|
||||||
protected final AllocationFactPropagator allocationFactPropagator;
|
protected final AllocationFactPropagator allocationFactPropagator;
|
||||||
protected final VirtualAccessorConstResolver virtualAccessorConstResolver;
|
protected final VirtualAccessorConstResolver virtualAccessorConstResolver;
|
||||||
|
protected final EventIdentityResolver eventIdentityResolver;
|
||||||
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
private final Map<String, ASTNode> parsedNodeCache = new HashMap<>();
|
||||||
private final Map<String, List<String>> polymorphicCallCache = new HashMap<>();
|
private final Map<String, List<String>> polymorphicCallCache = new HashMap<>();
|
||||||
private final Map<String, Map<String, String>> pathBindingsCache = new HashMap<>();
|
private final Map<String, Map<String, String>> pathBindingsCache = new HashMap<>();
|
||||||
@@ -84,6 +85,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
context, variableTracer, constructorAnalyzer);
|
context, variableTracer, constructorAnalyzer);
|
||||||
this.virtualAccessorConstResolver = new VirtualAccessorConstResolver(
|
this.virtualAccessorConstResolver = new VirtualAccessorConstResolver(
|
||||||
context, accessorResolver, constantResolver);
|
context, accessorResolver, constantResolver);
|
||||||
|
this.eventIdentityResolver = new EventIdentityResolver(
|
||||||
|
context,
|
||||||
|
allocationFactPropagator,
|
||||||
|
virtualAccessorConstResolver,
|
||||||
|
variableTracer,
|
||||||
|
constantResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||||
@@ -96,6 +103,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
pathBindingEvaluator.clearAnalysisCaches();
|
pathBindingEvaluator.clearAnalysisCaches();
|
||||||
pathBindingsCache.clear();
|
pathBindingsCache.clear();
|
||||||
parsedNodeCache.clear();
|
parsedNodeCache.clear();
|
||||||
|
if (eventIdentityResolver != null) {
|
||||||
|
eventIdentityResolver.clearSessionCache();
|
||||||
|
}
|
||||||
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
||||||
List<CallChain> chains = new ArrayList<>();
|
List<CallChain> chains = new ArrayList<>();
|
||||||
|
|
||||||
@@ -1332,8 +1342,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
|
return buildTriggerPointWithPolymorphicEvents(tp, resolvedValue, polymorphicEvents);
|
||||||
}
|
}
|
||||||
// valueOf(e.getType()) may have been collapsed to <SYMBOLIC:Enum.*> on the sendEvent
|
// valueOf(e.getType()) may have been collapsed to <SYMBOLIC:Enum.*> on the sendEvent
|
||||||
// edge — recover via allocation-sensitive getType on the dispatcher frame.
|
// edge — recover via event-identity resolution on the dispatcher frame.
|
||||||
List<String> recovered = recoverAllocationSensitiveAccessorFromPath(path, callGraph);
|
List<String> recovered = eventIdentityResolver != null
|
||||||
|
? eventIdentityResolver.recoverFromPath(path, callGraph != null ? callGraph : this.graph)
|
||||||
|
: List.of();
|
||||||
if (recovered.size() == 1) {
|
if (recovered.size() == 1) {
|
||||||
return buildTriggerPointWithPolymorphicEvents(
|
return buildTriggerPointWithPolymorphicEvents(
|
||||||
tp,
|
tp,
|
||||||
@@ -1688,9 +1700,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a virtual accessor ({@code getType}/{@code getEvent}/…) has a receiver proven to be a
|
* When a virtual accessor ({@code getType}/{@code getA}/…) has a receiver proven to be a
|
||||||
* concrete {@code new T()}, resolve only that override's compile-time constant instead of
|
* concrete {@code new T()} (or concrete declared type), resolve only that override's
|
||||||
* unioning all interface implementors.
|
* compile-time constant instead of unioning all interface implementors.
|
||||||
*
|
*
|
||||||
* @return true when a unique constant was proven and written into {@code polymorphicEvents}
|
* @return true when a unique constant was proven and written into {@code polymorphicEvents}
|
||||||
*/
|
*/
|
||||||
@@ -1700,20 +1712,20 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
List<String> path,
|
List<String> path,
|
||||||
Map<String, List<CallEdge>> callGraph,
|
Map<String, List<CallEdge>> callGraph,
|
||||||
List<String> polymorphicEvents) {
|
List<String> polymorphicEvents) {
|
||||||
if (!(exprNode instanceof Expression expression)
|
if (!(exprNode instanceof Expression expression) || eventIdentityResolver == null) {
|
||||||
|| allocationFactPropagator == null
|
|
||||||
|| virtualAccessorConstResolver == null) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MethodInvocation accessorCall = findVirtualAccessorInvocation(expression, methodName);
|
|
||||||
if (accessorCall == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String accessorName = accessorCall.getName().getIdentifier();
|
|
||||||
Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
|
Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
|
||||||
ReceiverFacts facts = allocationFactPropagator.factsForAccessorReceiver(
|
List<String> unique = eventIdentityResolver.resolveUnique(expression, path, graphForFacts);
|
||||||
accessorCall, path, graphForFacts);
|
if (unique.size() != 1) {
|
||||||
List<String> unique = virtualAccessorConstResolver.resolveUniqueConstant(facts, accessorName);
|
// Prefer peeling a nested accessor when methodName hints at get*
|
||||||
|
if (methodName != null && EventIdentityResolver.isVirtualAccessorName(methodName)) {
|
||||||
|
MethodInvocation accessorCall = findVirtualAccessorInvocation(expression, methodName);
|
||||||
|
if (accessorCall != null && accessorCall != expression) {
|
||||||
|
unique = eventIdentityResolver.resolveUnique(accessorCall, path, graphForFacts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (unique.size() != 1) {
|
if (unique.size() != 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1733,7 +1745,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
if (peeled instanceof MethodInvocation mi) {
|
if (peeled instanceof MethodInvocation mi) {
|
||||||
String name = mi.getName().getIdentifier();
|
String name = mi.getName().getIdentifier();
|
||||||
if (preferredName != null && preferredName.equals(name) && isVirtualAccessorName(name)) {
|
if (preferredName != null && preferredName.equals(name)
|
||||||
|
&& EventIdentityResolver.isVirtualAccessorName(name)) {
|
||||||
return mi;
|
return mi;
|
||||||
}
|
}
|
||||||
if ("valueOf".equals(name) && mi.arguments().size() == 1
|
if ("valueOf".equals(name) && mi.arguments().size() == 1
|
||||||
@@ -1743,10 +1756,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return nested;
|
return nested;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (preferredName == null && isVirtualAccessorName(name)) {
|
if (preferredName == null && EventIdentityResolver.isVirtualAccessorName(name)) {
|
||||||
return mi;
|
return mi;
|
||||||
}
|
}
|
||||||
if (isVirtualAccessorName(name)) {
|
if (EventIdentityResolver.isVirtualAccessorName(name)) {
|
||||||
return mi;
|
return mi;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1754,21 +1767,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isVirtualAccessorName(String name) {
|
private static boolean isVirtualAccessorName(String name) {
|
||||||
if (name == null || name.isBlank()) {
|
return EventIdentityResolver.isVirtualAccessorName(name);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return isEventIdentityAccessorName(name)
|
|
||||||
|| (name.startsWith("get") && name.length() > 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Accessors that identify the machine event (not arbitrary getters on the same frame).
|
|
||||||
*/
|
|
||||||
private static boolean isEventIdentityAccessorName(String name) {
|
private static boolean isEventIdentityAccessorName(String name) {
|
||||||
return "getType".equals(name)
|
return EventIdentityResolver.isEventIdentityAccessorName(name);
|
||||||
|| "getEvent".equals(name)
|
|
||||||
|| "getId".equals(name)
|
|
||||||
|| "resolveEventCode".equals(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1798,7 +1801,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
List<String> path,
|
List<String> path,
|
||||||
Map<String, List<CallEdge>> callGraph,
|
Map<String, List<CallEdge>> callGraph,
|
||||||
List<String> polymorphicEvents) {
|
List<String> polymorphicEvents) {
|
||||||
List<String> recovered = recoverAllocationSensitiveAccessorFromPath(path, callGraph);
|
if (eventIdentityResolver == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
|
||||||
|
List<String> recovered = eventIdentityResolver.recoverFromPath(path, graphForFacts);
|
||||||
if (recovered.size() != 1) {
|
if (recovered.size() != 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1807,91 +1814,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* When sendEvent's call-graph argument was collapsed to {@code <SYMBOLIC:Enum.*>}, walk the
|
|
||||||
* dispatcher frame for {@code valueOf(receiver.getType())} / event-identity accessors and
|
|
||||||
* apply allocation-sensitive constant resolution using the call path.
|
|
||||||
*/
|
|
||||||
private List<String> recoverAllocationSensitiveAccessorFromPath(
|
|
||||||
List<String> path,
|
|
||||||
Map<String, List<CallEdge>> callGraph) {
|
|
||||||
if (path == null || path.size() < 2
|
|
||||||
|| allocationFactPropagator == null
|
|
||||||
|| virtualAccessorConstResolver == null) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
// Prefer the frame that calls sendEvent (usually path.size()-2).
|
|
||||||
for (int i = path.size() - 2; i >= 0; i--) {
|
|
||||||
String methodFqn = path.get(i);
|
|
||||||
if (methodFqn == null || !methodFqn.contains(".")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
|
||||||
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
|
||||||
if (td == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
|
||||||
if (md == null || md.getBody() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<MethodInvocation> preferred = new ArrayList<>();
|
|
||||||
List<MethodInvocation> fallback = new ArrayList<>();
|
|
||||||
md.getBody().accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(MethodInvocation node) {
|
|
||||||
String name = node.getName().getIdentifier();
|
|
||||||
// Highest priority: nested in Enum.valueOf(...)
|
|
||||||
if ("valueOf".equals(name) && node.arguments().size() == 1
|
|
||||||
&& node.arguments().get(0) instanceof MethodInvocation nested
|
|
||||||
&& isVirtualAccessorName(nested.getName().getIdentifier())) {
|
|
||||||
preferred.add(nested);
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
if (node.getExpression() == null || !isVirtualAccessorName(name)) {
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
if (isEventIdentityAccessorName(name)) {
|
|
||||||
preferred.add(node);
|
|
||||||
} else {
|
|
||||||
fallback.add(node);
|
|
||||||
}
|
|
||||||
return super.visit(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
|
|
||||||
List<String> fromPreferred = resolveUniqueFromAccessors(preferred, path, graphForFacts);
|
|
||||||
if (fromPreferred.size() == 1) {
|
|
||||||
return fromPreferred;
|
|
||||||
}
|
|
||||||
List<String> fromFallback = resolveUniqueFromAccessors(fallback, path, graphForFacts);
|
|
||||||
if (fromFallback.size() == 1) {
|
|
||||||
return fromFallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveUniqueFromAccessors(
|
|
||||||
List<MethodInvocation> accessors,
|
|
||||||
List<String> path,
|
|
||||||
Map<String, List<CallEdge>> graphForFacts) {
|
|
||||||
for (MethodInvocation accessor : accessors) {
|
|
||||||
if (accessor == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ReceiverFacts facts = allocationFactPropagator.factsForAccessorReceiver(
|
|
||||||
accessor, path, graphForFacts);
|
|
||||||
List<String> unique = virtualAccessorConstResolver.resolveUniqueConstant(
|
|
||||||
facts, accessor.getName().getIdentifier());
|
|
||||||
if (unique.size() == 1) {
|
|
||||||
return unique;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String tryNarrowGetterViaLocalVarChain(MethodInvocation getterCall, SimpleName instanceSn) {
|
private String tryNarrowGetterViaLocalVarChain(MethodInvocation getterCall, SimpleName instanceSn) {
|
||||||
MethodDeclaration enclosing = findEnclosingMethod(getterCall);
|
MethodDeclaration enclosing = findEnclosingMethod(getterCall);
|
||||||
if (enclosing == null || enclosing.getBody() == null) {
|
if (enclosing == null || enclosing.getBody() == null) {
|
||||||
@@ -2296,6 +2218,13 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
resolvedValue, methodSuffix);
|
resolvedValue, methodSuffix);
|
||||||
List<String> searchMethods = buildGetterSearchMethods(resolvedValue, entryMethod, path);
|
List<String> searchMethods = buildGetterSearchMethods(resolvedValue, entryMethod, path);
|
||||||
|
|
||||||
|
// Prefer allocation-/identity-sensitive proof before CHA accessor union.
|
||||||
|
List<String> viaEir = resolveGetterConstantsViaEventIdentity(
|
||||||
|
expressionText, searchMethods, path, callGraph);
|
||||||
|
if (viaEir.size() == 1) {
|
||||||
|
return viaEir;
|
||||||
|
}
|
||||||
|
|
||||||
List<String> constants = resolveGetterConstantsViaDataflow(expressionText, searchMethods, path, callGraph, entryMethod);
|
List<String> constants = resolveGetterConstantsViaDataflow(expressionText, searchMethods, path, callGraph, entryMethod);
|
||||||
if (!constants.isEmpty()) {
|
if (!constants.isEmpty()) {
|
||||||
return constants;
|
return constants;
|
||||||
@@ -2305,6 +2234,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
|
String remapped = ReactiveExpressionSupport.remapLambdaParameterGetterInMethod(
|
||||||
expressionText, scopeMethod, constantResolver, context);
|
expressionText, scopeMethod, constantResolver, context);
|
||||||
if (remapped != null && !remapped.equals(expressionText)) {
|
if (remapped != null && !remapped.equals(expressionText)) {
|
||||||
|
viaEir = resolveGetterConstantsViaEventIdentity(remapped, searchMethods, path, callGraph);
|
||||||
|
if (viaEir.size() == 1) {
|
||||||
|
return viaEir;
|
||||||
|
}
|
||||||
constants = resolveGetterConstantsViaDataflow(remapped, searchMethods, path, callGraph, entryMethod);
|
constants = resolveGetterConstantsViaDataflow(remapped, searchMethods, path, callGraph, entryMethod);
|
||||||
if (!constants.isEmpty()) {
|
if (!constants.isEmpty()) {
|
||||||
return constants;
|
return constants;
|
||||||
@@ -2314,6 +2247,23 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return resolveGetterConstantsViaAccessorPipeline(resolvedValue, methodSuffix, entryMethod, path);
|
return resolveGetterConstantsViaAccessorPipeline(resolvedValue, methodSuffix, entryMethod, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> resolveGetterConstantsViaEventIdentity(
|
||||||
|
String expressionText,
|
||||||
|
List<String> searchMethods,
|
||||||
|
List<String> path,
|
||||||
|
Map<String, List<CallEdge>> callGraph) {
|
||||||
|
if (eventIdentityResolver == null || expressionText == null || expressionText.isBlank()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Expression anchored = click.kamil.springstatemachineexporter.ast.common.AstUtils.findExpressionInMethods(
|
||||||
|
searchMethods, expressionText, context);
|
||||||
|
if (anchored == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Map<String, List<CallEdge>> graphForFacts = callGraph != null ? callGraph : this.graph;
|
||||||
|
return eventIdentityResolver.resolveUnique(anchored, path, graphForFacts);
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> resolveGetterConstantsViaDataflow(
|
private List<String> resolveGetterConstantsViaDataflow(
|
||||||
String expressionText,
|
String expressionText,
|
||||||
List<String> searchMethods,
|
List<String> searchMethods,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.pipeline.MessageCarrierSupport;
|
||||||
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
|
import click.kamil.springstatemachineexporter.analysis.pipeline.ReactiveExpressionSupport;
|
||||||
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
@@ -13,6 +14,9 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
|
public JdtCallGraphEngine(CodebaseContext context, InjectionPointAnalyzer injectionAnalyzer) {
|
||||||
super(context);
|
super(context);
|
||||||
this.injectionAnalyzer = injectionAnalyzer;
|
this.injectionAnalyzer = injectionAnalyzer;
|
||||||
|
if (allocationFactPropagator != null) {
|
||||||
|
allocationFactPropagator.setInjectionAnalyzer(injectionAnalyzer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -175,7 +179,11 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Expression unwrapMessageBuilder(Expression expr) {
|
private Expression unwrapMessageBuilder(Expression expr) {
|
||||||
Expression peeled = ReactiveExpressionSupport.peelFactoryPayloadExpression(expr);
|
Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(expr);
|
||||||
return peeled != null ? peeled : expr;
|
if (peeled != null) {
|
||||||
|
return peeled;
|
||||||
|
}
|
||||||
|
Expression factory = ReactiveExpressionSupport.peelFactoryPayloadExpression(expr);
|
||||||
|
return factory != null ? factory : expr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.flow;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class CallSiteArgBinderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void uniqueSiteWithoutEdgeText(@TempDir Path tempDir) throws Exception {
|
||||||
|
MethodDeclaration caller = scanCaller(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(String a) { route(a); }
|
||||||
|
void route(String code) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CallEdge edge = new CallEdge("com.example.C.route", List.of("a"));
|
||||||
|
Optional<?> arg = CallSiteArgBinder.findUniqueArgument(caller, edge, 0);
|
||||||
|
assertThat(arg).isPresent();
|
||||||
|
assertThat(arg.get().toString()).isEqualTo("a");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiSiteUsesEdgeText(@TempDir Path tempDir) throws Exception {
|
||||||
|
MethodDeclaration caller = scanCaller(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(String a, String b) {
|
||||||
|
route(a);
|
||||||
|
route(b);
|
||||||
|
}
|
||||||
|
void route(String code) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CallEdge edge = new CallEdge("com.example.C.route", List.of("b"));
|
||||||
|
Optional<?> arg = CallSiteArgBinder.findUniqueArgument(caller, edge, 0);
|
||||||
|
assertThat(arg).isPresent();
|
||||||
|
assertThat(arg.get().toString()).isEqualTo("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiSiteIdenticalTextPicksFirst(@TempDir Path tempDir) throws Exception {
|
||||||
|
MethodDeclaration caller = scanCaller(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(String a) {
|
||||||
|
route(a);
|
||||||
|
route(a);
|
||||||
|
}
|
||||||
|
void route(String code) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CallEdge edge = new CallEdge("com.example.C.route", List.of("a"));
|
||||||
|
Optional<?> arg = CallSiteArgBinder.findUniqueArgument(caller, edge, 0);
|
||||||
|
assertThat(arg).isPresent();
|
||||||
|
assertThat(arg.get().toString()).isEqualTo("a");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiSiteWithoutEdgeTextFailsClosed(@TempDir Path tempDir) throws Exception {
|
||||||
|
MethodDeclaration caller = scanCaller(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(String a, String b) {
|
||||||
|
route(a);
|
||||||
|
route(b);
|
||||||
|
}
|
||||||
|
void route(String code) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CallEdge edge = new CallEdge("com.example.C.route", null);
|
||||||
|
assertThat(CallSiteArgBinder.findUniqueArgument(caller, edge, 0)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodDeclaration scanCaller(Path tempDir, String source) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("C.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.example.C");
|
||||||
|
if (td == null) {
|
||||||
|
td = context.getTypeDeclaration("C");
|
||||||
|
}
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
MethodDeclaration fire = context.findMethodDeclaration(td, "fire", true);
|
||||||
|
assertThat(fire).isNotNull();
|
||||||
|
return fire;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.flow.alloc;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class InstanceofNarrowerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void patternVariableProvesConcrete(@TempDir Path tempDir) throws Exception {
|
||||||
|
Fixture f = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(Object o) {
|
||||||
|
if (o instanceof PayRichEvent p) {
|
||||||
|
use(p.getA());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void use(String s) {}
|
||||||
|
}
|
||||||
|
class PayRichEvent { String getA() { return "PAY"; } }
|
||||||
|
class ShipRichEvent { String getA() { return "SHIP"; } }
|
||||||
|
""");
|
||||||
|
MethodInvocation getA = findInvocation(f.fire, "getA");
|
||||||
|
ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
|
||||||
|
assertThat(facts.uniqueOrNull()).isNotNull();
|
||||||
|
assertThat(facts.uniqueOrNull().typeFqn()).contains("PayRichEvent");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void thenBranchClassicInstanceofProvesConcrete(@TempDir Path tempDir) throws Exception {
|
||||||
|
Fixture f = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(Object o) {
|
||||||
|
if (o instanceof PayRichEvent) {
|
||||||
|
use(((PayRichEvent) o).getA());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void use(String s) {}
|
||||||
|
}
|
||||||
|
class PayRichEvent { String getA() { return "PAY"; } }
|
||||||
|
class ShipRichEvent { String getA() { return "SHIP"; } }
|
||||||
|
""");
|
||||||
|
MethodInvocation getA = findInvocation(f.fire, "getA");
|
||||||
|
ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
|
||||||
|
assertThat(facts.uniqueOrNull()).isNotNull();
|
||||||
|
assertThat(facts.uniqueOrNull().typeFqn()).contains("PayRichEvent");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void outsideThenStaysEmpty(@TempDir Path tempDir) throws Exception {
|
||||||
|
Fixture f = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(Object o) {
|
||||||
|
if (o instanceof PayRichEvent p) {}
|
||||||
|
use(((PayRichEvent) o).getA());
|
||||||
|
}
|
||||||
|
void use(String s) {}
|
||||||
|
}
|
||||||
|
class PayRichEvent { String getA() { return "PAY"; } }
|
||||||
|
""");
|
||||||
|
MethodInvocation getA = findInvocation(f.fire, "getA");
|
||||||
|
ReceiverFacts facts = InstanceofNarrower.factsAt(getA.getExpression(), getA, f.context);
|
||||||
|
assertThat(facts.isEmpty()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refineObjectCicWithInstanceof(@TempDir Path tempDir) throws Exception {
|
||||||
|
Fixture f = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(Object o) {
|
||||||
|
if (o instanceof PayRichEvent) {
|
||||||
|
use(((PayRichEvent) o).getA());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void use(String s) {}
|
||||||
|
}
|
||||||
|
class PayRichEvent { String getA() { return "PAY"; } }
|
||||||
|
""");
|
||||||
|
MethodInvocation getA = findInvocation(f.fire, "getA");
|
||||||
|
ReceiverFacts base = ReceiverFacts.of(new AllocationSite("java.lang.Object"));
|
||||||
|
ReceiverFacts refined = InstanceofNarrower.refine(base, getA.getExpression(), getA, f.context);
|
||||||
|
assertThat(refined.uniqueOrNull().typeFqn()).contains("PayRichEvent");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refineConcreteConflictFailsClosed(@TempDir Path tempDir) throws Exception {
|
||||||
|
Fixture f = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire(Object o) {
|
||||||
|
if (o instanceof PayRichEvent) {
|
||||||
|
use(((PayRichEvent) o).getA());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void use(String s) {}
|
||||||
|
}
|
||||||
|
class PayRichEvent { String getA() { return "PAY"; } }
|
||||||
|
class ShipRichEvent { String getA() { return "SHIP"; } }
|
||||||
|
""");
|
||||||
|
MethodInvocation getA = findInvocation(f.fire, "getA");
|
||||||
|
ReceiverFacts base = ReceiverFacts.of(new AllocationSite("com.example.ShipRichEvent"));
|
||||||
|
ReceiverFacts refined = InstanceofNarrower.refine(base, getA.getExpression(), getA, f.context);
|
||||||
|
assertThat(refined.isEmpty()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fixture scan(Path tempDir, String source) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("C.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.example.C");
|
||||||
|
if (td == null) {
|
||||||
|
td = context.getTypeDeclaration("C");
|
||||||
|
}
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
MethodDeclaration fire = context.findMethodDeclaration(td, "fire", true);
|
||||||
|
assertThat(fire).isNotNull();
|
||||||
|
return new Fixture(context, fire);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInvocation findInvocation(MethodDeclaration method, String name) {
|
||||||
|
MethodInvocation[] found = {null};
|
||||||
|
method.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (name.equals(node.getName().getIdentifier())) {
|
||||||
|
found[0] = node;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertThat(found[0]).as("expected invocation %s", name).isNotNull();
|
||||||
|
return found[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Fixture(CodebaseContext context, MethodDeclaration fire) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.pipeline;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class MessageCarrierSupportTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void peelsWithPayload(@TempDir Path tempDir) throws Exception {
|
||||||
|
Expression sendArg = sendArg(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire() {
|
||||||
|
send(MessageBuilder.withPayload(OrderEvent.PAY).build());
|
||||||
|
}
|
||||||
|
void send(Object e) {}
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
class MessageBuilder {
|
||||||
|
static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
|
||||||
|
Object build() { return null; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
|
||||||
|
assertThat(peeled).isNotNull();
|
||||||
|
assertThat(peeled.toString()).contains("PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void prefersAllowlistedHeaderOverPayload(@TempDir Path tempDir) throws Exception {
|
||||||
|
Expression sendArg = sendArg(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire() {
|
||||||
|
Object dto = new Object();
|
||||||
|
send(MessageBuilder.withPayload(dto).setHeader("event", OrderEvent.PAY).build());
|
||||||
|
}
|
||||||
|
void send(Object e) {}
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
class MessageBuilder {
|
||||||
|
static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
|
||||||
|
MessageBuilder setHeader(String k, Object v) { return this; }
|
||||||
|
Object build() { return null; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
|
||||||
|
assertThat(peeled).isNotNull();
|
||||||
|
assertThat(peeled.toString()).contains("PAY");
|
||||||
|
assertThat(MessageCarrierSupport.findSetHeaderValueOnChain(sendArg, "event").toString())
|
||||||
|
.contains("PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void freeFormHeaderKeyIgnored(@TempDir Path tempDir) throws Exception {
|
||||||
|
Expression sendArg = sendArg(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire() {
|
||||||
|
Object dto = new Object();
|
||||||
|
send(MessageBuilder.withPayload(dto).setHeader("customKey", OrderEvent.PAY).build());
|
||||||
|
}
|
||||||
|
void send(Object e) {}
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
class MessageBuilder {
|
||||||
|
static MessageBuilder withPayload(Object e) { return new MessageBuilder(); }
|
||||||
|
MessageBuilder setHeader(String k, Object v) { return this; }
|
||||||
|
Object build() { return null; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
assertThat(MessageCarrierSupport.findSetHeaderValueOnChain(sendArg, null)).isNull();
|
||||||
|
Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
|
||||||
|
// Falls through to dto payload — not the header const.
|
||||||
|
assertThat(peeled).isNotNull();
|
||||||
|
assertThat(peeled.toString()).doesNotContain("PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void peelsGenericMessageCtor(@TempDir Path tempDir) throws Exception {
|
||||||
|
Expression sendArg = sendArg(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
class C {
|
||||||
|
void fire() {
|
||||||
|
send(new GenericMessage<>(OrderEvent.PAY));
|
||||||
|
}
|
||||||
|
void send(Object e) {}
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY }
|
||||||
|
class GenericMessage<T> { GenericMessage(T payload) {} }
|
||||||
|
""");
|
||||||
|
Expression peeled = MessageCarrierSupport.peelEventFromMessageExpr(sendArg);
|
||||||
|
assertThat(peeled).isNotNull();
|
||||||
|
assertThat(peeled.toString()).contains("PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression sendArg(Path tempDir, String source) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("C.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.example.C");
|
||||||
|
if (td == null) {
|
||||||
|
td = context.getTypeDeclaration("C");
|
||||||
|
}
|
||||||
|
assertThat(td).isNotNull();
|
||||||
|
MethodDeclaration fire = context.findMethodDeclaration(td, "fire", true);
|
||||||
|
assertThat(fire).isNotNull();
|
||||||
|
MethodInvocation[] send = {null};
|
||||||
|
fire.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if ("send".equals(node.getName().getIdentifier()) && !node.arguments().isEmpty()) {
|
||||||
|
send[0] = node;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertThat(send[0]).isNotNull();
|
||||||
|
return (Expression) send[0].arguments().get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnalyzer;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Event;
|
||||||
|
import click.kamil.springstatemachineexporter.model.State;
|
||||||
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique {@code @Component} implementing an interface field acts as CIC substitute for EIR.
|
||||||
|
*/
|
||||||
|
class BeanAsAllocEventIdentityTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void uniqueComponentBeanAsAllocResolves(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), """
|
||||||
|
package com.example;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
public class ApiController {
|
||||||
|
Dispatcher dispatcher;
|
||||||
|
public void pay() { dispatcher.fire(); }
|
||||||
|
}
|
||||||
|
class Dispatcher {
|
||||||
|
StateMachine machine;
|
||||||
|
RichEvent carrier;
|
||||||
|
void fire() {
|
||||||
|
machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface RichEvent { String getA(); }
|
||||||
|
@Component
|
||||||
|
class PayRichEvent implements RichEvent {
|
||||||
|
public String getA() { return OrderEvent.PAY.name(); }
|
||||||
|
}
|
||||||
|
class ShipRichEvent implements RichEvent {
|
||||||
|
public String getA() { return OrderEvent.SHIP.name(); }
|
||||||
|
}
|
||||||
|
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||||
|
enum OrderEvent { PAY, SHIP, CANCEL }
|
||||||
|
enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
|
||||||
|
class OrderStateMachineConfig {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||||
|
context.scan(Set.of(tempDir), Set.of());
|
||||||
|
|
||||||
|
SpringBeanRegistry registry = new SpringBeanRegistry();
|
||||||
|
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||||
|
cu.accept(new SpringContextScanner(registry));
|
||||||
|
}
|
||||||
|
InjectionPointAnalyzer injectionAnalyzer =
|
||||||
|
new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
|
||||||
|
JdtCallGraphEngine engine = new JdtCallGraphEngine(context, injectionAnalyzer);
|
||||||
|
|
||||||
|
EntryPoint entry = EntryPoint.builder()
|
||||||
|
.className("com.example.ApiController")
|
||||||
|
.methodName("pay")
|
||||||
|
.build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.event("e")
|
||||||
|
.build();
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
|
||||||
|
Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
|
||||||
|
Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.OrderStateMachineConfig")
|
||||||
|
.transitions(List.of(pay, ship))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(chains).build())
|
||||||
|
.build();
|
||||||
|
new TransitionLinkerEnricher().enrich(result, null, null);
|
||||||
|
CallChain linked = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
|
||||||
|
assertThat(linked.getMatchedTransitions()).hasSize(1);
|
||||||
|
assertThat(linked.getMatchedTransitions().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.OrderEvent.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiComponentBeanStaysFailClosed(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), """
|
||||||
|
package com.example;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
public class ApiController {
|
||||||
|
Dispatcher dispatcher;
|
||||||
|
public void pay() { dispatcher.fire(); }
|
||||||
|
}
|
||||||
|
class Dispatcher {
|
||||||
|
StateMachine machine;
|
||||||
|
RichEvent carrier;
|
||||||
|
void fire() {
|
||||||
|
machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface RichEvent { String getA(); }
|
||||||
|
@Component
|
||||||
|
class PayRichEvent implements RichEvent {
|
||||||
|
public String getA() { return OrderEvent.PAY.name(); }
|
||||||
|
}
|
||||||
|
@Component
|
||||||
|
class ShipRichEvent implements RichEvent {
|
||||||
|
public String getA() { return OrderEvent.SHIP.name(); }
|
||||||
|
}
|
||||||
|
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||||
|
enum OrderEvent { PAY, SHIP, CANCEL }
|
||||||
|
enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
|
||||||
|
class OrderStateMachineConfig {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||||
|
context.scan(Set.of(tempDir), Set.of());
|
||||||
|
|
||||||
|
SpringBeanRegistry registry = new SpringBeanRegistry();
|
||||||
|
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||||
|
cu.accept(new SpringContextScanner(registry));
|
||||||
|
}
|
||||||
|
InjectionPointAnalyzer injectionAnalyzer =
|
||||||
|
new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
|
||||||
|
JdtCallGraphEngine engine = new JdtCallGraphEngine(context, injectionAnalyzer);
|
||||||
|
|
||||||
|
EntryPoint entry = EntryPoint.builder()
|
||||||
|
.className("com.example.ApiController")
|
||||||
|
.methodName("pay")
|
||||||
|
.build();
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.event("e")
|
||||||
|
.build();
|
||||||
|
List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
|
||||||
|
Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
|
||||||
|
Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.OrderStateMachineConfig")
|
||||||
|
.transitions(List.of(pay, ship))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(chains).build())
|
||||||
|
.build();
|
||||||
|
new TransitionLinkerEnricher().enrich(result, null, null);
|
||||||
|
CallChain linked = result.getMetadata().getCallChains().get(0);
|
||||||
|
assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
|
||||||
|
assertThat(linked.getLinkResolution())
|
||||||
|
.isIn(LinkResolution.AMBIGUOUS_WIDEN, LinkResolution.NO_MATCH, LinkResolution.UNRESOLVED_EXTERNAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Transition transition(String source, String target, String eventFqn) {
|
||||||
|
Transition transition = new Transition();
|
||||||
|
transition.setSourceStates(List.of(State.of(source, source)));
|
||||||
|
transition.setTargetStates(List.of(State.of(target, target)));
|
||||||
|
transition.setEvent(Event.of(eventFqn, eventFqn));
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user