Compare commits
2 Commits
77bf840465
...
e6effa3dcd
| Author | SHA1 | Date | |
|---|---|---|---|
| e6effa3dcd | |||
| 83008a6898 |
@@ -9,7 +9,10 @@ import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntellige
|
|||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CallChainEnricher implements AnalysisEnricher {
|
public class CallChainEnricher implements AnalysisEnricher {
|
||||||
@@ -47,6 +50,9 @@ public class CallChainEnricher implements AnalysisEnricher {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
List<EntryPoint> entryPoints = result.getMetadata().getEntryPoints();
|
List<EntryPoint> entryPoints = result.getMetadata().getEntryPoints();
|
||||||
|
if (entryPoints == null || entryPoints.isEmpty()) {
|
||||||
|
entryPoints = synthesizeEntryPoints(result.getMetadata().getCallChains());
|
||||||
|
}
|
||||||
if (entryPoints == null || entryPoints.isEmpty()) {
|
if (entryPoints == null || entryPoints.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -65,4 +71,37 @@ public class CallChainEnricher implements AnalysisEnricher {
|
|||||||
List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
|
List<CallChain> chains = intelligence.findCallChains(entryPoints, scopedTriggers);
|
||||||
return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
return MachineScopeFilter.filterCallChainsForMachine(chains, result.getName(), context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<EntryPoint> synthesizeEntryPoints(List<CallChain> callChains) {
|
||||||
|
if (callChains == null || callChains.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Map<String, EntryPoint> byMethod = new LinkedHashMap<>();
|
||||||
|
for (CallChain chain : callChains) {
|
||||||
|
EntryPoint entryPoint = chain.getEntryPoint();
|
||||||
|
if (hasClassAndMethod(entryPoint)) {
|
||||||
|
byMethod.putIfAbsent(entryPoint.getClassName() + "#" + entryPoint.getMethodName(), entryPoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TriggerPoint triggerPoint = chain.getTriggerPoint();
|
||||||
|
if (triggerPoint != null
|
||||||
|
&& triggerPoint.getClassName() != null
|
||||||
|
&& triggerPoint.getMethodName() != null) {
|
||||||
|
EntryPoint fallback = EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.className(triggerPoint.getClassName())
|
||||||
|
.methodName(triggerPoint.getMethodName())
|
||||||
|
.sourceFile(triggerPoint.getSourceFile())
|
||||||
|
.build();
|
||||||
|
byMethod.putIfAbsent(fallback.getClassName() + "#" + fallback.getMethodName(), fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(byMethod.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasClassAndMethod(EntryPoint entryPoint) {
|
||||||
|
return entryPoint != null
|
||||||
|
&& entryPoint.getClassName() != null
|
||||||
|
&& entryPoint.getMethodName() != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,11 @@ public class TriggerCanonicalizationEnricher implements AnalysisEnricher {
|
|||||||
if (chain.getTriggerPoint() == null) {
|
if (chain.getTriggerPoint() == null) {
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
return chain.toBuilder()
|
TriggerPoint canonical = MachineEnumCanonicalizer.canonicalizeTriggerPoint(
|
||||||
.triggerPoint(MachineEnumCanonicalizer.canonicalizeAndExpandTriggerPoint(
|
chain.getTriggerPoint(), machineTypes);
|
||||||
chain.getTriggerPoint(), machineTypes, context))
|
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||||
.build();
|
canonical, machineTypes, context, result.getTransitions(), true);
|
||||||
|
return chain.toBuilder().triggerPoint(enriched).build();
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
|||||||
@@ -62,16 +62,13 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
|
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
|
||||||
if (triggerPoint.getEventTypeFqn() != null) {
|
if (triggerPoint.getEventTypeFqn() != null && !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
||||||
if (smEventRaw.startsWith(triggerPoint.getEventTypeFqn() + ".")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn()) && !smEventRaw.contains(".")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
if (triggerPoint.getEventTypeFqn() != null && isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
|
||||||
|
return !smEventRaw.contains(".");
|
||||||
|
}
|
||||||
|
return !smEventRaw.contains(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchEventNames(rawTriggerEvent, smEventRaw, triggerPoint.getEventTypeFqn());
|
return matchEventNames(rawTriggerEvent, smEventRaw, triggerPoint.getEventTypeFqn());
|
||||||
|
|||||||
@@ -4,18 +4,30 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|||||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||||
import org.eclipse.jdt.core.dom.BooleanLiteral;
|
import org.eclipse.jdt.core.dom.BooleanLiteral;
|
||||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
import org.eclipse.jdt.core.dom.ConditionalExpression;
|
||||||
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
|
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
|
||||||
import org.eclipse.jdt.core.dom.EnumDeclaration;
|
import org.eclipse.jdt.core.dom.EnumDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.Block;
|
||||||
import org.eclipse.jdt.core.dom.Expression;
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.ExpressionStatement;
|
||||||
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.FieldDeclaration;
|
||||||
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.ParenthesizedExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.QualifiedName;
|
||||||
import org.eclipse.jdt.core.dom.ReturnStatement;
|
import org.eclipse.jdt.core.dom.ReturnStatement;
|
||||||
import org.eclipse.jdt.core.dom.SimpleName;
|
import org.eclipse.jdt.core.dom.SimpleName;
|
||||||
|
import org.eclipse.jdt.core.dom.SimpleType;
|
||||||
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.SwitchCase;
|
||||||
|
import org.eclipse.jdt.core.dom.SwitchExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.SwitchStatement;
|
||||||
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 org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||||
|
import org.eclipse.jdt.core.dom.YieldStatement;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -76,7 +88,7 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
}
|
}
|
||||||
List<String> filtered = new ArrayList<>();
|
List<String> filtered = new ArrayList<>();
|
||||||
for (String candidate : candidates) {
|
for (String candidate : candidates) {
|
||||||
if (satisfiesPredicates(candidate, predicates, enumDecl)) {
|
if (satisfiesPredicates(candidate, predicates, enumDecl, context)) {
|
||||||
filtered.add(candidate);
|
filtered.add(candidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,16 +98,18 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
private static boolean satisfiesPredicates(
|
private static boolean satisfiesPredicates(
|
||||||
String canonicalConstantFqn,
|
String canonicalConstantFqn,
|
||||||
List<PredicateCall> predicates,
|
List<PredicateCall> predicates,
|
||||||
EnumDeclaration enumDecl) {
|
EnumDeclaration enumDecl,
|
||||||
|
CodebaseContext context) {
|
||||||
String constantName = constantSimpleName(canonicalConstantFqn);
|
String constantName = constantSimpleName(canonicalConstantFqn);
|
||||||
if (constantName == null) {
|
if (constantName == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Map<String, Boolean> boolFields = resolveConstantBooleanFields(enumDecl, constantName);
|
Map<String, Boolean> boolFields = resolveConstantBooleanFields(enumDecl, constantName);
|
||||||
for (PredicateCall predicate : predicates) {
|
for (PredicateCall predicate : predicates) {
|
||||||
Boolean methodResult = evaluateBooleanMethod(enumDecl, predicate.methodName(), boolFields);
|
Boolean methodResult = evaluateBooleanMethod(
|
||||||
|
enumDecl, predicate.methodName(), constantName, boolFields, context);
|
||||||
if (methodResult == null) {
|
if (methodResult == null) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
boolean expected = predicate.negated ? !methodResult : methodResult;
|
boolean expected = predicate.negated ? !methodResult : methodResult;
|
||||||
if (!expected) {
|
if (!expected) {
|
||||||
@@ -108,17 +122,28 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
static Boolean evaluateBooleanMethod(
|
static Boolean evaluateBooleanMethod(
|
||||||
EnumDeclaration enumDecl,
|
EnumDeclaration enumDecl,
|
||||||
String methodName,
|
String methodName,
|
||||||
Map<String, Boolean> boolFields) {
|
String currentConstantName,
|
||||||
|
Map<String, Boolean> boolFields,
|
||||||
|
CodebaseContext context) {
|
||||||
MethodDeclaration method = findParameterlessMethod(enumDecl, methodName);
|
MethodDeclaration method = findParameterlessMethod(enumDecl, methodName);
|
||||||
|
if (method == null && context != null) {
|
||||||
|
method = findInterfaceParameterlessMethod(enumDecl, methodName, context);
|
||||||
|
}
|
||||||
if (method == null || method.getBody() == null) {
|
if (method == null || method.getBody() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
Boolean switchResult = evaluateSwitchDrivenMethod(
|
||||||
|
method, currentConstantName, boolFields, enumDecl, context);
|
||||||
|
if (switchResult != null) {
|
||||||
|
return switchResult;
|
||||||
|
}
|
||||||
Boolean[] result = new Boolean[1];
|
Boolean[] result = new Boolean[1];
|
||||||
method.getBody().accept(new ASTVisitor() {
|
method.getBody().accept(new ASTVisitor() {
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(ReturnStatement node) {
|
public boolean visit(ReturnStatement node) {
|
||||||
if (result[0] == null) {
|
if (result[0] == null) {
|
||||||
result[0] = evaluateBooleanExpression(node.getExpression(), boolFields);
|
result[0] = evaluateBooleanExpression(
|
||||||
|
node.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
||||||
}
|
}
|
||||||
return super.visit(node);
|
return super.visit(node);
|
||||||
}
|
}
|
||||||
@@ -126,6 +151,35 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
return result[0];
|
return result[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Boolean evaluateSwitchDrivenMethod(
|
||||||
|
MethodDeclaration method,
|
||||||
|
String currentConstantName,
|
||||||
|
Map<String, Boolean> boolFields,
|
||||||
|
EnumDeclaration enumDecl,
|
||||||
|
CodebaseContext context) {
|
||||||
|
Boolean[] result = new Boolean[1];
|
||||||
|
method.getBody().accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(SwitchStatement node) {
|
||||||
|
if (result[0] == null && isEnumSelfSelector(node.getExpression())) {
|
||||||
|
result[0] = evaluateSwitchLike(
|
||||||
|
node.statements(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
return result[0] == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(SwitchExpression node) {
|
||||||
|
if (result[0] == null && isEnumSelfSelector(node.getExpression())) {
|
||||||
|
result[0] = evaluateSwitchLike(
|
||||||
|
node.statements(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
return result[0] == null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
static Map<String, Boolean> resolveConstantBooleanFields(EnumDeclaration enumDecl, String constantName) {
|
static Map<String, Boolean> resolveConstantBooleanFields(EnumDeclaration enumDecl, String constantName) {
|
||||||
Map<String, Boolean> fields = new HashMap<>();
|
Map<String, Boolean> fields = new HashMap<>();
|
||||||
EnumConstantDeclaration constant = findEnumConstant(enumDecl, constantName);
|
EnumConstantDeclaration constant = findEnumConstant(enumDecl, constantName);
|
||||||
@@ -179,7 +233,12 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Boolean evaluateBooleanExpression(Expression expr, Map<String, Boolean> boolFields) {
|
private static Boolean evaluateBooleanExpression(
|
||||||
|
Expression expr,
|
||||||
|
String currentConstantName,
|
||||||
|
Map<String, Boolean> boolFields,
|
||||||
|
EnumDeclaration enumDecl,
|
||||||
|
CodebaseContext context) {
|
||||||
if (expr instanceof BooleanLiteral bl) {
|
if (expr instanceof BooleanLiteral bl) {
|
||||||
return bl.booleanValue();
|
return bl.booleanValue();
|
||||||
}
|
}
|
||||||
@@ -191,6 +250,149 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
return boolFields.get(fa.getName().getIdentifier());
|
return boolFields.get(fa.getName().getIdentifier());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (expr instanceof ParenthesizedExpression pe) {
|
||||||
|
return evaluateBooleanExpression(
|
||||||
|
pe.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
if (expr instanceof ConditionalExpression ce) {
|
||||||
|
Boolean thenVal = evaluateBooleanExpression(
|
||||||
|
ce.getThenExpression(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
Boolean elseVal = evaluateBooleanExpression(
|
||||||
|
ce.getElseExpression(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
if (thenVal != null && elseVal != null && thenVal.equals(elseVal)) {
|
||||||
|
return thenVal;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (expr instanceof SwitchExpression se && isEnumSelfSelector(se.getExpression())) {
|
||||||
|
return evaluateSwitchLike(se.statements(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
if (expr instanceof MethodInvocation mi
|
||||||
|
&& mi.arguments().isEmpty()
|
||||||
|
&& enumDecl != null
|
||||||
|
&& context != null) {
|
||||||
|
return evaluateBooleanMethod(
|
||||||
|
enumDecl, mi.getName().getIdentifier(), currentConstantName, boolFields, context);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean evaluateSwitchLike(
|
||||||
|
List<?> statements,
|
||||||
|
String currentConstantName,
|
||||||
|
Map<String, Boolean> boolFields,
|
||||||
|
EnumDeclaration enumDecl,
|
||||||
|
CodebaseContext context) {
|
||||||
|
List<String> caseConstants = new ArrayList<>();
|
||||||
|
boolean defaultCase = false;
|
||||||
|
boolean sawArmBody = false;
|
||||||
|
boolean pendingCase = false;
|
||||||
|
for (Object statementObj : statements) {
|
||||||
|
if (statementObj instanceof SwitchCase switchCase) {
|
||||||
|
if (sawArmBody) {
|
||||||
|
caseConstants.clear();
|
||||||
|
defaultCase = false;
|
||||||
|
sawArmBody = false;
|
||||||
|
}
|
||||||
|
pendingCase = true;
|
||||||
|
if (switchCase.isDefault()) {
|
||||||
|
defaultCase = true;
|
||||||
|
} else {
|
||||||
|
for (Object exprObj : switchCase.expressions()) {
|
||||||
|
if (exprObj instanceof Expression caseExpr) {
|
||||||
|
String caseConstant = extractCaseConstantName(caseExpr);
|
||||||
|
if (caseConstant != null) {
|
||||||
|
caseConstants.add(caseConstant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!pendingCase) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sawArmBody = true;
|
||||||
|
if (!defaultCase && !caseConstants.contains(currentConstantName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Boolean armValue = evaluateSwitchArmNode(
|
||||||
|
statementObj, currentConstantName, boolFields, enumDecl, context);
|
||||||
|
if (armValue != null) {
|
||||||
|
return armValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean evaluateSwitchArmNode(
|
||||||
|
Object node,
|
||||||
|
String currentConstantName,
|
||||||
|
Map<String, Boolean> boolFields,
|
||||||
|
EnumDeclaration enumDecl,
|
||||||
|
CodebaseContext context) {
|
||||||
|
if (node instanceof ReturnStatement rs) {
|
||||||
|
return evaluateBooleanExpression(
|
||||||
|
rs.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
if (node instanceof YieldStatement ys) {
|
||||||
|
return evaluateBooleanExpression(
|
||||||
|
ys.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
if (node instanceof ExpressionStatement es) {
|
||||||
|
return evaluateBooleanExpression(
|
||||||
|
es.getExpression(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
if (node instanceof Block block) {
|
||||||
|
return evaluateLinearBooleanStatements(
|
||||||
|
block.statements(), currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
if (node instanceof Expression expression) {
|
||||||
|
return evaluateBooleanExpression(
|
||||||
|
expression, currentConstantName, boolFields, enumDecl, context);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean evaluateLinearBooleanStatements(
|
||||||
|
List<?> statements,
|
||||||
|
String currentConstantName,
|
||||||
|
Map<String, Boolean> boolFields,
|
||||||
|
EnumDeclaration enumDecl,
|
||||||
|
CodebaseContext context) {
|
||||||
|
for (Object statementObj : statements) {
|
||||||
|
Boolean value = evaluateSwitchArmNode(
|
||||||
|
statementObj, currentConstantName, boolFields, enumDecl, context);
|
||||||
|
if (value != null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEnumSelfSelector(Expression expr) {
|
||||||
|
if (expr instanceof ThisExpression) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (expr instanceof ParenthesizedExpression pe) {
|
||||||
|
return isEnumSelfSelector(pe.getExpression());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractCaseConstantName(Expression expr) {
|
||||||
|
if (expr instanceof SimpleName simpleName) {
|
||||||
|
return simpleName.getIdentifier();
|
||||||
|
}
|
||||||
|
if (expr instanceof QualifiedName qualifiedName) {
|
||||||
|
return qualifiedName.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (expr instanceof FieldAccess fieldAccess) {
|
||||||
|
return fieldAccess.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
if (expr instanceof ParenthesizedExpression pe) {
|
||||||
|
return extractCaseConstantName(pe.getExpression());
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +400,11 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
return expr instanceof BooleanLiteral bl ? bl.booleanValue() : null;
|
return expr instanceof BooleanLiteral bl ? bl.booleanValue() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MethodDeclaration findParameterlessMethod(EnumDeclaration enumDecl, String methodName) {
|
private static MethodDeclaration findParameterlessMethod(AbstractTypeDeclaration typeDecl, String methodName) {
|
||||||
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
if (typeDecl == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Object bodyObj : typeDecl.bodyDeclarations()) {
|
||||||
if (bodyObj instanceof MethodDeclaration md
|
if (bodyObj instanceof MethodDeclaration md
|
||||||
&& !md.isConstructor()
|
&& !md.isConstructor()
|
||||||
&& methodName.equals(md.getName().getIdentifier())
|
&& methodName.equals(md.getName().getIdentifier())
|
||||||
@@ -210,6 +415,25 @@ public final class EnumMemberPredicateEvaluator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MethodDeclaration findInterfaceParameterlessMethod(
|
||||||
|
EnumDeclaration enumDecl,
|
||||||
|
String methodName,
|
||||||
|
CodebaseContext context) {
|
||||||
|
CompilationUnit cu = enumDecl.getRoot() instanceof CompilationUnit root ? root : null;
|
||||||
|
for (Object typeObj : enumDecl.superInterfaceTypes()) {
|
||||||
|
if (!(typeObj instanceof SimpleType simpleType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String interfaceName = simpleType.getName().getFullyQualifiedName();
|
||||||
|
TypeDeclaration interfaceDecl = context.getTypeDeclaration(interfaceName, cu);
|
||||||
|
MethodDeclaration method = findParameterlessMethod(interfaceDecl, methodName);
|
||||||
|
if (method != null) {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static MethodDeclaration findEnumConstructor(EnumDeclaration enumDecl) {
|
private static MethodDeclaration findEnumConstructor(EnumDeclaration enumDecl) {
|
||||||
MethodDeclaration implicit = null;
|
MethodDeclaration implicit = null;
|
||||||
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
for (Object bodyObj : enumDecl.bodyDeclarations()) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -146,10 +147,32 @@ public final class MachineEnumCanonicalizer {
|
|||||||
}
|
}
|
||||||
List<String> expanded = expandSymbolicPolymorphicEvents(
|
List<String> expanded = expandSymbolicPolymorphicEvents(
|
||||||
canonical.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context);
|
canonical.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context);
|
||||||
if (expanded == canonical.getPolymorphicEvents()) {
|
List<String> narrowed = narrowExpandedPolymorphicEvents(
|
||||||
|
expanded,
|
||||||
|
canonical.getConstraint(),
|
||||||
|
machineTypes.eventTypeFqn(),
|
||||||
|
null,
|
||||||
|
context);
|
||||||
|
if (Objects.equals(narrowed, canonical.getPolymorphicEvents())) {
|
||||||
return canonical;
|
return canonical;
|
||||||
}
|
}
|
||||||
return canonical.toBuilder().polymorphicEvents(expanded).build();
|
return canonical.toBuilder().polymorphicEvents(narrowed).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands symbolic placeholders and applies predicate/transition narrowing when possible.
|
||||||
|
*/
|
||||||
|
public static List<String> narrowExpandedPolymorphicEvents(
|
||||||
|
List<String> polymorphicEvents,
|
||||||
|
String constraint,
|
||||||
|
String eventTypeFqn,
|
||||||
|
List<Transition> machineTransitions,
|
||||||
|
CodebaseContext context) {
|
||||||
|
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
|
||||||
|
return polymorphicEvents;
|
||||||
|
}
|
||||||
|
return narrowPolymorphicCandidates(
|
||||||
|
polymorphicEvents, constraint, eventTypeFqn, machineTransitions, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,11 +202,25 @@ public final class MachineEnumCanonicalizer {
|
|||||||
List<Transition> machineTransitions,
|
List<Transition> machineTransitions,
|
||||||
boolean skipCanonicalization) {
|
boolean skipCanonicalization) {
|
||||||
TriggerPoint expanded = skipCanonicalization
|
TriggerPoint expanded = skipCanonicalization
|
||||||
? trigger
|
? expandSymbolicOnly(trigger, machineTypes, context)
|
||||||
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
|
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
|
||||||
if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||||
return expanded;
|
return expanded;
|
||||||
}
|
}
|
||||||
|
List<String> postExpand = expanded.getPolymorphicEvents();
|
||||||
|
if (postExpand != null && postExpand.stream().anyMatch(pe -> pe != null && pe.startsWith("<SYMBOLIC:"))) {
|
||||||
|
postExpand = expandSymbolicPolymorphicEvents(
|
||||||
|
postExpand, machineTypes.eventTypeFqn(), context);
|
||||||
|
postExpand = narrowExpandedPolymorphicEvents(
|
||||||
|
postExpand,
|
||||||
|
expanded.getConstraint(),
|
||||||
|
machineTypes.eventTypeFqn(),
|
||||||
|
machineTransitions,
|
||||||
|
context);
|
||||||
|
if (!Objects.equals(postExpand, expanded.getPolymorphicEvents())) {
|
||||||
|
expanded = expanded.toBuilder().polymorphicEvents(postExpand).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (hasConcretePolymorphicEvents(expanded.getPolymorphicEvents())) {
|
if (hasConcretePolymorphicEvents(expanded.getPolymorphicEvents())) {
|
||||||
List<String> narrowed = narrowPolymorphicCandidates(
|
List<String> narrowed = narrowPolymorphicCandidates(
|
||||||
expanded.getPolymorphicEvents(),
|
expanded.getPolymorphicEvents(),
|
||||||
@@ -199,22 +236,78 @@ public final class MachineEnumCanonicalizer {
|
|||||||
}
|
}
|
||||||
return expanded;
|
return expanded;
|
||||||
}
|
}
|
||||||
if (classifyTriggerEvent(expanded.getEvent()) != TriggerEventKind.DYNAMIC_EXPRESSION) {
|
if (shouldInferPolymorphicEvents(expanded, machineTransitions)) {
|
||||||
return expanded;
|
|
||||||
}
|
|
||||||
List<String> machineEvents = inferPolymorphicCandidates(
|
List<String> machineEvents = inferPolymorphicCandidates(
|
||||||
expanded.getConstraint(),
|
expanded.getConstraint(),
|
||||||
machineTypes.eventTypeFqn(),
|
machineTypes.eventTypeFqn(),
|
||||||
machineTransitions,
|
machineTransitions,
|
||||||
context);
|
context);
|
||||||
if (machineEvents.isEmpty()) {
|
if (!machineEvents.isEmpty()) {
|
||||||
return expanded;
|
|
||||||
}
|
|
||||||
return expanded.toBuilder()
|
return expanded.toBuilder()
|
||||||
.polymorphicEvents(machineEvents)
|
.polymorphicEvents(machineEvents)
|
||||||
.ambiguous(machineEvents.size() > 1)
|
.ambiguous(machineEvents.size() > 1)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TriggerPoint expandSymbolicOnly(
|
||||||
|
TriggerPoint trigger,
|
||||||
|
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||||
|
CodebaseContext context) {
|
||||||
|
if (trigger == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
List<String> expanded = expandSymbolicPolymorphicEvents(
|
||||||
|
trigger.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context);
|
||||||
|
if (java.util.Objects.equals(expanded, trigger.getPolymorphicEvents())) {
|
||||||
|
return trigger;
|
||||||
|
}
|
||||||
|
return trigger.toBuilder().polymorphicEvents(expanded).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldInferPolymorphicEvents(
|
||||||
|
TriggerPoint trigger,
|
||||||
|
List<Transition> machineTransitions) {
|
||||||
|
if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (hasOnlySymbolicPolymorphicEvents(trigger.getPolymorphicEvents())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
List<String> polyEvents = trigger.getPolymorphicEvents();
|
||||||
|
if (polyEvents != null && !polyEvents.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(trigger.getConstraint())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (trigger.isExternal() || trigger.isAmbiguous()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return classifyTriggerEvent(trigger.getEvent()) == TriggerEventKind.DYNAMIC_EXPRESSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasOnlySymbolicPolymorphicEvents(List<String> polymorphicEvents) {
|
||||||
|
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean hasSymbolic = false;
|
||||||
|
for (String pe : polymorphicEvents) {
|
||||||
|
if (pe == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pe.startsWith("<SYMBOLIC:")) {
|
||||||
|
hasSymbolic = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasSymbolic;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<String> inferPolymorphicCandidates(
|
private static List<String> inferPolymorphicCandidates(
|
||||||
String constraint,
|
String constraint,
|
||||||
@@ -233,6 +326,9 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (!filtered.isEmpty()) {
|
if (!filtered.isEmpty()) {
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
if (!transitionEvents.isEmpty()) {
|
||||||
|
return transitionEvents;
|
||||||
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,9 +338,12 @@ public final class MachineEnumCanonicalizer {
|
|||||||
String eventTypeFqn,
|
String eventTypeFqn,
|
||||||
List<Transition> machineTransitions,
|
List<Transition> machineTransitions,
|
||||||
CodebaseContext context) {
|
CodebaseContext context) {
|
||||||
|
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
|
||||||
List<String> result = EnumMemberPredicateEvaluator.filterEnumConstants(
|
List<String> result = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||||
current, constraint, eventTypeFqn, context);
|
current, constraint, eventTypeFqn, context);
|
||||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
|
if (result.isEmpty() && !transitionEvents.isEmpty()) {
|
||||||
|
result = new ArrayList<>(transitionEvents);
|
||||||
|
}
|
||||||
if (!looksOverBroad(result, transitionEvents, constraint)) {
|
if (!looksOverBroad(result, transitionEvents, constraint)) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -281,10 +380,18 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (transitionEvents.isEmpty()) {
|
if (transitionEvents.isEmpty()) {
|
||||||
return current.size() > 1;
|
return current.size() > 1;
|
||||||
}
|
}
|
||||||
return current.size() > transitionEvents.size();
|
if (current.size() > transitionEvents.size()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (String event : current) {
|
||||||
|
if (!transitionEvents.contains(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<String> polymorphicEventsFromTransitions(
|
public static List<String> polymorphicEventsFromTransitions(
|
||||||
List<Transition> machineTransitions,
|
List<Transition> machineTransitions,
|
||||||
String eventTypeFqn) {
|
String eventTypeFqn) {
|
||||||
if (machineTransitions == null || machineTransitions.isEmpty() || eventTypeFqn == null) {
|
if (machineTransitions == null || machineTransitions.isEmpty() || eventTypeFqn == null) {
|
||||||
|
|||||||
@@ -677,33 +677,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
sourceMethod = "inline-ternary";
|
sourceMethod = "inline-ternary";
|
||||||
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
|
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
|
||||||
} else if (exprNode instanceof SwitchExpression swExpr) {
|
} else if (exprNode instanceof SwitchExpression swExpr) {
|
||||||
List<String> polyEventsRef = polymorphicEvents;
|
constantExtractor.extractConstantsFromExpression(swExpr, polymorphicEvents);
|
||||||
swExpr.accept(new ASTVisitor() {
|
|
||||||
@Override
|
|
||||||
public boolean visit(YieldStatement ys) {
|
|
||||||
List<Expression> tracedBranch = variableTracer.traceVariableAll(ys.getExpression());
|
|
||||||
for (Expression tb : tracedBranch) {
|
|
||||||
if (tb instanceof ClassInstanceCreation cic) {
|
|
||||||
polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
|
|
||||||
} else {
|
|
||||||
constantExtractor.extractConstantsFromExpression(tb, polyEventsRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(ys);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean visit(ExpressionStatement es) {
|
|
||||||
List<Expression> tracedBranch = variableTracer.traceVariableAll(es.getExpression());
|
|
||||||
for (Expression tb : tracedBranch) {
|
|
||||||
if (tb instanceof ClassInstanceCreation cic) {
|
|
||||||
polyEventsRef.add(click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(cic.getType()));
|
|
||||||
} else {
|
|
||||||
constantExtractor.extractConstantsFromExpression(tb, polyEventsRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.visit(es);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
sourceMethod = "inline-switch";
|
sourceMethod = "inline-switch";
|
||||||
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
|
declaredType = polymorphicEvents.isEmpty() ? null : polymorphicEvents.get(0);
|
||||||
} else if (exprNode instanceof SimpleName sn) {
|
} else if (exprNode instanceof SimpleName sn) {
|
||||||
@@ -888,12 +862,36 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (declaredType != null) {
|
if (declaredType != null) {
|
||||||
List<String> enumValues = context.getEnumValues(declaredType);
|
List<String> enumValues = context.getEnumValues(declaredType);
|
||||||
if (enumValues != null && !enumValues.isEmpty()) {
|
if (enumValues != null && !enumValues.isEmpty()) {
|
||||||
|
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
||||||
|
&& polymorphicEvents.isEmpty()
|
||||||
|
&& exprNode instanceof MethodInvocation miArg
|
||||||
|
&& entryMethod != null
|
||||||
|
&& entryMethod.contains(".")) {
|
||||||
|
String entryClass = entryMethod.substring(0, entryMethod.lastIndexOf('.'));
|
||||||
|
List<String> methodReturns = constantExtractor.resolveMethodReturnConstant(
|
||||||
|
entryClass,
|
||||||
|
miArg.getName().getIdentifier(),
|
||||||
|
0,
|
||||||
|
new java.util.HashSet<>(),
|
||||||
|
null);
|
||||||
|
for (String methodReturn : methodReturns) {
|
||||||
|
if (methodReturn != null && !polymorphicEvents.contains(methodReturn)) {
|
||||||
|
polymorphicEvents.add(methodReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
||||||
|
&& polymorphicEvents.isEmpty()
|
||||||
|
&& exprNode instanceof Expression expressionArg) {
|
||||||
|
addConstantsFromTracedExpression(expressionArg, polymorphicEvents, path, callGraph, entryMethod);
|
||||||
|
}
|
||||||
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
||||||
&& !tp.isExternal()
|
&& !tp.isExternal()
|
||||||
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)
|
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)
|
||||||
&& !(keyedMapLookupOnInitializer[0] && pathBoundMapKeyOnInitializer[0])) {
|
&& !(keyedMapLookupOnInitializer[0] && pathBoundMapKeyOnInitializer[0])) {
|
||||||
for (String ev : enumValues) {
|
String symbolic = "<SYMBOLIC: " + declaredType + ".*>";
|
||||||
polymorphicEvents.add(constantExtractor.parseEnumSetElement(ev));
|
if (!polymorphicEvents.contains(symbolic)) {
|
||||||
|
polymorphicEvents.add(symbolic);
|
||||||
}
|
}
|
||||||
isAmbiguous = true;
|
isAmbiguous = true;
|
||||||
}
|
}
|
||||||
@@ -941,6 +939,48 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
if (!hasValidConstant && exprNode instanceof Expression expressionFallback) {
|
if (!hasValidConstant && exprNode instanceof Expression expressionFallback) {
|
||||||
addConstantsFromTracedExpression(expressionFallback, polymorphicEvents, path, callGraph, entryMethod);
|
addConstantsFromTracedExpression(expressionFallback, polymorphicEvents, path, callGraph, entryMethod);
|
||||||
|
List<String> directConstants = new ArrayList<>();
|
||||||
|
constantExtractor.extractConstantsFromExpression(expressionFallback, directConstants);
|
||||||
|
for (String directConstant : directConstants) {
|
||||||
|
if (directConstant != null && !directConstant.startsWith("<SYMBOLIC:")
|
||||||
|
&& !polymorphicEvents.contains(directConstant)) {
|
||||||
|
polymorphicEvents.add(directConstant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expressionFallback instanceof MethodInvocation fallbackMi
|
||||||
|
&& (fallbackMi.getExpression() == null || fallbackMi.getExpression() instanceof ThisExpression)) {
|
||||||
|
TypeDeclaration enclosingType = findEnclosingType(fallbackMi);
|
||||||
|
CompilationUnit fallbackCu = fallbackMi.getRoot() instanceof CompilationUnit cu ? cu : null;
|
||||||
|
if (enclosingType != null) {
|
||||||
|
List<String> switchConstants = resolveLocalSwitchMethodConstants(enclosingType, fallbackMi);
|
||||||
|
for (String switchConstant : switchConstants) {
|
||||||
|
if (switchConstant != null && !switchConstant.startsWith("<SYMBOLIC:")
|
||||||
|
&& !polymorphicEvents.contains(switchConstant)) {
|
||||||
|
polymorphicEvents.add(switchConstant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> methodReturns = constantExtractor.resolveMethodReturnConstant(
|
||||||
|
context.getFqn(enclosingType),
|
||||||
|
fallbackMi.getName().getIdentifier(),
|
||||||
|
0,
|
||||||
|
new HashSet<>(),
|
||||||
|
fallbackCu);
|
||||||
|
for (String methodReturn : methodReturns) {
|
||||||
|
if (methodReturn != null && !methodReturn.startsWith("<SYMBOLIC:")
|
||||||
|
&& !polymorphicEvents.contains(methodReturn)) {
|
||||||
|
polymorphicEvents.add(methodReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
||||||
|
&& polymorphicEvents.stream().anyMatch(pe -> pe != null && pe.startsWith("<SYMBOLIC:"))
|
||||||
|
&& declaredType != null) {
|
||||||
|
List<String> expandedEnumValues = expandDeclaredEnumValues(declaredType);
|
||||||
|
if (!expandedEnumValues.isEmpty()) {
|
||||||
|
polymorphicEvents = new ArrayList<>(expandedEnumValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> newPolyEvents = new ArrayList<>();
|
List<String> newPolyEvents = new ArrayList<>();
|
||||||
@@ -1003,7 +1043,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
unpacked.add(clean);
|
unpacked.add(clean);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (ev.startsWith("<SYMBOLIC:")) {
|
} else if (ev.startsWith("<SYMBOLIC:") || ev.contains("SYMBOLIC:") || ev.endsWith(".*>")) {
|
||||||
if (!unpacked.contains(ev)) {
|
if (!unpacked.contains(ev)) {
|
||||||
unpacked.add(ev);
|
unpacked.add(ev);
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1059,8 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
|
|
||||||
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
|
polymorphicEvents = polymorphicEvents.stream().distinct().collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
|
polymorphicEvents.removeIf(pe -> pe != null && (pe.equals("*>") || pe.equals(".*>")));
|
||||||
|
|
||||||
polymorphicEvents = narrowPolymorphicEventsWithPathBindings(polymorphicEvents, path, callGraph, tp);
|
polymorphicEvents = narrowPolymorphicEventsWithPathBindings(polymorphicEvents, path, callGraph, tp);
|
||||||
|
|
||||||
Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
|
Expression runtimeExpr = exprNode instanceof Expression expression ? expression : null;
|
||||||
@@ -1165,12 +1207,10 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return originalExpr.toString();
|
return originalExpr.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve field-wired helper calls (routingHelper.payAction()) for per-endpoint literal forwarding.
|
// Preserve routing helper calls for per-endpoint literal forwarding.
|
||||||
if (originalExpr instanceof MethodInvocation helperMi
|
if (originalExpr instanceof MethodInvocation helperMi
|
||||||
&& helperMi.getExpression() instanceof SimpleName helperReceiver
|
|
||||||
&& !helperReceiver.getIdentifier().isEmpty()
|
|
||||||
&& Character.isLowerCase(helperReceiver.getIdentifier().charAt(0))
|
|
||||||
&& helperMi.arguments().isEmpty()
|
&& helperMi.arguments().isEmpty()
|
||||||
|
&& isLikelyRoutingHelperReceiver(helperMi.getExpression())
|
||||||
&& resolvedValue != null
|
&& resolvedValue != null
|
||||||
&& resolvedValue.matches("^[A-Z_][A-Z0-9_]*$")) {
|
&& resolvedValue.matches("^[A-Z_][A-Z0-9_]*$")) {
|
||||||
return originalExpr.toString();
|
return originalExpr.toString();
|
||||||
@@ -1200,6 +1240,25 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return resolvedValue;
|
return resolvedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isLikelyRoutingHelperReceiver(Expression receiver) {
|
||||||
|
if (receiver instanceof SimpleName simpleName) {
|
||||||
|
String identifier = simpleName.getIdentifier();
|
||||||
|
return !identifier.isEmpty()
|
||||||
|
&& (Character.isLowerCase(identifier.charAt(0))
|
||||||
|
|| Character.isUpperCase(identifier.charAt(0)));
|
||||||
|
}
|
||||||
|
if (receiver instanceof QualifiedName qualifiedName) {
|
||||||
|
String identifier = qualifiedName.getName().getIdentifier();
|
||||||
|
return !identifier.isEmpty() && Character.isUpperCase(identifier.charAt(0));
|
||||||
|
}
|
||||||
|
if (receiver instanceof TypeLiteral typeLiteral) {
|
||||||
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||||
|
.extractSimpleTypeName(typeLiteral.getType());
|
||||||
|
return typeName != null && !typeName.isEmpty() && Character.isUpperCase(typeName.charAt(0));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected String getReceiverString(Expression receiver) {
|
protected String getReceiverString(Expression receiver) {
|
||||||
if (receiver == null) {
|
if (receiver == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2731,6 +2790,63 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> resolveLocalSwitchMethodConstants(TypeDeclaration ownerType, MethodInvocation methodInvocation) {
|
||||||
|
if (ownerType == null || methodInvocation == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
MethodDeclaration methodDeclaration = context.findMethodDeclaration(
|
||||||
|
ownerType, methodInvocation.getName().getIdentifier(), true);
|
||||||
|
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
for (Object statementObj : methodDeclaration.getBody().statements()) {
|
||||||
|
if (statementObj instanceof SwitchStatement switchStatement) {
|
||||||
|
constantExtractor.extractConstantsFromSwitchStatement(switchStatement, constants);
|
||||||
|
} else if (statementObj instanceof ReturnStatement returnStatement
|
||||||
|
&& returnStatement.getExpression() instanceof SwitchExpression switchExpression) {
|
||||||
|
constantExtractor.extractConstantsFromExpression(switchExpression, constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return constants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> expandDeclaredEnumValues(String declaredType) {
|
||||||
|
if (declaredType == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> enumValues = context.getEnumValues(declaredType);
|
||||||
|
if ((enumValues == null || enumValues.isEmpty()) && declaredType.contains(".")) {
|
||||||
|
enumValues = context.getEnumValues(declaredType.substring(declaredType.lastIndexOf('.') + 1));
|
||||||
|
}
|
||||||
|
if ((enumValues == null || enumValues.isEmpty())) {
|
||||||
|
String simpleDeclared = declaredType.contains(".")
|
||||||
|
? declaredType.substring(declaredType.lastIndexOf('.') + 1)
|
||||||
|
: declaredType;
|
||||||
|
for (Map.Entry<String, List<String>> entry : context.getEnumValuesMap().entrySet()) {
|
||||||
|
String enumType = entry.getKey();
|
||||||
|
String simpleType = enumType.contains(".")
|
||||||
|
? enumType.substring(enumType.lastIndexOf('.') + 1)
|
||||||
|
: enumType;
|
||||||
|
if (simpleDeclared.equals(simpleType)) {
|
||||||
|
enumValues = entry.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enumValues == null || enumValues.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> expanded = new ArrayList<>();
|
||||||
|
for (String enumValue : enumValues) {
|
||||||
|
String parsed = constantExtractor.parseEnumSetElement(enumValue);
|
||||||
|
if (parsed != null && !expanded.contains(parsed)) {
|
||||||
|
expanded.add(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isRuntimeEnumParameter(Expression exprNode) {
|
private boolean isRuntimeEnumParameter(Expression exprNode) {
|
||||||
if (!(exprNode instanceof MethodInvocation mi)) {
|
if (!(exprNode instanceof MethodInvocation mi)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2876,9 +2992,38 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
Map<String, String> bindings = pathBindingEvaluator.traceBindings(path, callGraph, pathFinder);
|
||||||
if (bindings.isEmpty()) {
|
if (bindings.isEmpty()) {
|
||||||
|
if (hasMultiClassPolymorphicPath(path)
|
||||||
|
&& polymorphicEvents.stream().noneMatch(this::looksLikeEnumConstant)) {
|
||||||
|
List<String> callerArgumentResolved = resolvePolymorphicEventsFromCallerArgument(path);
|
||||||
|
if (!callerArgumentResolved.isEmpty()) {
|
||||||
|
return callerArgumentResolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
return polymorphicEvents;
|
return polymorphicEvents;
|
||||||
}
|
}
|
||||||
List<String> narrowed = new ArrayList<>(polymorphicEvents);
|
List<String> narrowed = new ArrayList<>(polymorphicEvents);
|
||||||
|
if (narrowed.stream().noneMatch(this::looksLikeEnumConstant)) {
|
||||||
|
List<String> bindingResolved = resolvePolymorphicEventsFromBindings(bindings, path);
|
||||||
|
boolean bindingHasConcrete = bindingResolved.stream().anyMatch(this::looksLikeEnumConstant);
|
||||||
|
if (bindingHasConcrete) {
|
||||||
|
narrowed = new ArrayList<>(bindingResolved);
|
||||||
|
} else {
|
||||||
|
for (String candidate : bindingResolved) {
|
||||||
|
if (candidate != null && !narrowed.contains(candidate)) {
|
||||||
|
narrowed.add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean originallySymbolicOnly = polymorphicEvents.stream().noneMatch(this::looksLikeEnumConstant);
|
||||||
|
if (originallySymbolicOnly && narrowed.stream().filter(this::looksLikeEnumConstant).count() <= 1) {
|
||||||
|
List<String> callerArgumentResolved = resolvePolymorphicEventsFromCallerArgument(path);
|
||||||
|
for (String candidate : callerArgumentResolved) {
|
||||||
|
if (candidate != null && !narrowed.contains(candidate)) {
|
||||||
|
narrowed.add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
boolean hasConcrete = narrowed.stream().anyMatch(this::looksLikeEnumConstant);
|
boolean hasConcrete = narrowed.stream().anyMatch(this::looksLikeEnumConstant);
|
||||||
if (hasConcrete) {
|
if (hasConcrete) {
|
||||||
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
narrowed.removeIf(pe -> pe != null && pe.startsWith("<SYMBOLIC:"));
|
||||||
@@ -2889,6 +3034,322 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
return narrowed;
|
return narrowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> resolvePolymorphicEventsFromBindings(
|
||||||
|
Map<String, String> bindings,
|
||||||
|
List<String> path) {
|
||||||
|
if (bindings == null || bindings.isEmpty() || path == null || path.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
String callerMethod = path.size() >= 2 ? path.get(path.size() - 2) : path.get(0);
|
||||||
|
String callerClass = callerMethod.contains(".")
|
||||||
|
? callerMethod.substring(0, callerMethod.lastIndexOf('.'))
|
||||||
|
: callerMethod;
|
||||||
|
TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
|
||||||
|
List<String> resolved = new ArrayList<>();
|
||||||
|
for (String boundValue : bindings.values()) {
|
||||||
|
if (boundValue == null || boundValue.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (boundValue.startsWith("ENUM_SET:")
|
||||||
|
|| boundValue.startsWith("<SYMBOLIC:")
|
||||||
|
|| boundValue.contains("SYMBOLIC:")
|
||||||
|
|| boundValue.endsWith(".*>")) {
|
||||||
|
addResolvedConstantToList(boundValue, resolved);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ASTNode parsed = parseExpressionString(boundValue);
|
||||||
|
if (!(parsed instanceof Expression boundExpr)) {
|
||||||
|
addResolvedConstantToList(boundValue, resolved);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String directResolved = constantResolver.resolve(boundExpr, context);
|
||||||
|
if (directResolved != null) {
|
||||||
|
addResolvedConstantToList(directResolved, resolved);
|
||||||
|
}
|
||||||
|
List<String> extracted = new ArrayList<>();
|
||||||
|
if (boundExpr instanceof MethodInvocation methodInvocation
|
||||||
|
&& callerType != null
|
||||||
|
&& (methodInvocation.getExpression() == null
|
||||||
|
|| methodInvocation.getExpression() instanceof ThisExpression)) {
|
||||||
|
extracted.addAll(resolveLocalSwitchMethodConstants(callerType, methodInvocation));
|
||||||
|
}
|
||||||
|
if (extracted.isEmpty()) {
|
||||||
|
constantExtractor.extractConstantsFromExpression(boundExpr, extracted);
|
||||||
|
}
|
||||||
|
for (String candidate : extracted) {
|
||||||
|
addResolvedConstantToList(candidate, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved.stream().distinct().collect(java.util.stream.Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolvePolymorphicEventsFromCallerArgument(List<String> path) {
|
||||||
|
if (path == null || path.size() < 2) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
String callerMethod = path.get(path.size() - 2);
|
||||||
|
String targetMethod = path.get(path.size() - 1);
|
||||||
|
String callerClass = callerMethod.substring(0, callerMethod.lastIndexOf('.'));
|
||||||
|
String callerMethodName = callerMethod.substring(callerMethod.lastIndexOf('.') + 1);
|
||||||
|
String targetMethodName = targetMethod.substring(targetMethod.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration callerType = context.getTypeDeclaration(callerClass);
|
||||||
|
if (callerType == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
MethodDeclaration callerDeclaration = context.findMethodDeclaration(callerType, callerMethodName, true);
|
||||||
|
if (callerDeclaration == null || callerDeclaration.getBody() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> resolved = new ArrayList<>();
|
||||||
|
final boolean[] expansionHint = {false};
|
||||||
|
callerDeclaration.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (!targetMethodName.equals(node.getName().getIdentifier()) || node.arguments().isEmpty()) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
Expression argument = (Expression) node.arguments().get(0);
|
||||||
|
List<Expression> candidateExpressions = new ArrayList<>();
|
||||||
|
if (argument instanceof SimpleName && variableTracer != null) {
|
||||||
|
candidateExpressions.addAll(variableTracer.traceVariableAll(argument));
|
||||||
|
}
|
||||||
|
if (candidateExpressions.isEmpty()) {
|
||||||
|
candidateExpressions.add(argument);
|
||||||
|
}
|
||||||
|
for (Expression candidateExpr : candidateExpressions) {
|
||||||
|
if (candidateExpr == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (candidateExpr.toString().contains(".toUpperCase()")) {
|
||||||
|
expansionHint[0] = true;
|
||||||
|
}
|
||||||
|
if (candidateExpr instanceof MethodInvocation localMethod
|
||||||
|
&& (localMethod.getExpression() == null
|
||||||
|
|| localMethod.getExpression() instanceof ThisExpression)) {
|
||||||
|
String resolvedPathMethod = findPathMethodByName(path, localMethod.getName().getIdentifier());
|
||||||
|
if (resolvedPathMethod == null) {
|
||||||
|
String fallbackOwner = findPrecedingConcretePathClass(path, callerClass);
|
||||||
|
if (fallbackOwner != null) {
|
||||||
|
TypeDeclaration fallbackType = context.getTypeDeclaration(fallbackOwner);
|
||||||
|
MethodDeclaration fallbackMethod = fallbackType != null
|
||||||
|
? context.findMethodDeclaration(
|
||||||
|
fallbackType, localMethod.getName().getIdentifier(), true)
|
||||||
|
: null;
|
||||||
|
if (fallbackMethod != null) {
|
||||||
|
resolvedPathMethod = fallbackOwner + "." + localMethod.getName().getIdentifier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resolvedPathMethod != null && resolvedPathMethod.contains(".")) {
|
||||||
|
String ownerFqn = resolvedPathMethod.substring(0, resolvedPathMethod.lastIndexOf('.'));
|
||||||
|
String methodName = resolvedPathMethod.substring(resolvedPathMethod.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration pathOwnerType = context.getTypeDeclaration(ownerFqn);
|
||||||
|
MethodDeclaration pathMethodDeclaration = pathOwnerType != null
|
||||||
|
? context.findMethodDeclaration(pathOwnerType, methodName, true)
|
||||||
|
: null;
|
||||||
|
if (hasEnumExpansionHint(pathMethodDeclaration)) {
|
||||||
|
expansionHint[0] = true;
|
||||||
|
String hintedEnumType = inferEnumTypeFromMethod(pathMethodDeclaration);
|
||||||
|
if (hintedEnumType != null) {
|
||||||
|
for (String expanded : expandDeclaredEnumValues(hintedEnumType)) {
|
||||||
|
addResolvedConstantToList(expanded, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String candidate : constantExtractor.resolveMethodReturnConstant(
|
||||||
|
ownerFqn, methodName, 0, new HashSet<>(), null)) {
|
||||||
|
addResolvedConstantToList(candidate, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String candidate : resolveLocalSwitchMethodConstants(callerType, localMethod)) {
|
||||||
|
addResolvedConstantToList(candidate, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String directResolved = constantResolver.resolve(candidateExpr, context);
|
||||||
|
if (directResolved != null) {
|
||||||
|
addResolvedConstantToList(directResolved, resolved);
|
||||||
|
}
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
constantExtractor.extractConstantsFromExpression(candidateExpr, constants);
|
||||||
|
for (String candidate : constants) {
|
||||||
|
addResolvedConstantToList(candidate, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<String> distinct = resolved.stream().distinct().collect(java.util.stream.Collectors.toList());
|
||||||
|
if (distinct.stream().noneMatch(AbstractCallGraphEngine.this::looksLikeEnumConstant)) {
|
||||||
|
for (int i = Math.max(0, path.size() - 2); i >= 0; i--) {
|
||||||
|
String pathMethod = path.get(i);
|
||||||
|
if (pathMethod == null || !pathMethod.contains(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String ownerFqn = pathMethod.substring(0, pathMethod.lastIndexOf('.'));
|
||||||
|
String methodName = pathMethod.substring(pathMethod.lastIndexOf('.') + 1);
|
||||||
|
for (String candidate : constantExtractor.resolveMethodReturnConstant(
|
||||||
|
ownerFqn, methodName, 0, new HashSet<>(), null)) {
|
||||||
|
addResolvedConstantToList(candidate, distinct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((expansionHint[0] || hasMultiClassPolymorphicPath(path))
|
||||||
|
&& distinct.stream().noneMatch(AbstractCallGraphEngine.this::looksLikeEnumConstant)) {
|
||||||
|
List<String> expanded = expandSingleSymbolicType(distinct);
|
||||||
|
if (expanded.isEmpty() && hasMultiClassPolymorphicPath(path)) {
|
||||||
|
expanded = inferEnumValuesFromPath(path);
|
||||||
|
}
|
||||||
|
if (!expanded.isEmpty()) {
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return distinct;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> inferEnumValuesFromPath(List<String> path) {
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
for (int i = path.size() - 1; i >= 0; i--) {
|
||||||
|
String methodFqn = path.get(i);
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String ownerFqn = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
TypeDeclaration ownerType = context.getTypeDeclaration(ownerFqn);
|
||||||
|
MethodDeclaration methodDeclaration = ownerType != null
|
||||||
|
? context.findMethodDeclaration(ownerType, methodName, true)
|
||||||
|
: null;
|
||||||
|
String enumType = inferEnumTypeFromMethod(methodDeclaration);
|
||||||
|
if (enumType == null && ownerType != null) {
|
||||||
|
for (MethodDeclaration candidateMethod : ownerType.getMethods()) {
|
||||||
|
enumType = inferEnumTypeFromMethod(candidateMethod);
|
||||||
|
if (enumType != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enumType != null) {
|
||||||
|
List<String> expanded = expandDeclaredEnumValues(enumType);
|
||||||
|
if (!expanded.isEmpty()) {
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String inferEnumTypeFromMethod(MethodDeclaration methodDeclaration) {
|
||||||
|
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String[] enumType = {null};
|
||||||
|
methodDeclaration.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
if (!"valueOf".equals(node.getName().getIdentifier()) || node.getExpression() == null) {
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
String receiver = node.getExpression().toString();
|
||||||
|
if (receiver != null && !receiver.isBlank() && Character.isUpperCase(receiver.charAt(0))) {
|
||||||
|
enumType[0] = receiver;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return enumType[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasMultiClassPolymorphicPath(List<String> path) {
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Set<String> classes = new LinkedHashSet<>();
|
||||||
|
for (String methodFqn : path) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
classes.add(methodFqn.substring(0, methodFqn.lastIndexOf('.')));
|
||||||
|
}
|
||||||
|
return classes.size() > 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findPrecedingConcretePathClass(List<String> path, String callerClass) {
|
||||||
|
if (path == null || callerClass == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = path.size() - 1; i >= 0; i--) {
|
||||||
|
String methodFqn = path.get(i);
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String classFqn = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
if (!callerClass.equals(classFqn)) {
|
||||||
|
return classFqn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasEnumExpansionHint(MethodDeclaration methodDeclaration) {
|
||||||
|
if (methodDeclaration == null || methodDeclaration.getBody() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final boolean[] hint = {false};
|
||||||
|
methodDeclaration.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(MethodInvocation node) {
|
||||||
|
String name = node.getName().getIdentifier();
|
||||||
|
if ("name".equals(name) || "toUpperCase".equals(name)) {
|
||||||
|
hint[0] = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.visit(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hint[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findPathMethodByName(List<String> path, String methodName) {
|
||||||
|
if (path == null || methodName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = path.size() - 1; i >= 0; i--) {
|
||||||
|
String methodFqn = path.get(i);
|
||||||
|
if (methodFqn != null && methodFqn.endsWith("." + methodName)) {
|
||||||
|
return methodFqn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> expandSingleSymbolicType(List<String> values) {
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Set<String> normalizedTypes = new LinkedHashSet<>();
|
||||||
|
String preferredType = null;
|
||||||
|
for (String value : values) {
|
||||||
|
if (value == null || !value.startsWith("<SYMBOLIC: ") || !value.endsWith(".*>")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String symbolicType = value.substring("<SYMBOLIC: ".length(), value.length() - 3).trim();
|
||||||
|
if (preferredType == null || symbolicType.contains(".")) {
|
||||||
|
preferredType = symbolicType;
|
||||||
|
}
|
||||||
|
String normalized = symbolicType.contains(".")
|
||||||
|
? symbolicType.substring(symbolicType.lastIndexOf('.') + 1)
|
||||||
|
: symbolicType;
|
||||||
|
normalizedTypes.add(normalized);
|
||||||
|
}
|
||||||
|
if (preferredType == null || normalizedTypes.size() != 1) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return expandDeclaredEnumValues(preferredType);
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveKeyedValueFromMapExpression(Expression mapExpr, String key) {
|
private String resolveKeyedValueFromMapExpression(Expression mapExpr, String key) {
|
||||||
if (mapExpr == null || key == null) {
|
if (mapExpr == null || key == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -2943,6 +3404,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (resolved.startsWith("<SYMBOLIC:") || resolved.contains("SYMBOLIC:") || resolved.endsWith(".*>")) {
|
||||||
|
if (!results.contains(resolved)) {
|
||||||
|
results.add(resolved);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
String parsed = constantExtractor.parseEnumSetElement(resolved);
|
String parsed = constantExtractor.parseEnumSetElement(resolved);
|
||||||
if (!results.contains(parsed)) {
|
if (!results.contains(parsed)) {
|
||||||
results.add(parsed);
|
results.add(parsed);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public class ConstantExtractor {
|
|||||||
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
public void extractConstantsFromExpression(Expression expr, List<String> constants) {
|
||||||
if (constantResolver != null) {
|
if (constantResolver != null) {
|
||||||
String resolved = constantResolver.resolve(expr, context);
|
String resolved = constantResolver.resolve(expr, context);
|
||||||
if (resolved != null) {
|
if (resolved != null && !shouldInspectExpressionStructure(expr, resolved)) {
|
||||||
addResolvedConstant(resolved, constants);
|
addResolvedConstant(resolved, constants);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,23 +66,7 @@ public class ConstantExtractor {
|
|||||||
} else if (expr instanceof Assignment assignment) {
|
} else if (expr instanceof Assignment assignment) {
|
||||||
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
extractConstantsFromExpression(assignment.getRightHandSide(), constants);
|
||||||
} else if (expr instanceof SwitchExpression se) {
|
} else if (expr instanceof SwitchExpression se) {
|
||||||
se.accept(new ASTVisitor() {
|
extractConstantsFromSwitchLike(se, constants);
|
||||||
@Override
|
|
||||||
public boolean visit(YieldStatement ys) {
|
|
||||||
extractConstantsFromExpression(ys.getExpression(), constants);
|
|
||||||
return super.visit(ys);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean visit(ExpressionStatement es) {
|
|
||||||
extractConstantsFromExpression(es.getExpression(), constants);
|
|
||||||
return super.visit(es);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public boolean visit(ReturnStatement rs) {
|
|
||||||
extractConstantsFromExpression(rs.getExpression(), constants);
|
|
||||||
return super.visit(rs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (expr instanceof ArrayAccess aa) {
|
} else if (expr instanceof ArrayAccess aa) {
|
||||||
extractConstantsFromExpression(aa.getArray(), constants);
|
extractConstantsFromExpression(aa.getArray(), constants);
|
||||||
} else if (expr instanceof ArrayCreation ac && ac.getInitializer() != null) {
|
} else if (expr instanceof ArrayCreation ac && ac.getInitializer() != null) {
|
||||||
@@ -236,6 +220,17 @@ public class ConstantExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldInspectExpressionStructure(Expression expr, String resolved) {
|
||||||
|
if (expr == null || resolved == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!(resolved.startsWith("<SYMBOLIC:") || resolved.contains("SYMBOLIC:") || resolved.endsWith(".*>"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return expr instanceof SwitchExpression
|
||||||
|
|| expr instanceof ConditionalExpression;
|
||||||
|
}
|
||||||
|
|
||||||
public void extractConstantsFromArgument(Expression argObj, List<String> constants) {
|
public void extractConstantsFromArgument(Expression argObj, List<String> constants) {
|
||||||
if (argObj instanceof ClassInstanceCreation innerCic) {
|
if (argObj instanceof ClassInstanceCreation innerCic) {
|
||||||
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
|
String typeName = click.kamil.springstatemachineexporter.ast.common.AstUtils.extractSimpleTypeName(innerCic.getType());
|
||||||
@@ -382,6 +377,11 @@ public class ConstantExtractor {
|
|||||||
if (td != null) {
|
if (td != null) {
|
||||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||||
if (md != null && md.getBody() != null) {
|
if (md != null && md.getBody() != null) {
|
||||||
|
for (Object stmtObj : md.getBody().statements()) {
|
||||||
|
if (stmtObj instanceof SwitchStatement switchStatement) {
|
||||||
|
extractConstantsFromSwitchStatement(switchStatement, constants);
|
||||||
|
}
|
||||||
|
}
|
||||||
md.getBody().accept(new ASTVisitor() {
|
md.getBody().accept(new ASTVisitor() {
|
||||||
@Override
|
@Override
|
||||||
public boolean visit(ReturnStatement node) {
|
public boolean visit(ReturnStatement node) {
|
||||||
@@ -441,7 +441,13 @@ public class ConstantExtractor {
|
|||||||
if (val.startsWith("ENUM_SET:")) {
|
if (val.startsWith("ENUM_SET:")) {
|
||||||
for (String eVal : val.substring(9).split(",")) {
|
for (String eVal : val.substring(9).split(",")) {
|
||||||
String parsed = parseEnumSetElement(eVal);
|
String parsed = parseEnumSetElement(eVal);
|
||||||
if (!constants.contains(parsed)) constants.add(parsed);
|
if (!constants.contains(parsed)) {
|
||||||
|
constants.add(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (val.startsWith("<SYMBOLIC:") || val.contains("SYMBOLIC:") || val.endsWith(".*>")) {
|
||||||
|
if (!constants.contains(val)) {
|
||||||
|
constants.add(val);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!constants.contains(val)) constants.add(val);
|
if (!constants.contains(val)) constants.add(val);
|
||||||
@@ -559,9 +565,41 @@ public class ConstantExtractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String parseEnumSetElement(String eVal) {
|
public String parseEnumSetElement(String eVal) {
|
||||||
|
if (eVal != null && (eVal.startsWith("<SYMBOLIC:") || eVal.contains("SYMBOLIC:") || eVal.endsWith(".*>"))) {
|
||||||
|
return eVal;
|
||||||
|
}
|
||||||
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
return eVal.contains(".") ? eVal.substring(eVal.lastIndexOf('.', eVal.lastIndexOf('.') - 1) + 1) : eVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void extractConstantsFromSwitchStatement(SwitchStatement switchStatement, List<String> constants) {
|
||||||
|
if (switchStatement == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
extractConstantsFromSwitchLike(switchStatement, constants);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractConstantsFromSwitchLike(ASTNode switchNode, List<String> constants) {
|
||||||
|
switchNode.accept(new ASTVisitor() {
|
||||||
|
@Override
|
||||||
|
public boolean visit(YieldStatement ys) {
|
||||||
|
extractConstantsFromExpression(ys.getExpression(), constants);
|
||||||
|
return super.visit(ys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ExpressionStatement es) {
|
||||||
|
extractConstantsFromExpression(es.getExpression(), constants);
|
||||||
|
return super.visit(es);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean visit(ReturnStatement rs) {
|
||||||
|
extractConstantsFromExpression(rs.getExpression(), constants);
|
||||||
|
return super.visit(rs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isKeyedLookupMethod(MethodInvocation mi) {
|
private boolean isKeyedLookupMethod(MethodInvocation mi) {
|
||||||
if (expressionAccessClassifier != null) {
|
if (expressionAccessClassifier != null) {
|
||||||
return expressionAccessClassifier.isKeyedLookup(mi, null);
|
return expressionAccessClassifier.isKeyedLookup(mi, null);
|
||||||
|
|||||||
@@ -101,15 +101,31 @@ public class EnrichmentService {
|
|||||||
Map<String, CallChain> byEntryPoint = new LinkedHashMap<>();
|
Map<String, CallChain> byEntryPoint = new LinkedHashMap<>();
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
for (CallChain chain : existing) {
|
for (CallChain chain : existing) {
|
||||||
byEntryPoint.putIfAbsent(entryPointKey(chain.getEntryPoint()), chain);
|
byEntryPoint.putIfAbsent(callChainKey(chain), chain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (CallChain chain : refreshed) {
|
for (CallChain chain : refreshed) {
|
||||||
byEntryPoint.put(entryPointKey(chain.getEntryPoint()), chain);
|
byEntryPoint.put(callChainKey(chain), chain);
|
||||||
}
|
}
|
||||||
return new ArrayList<>(byEntryPoint.values());
|
return new ArrayList<>(byEntryPoint.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String callChainKey(CallChain chain) {
|
||||||
|
if (chain == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String entryPointKey = entryPointKey(chain.getEntryPoint());
|
||||||
|
if (!entryPointKey.isBlank()) {
|
||||||
|
return entryPointKey;
|
||||||
|
}
|
||||||
|
if (chain.getTriggerPoint() != null
|
||||||
|
&& chain.getTriggerPoint().getClassName() != null
|
||||||
|
&& chain.getTriggerPoint().getMethodName() != null) {
|
||||||
|
return chain.getTriggerPoint().getClassName() + "#" + chain.getTriggerPoint().getMethodName();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private static String entryPointKey(EntryPoint entryPoint) {
|
private static String entryPointKey(EntryPoint entryPoint) {
|
||||||
if (entryPoint == null) {
|
if (entryPoint == null) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -266,6 +266,8 @@ public final class AnalysisCanonicalFormValidator {
|
|||||||
}
|
}
|
||||||
validateMatchedTransitionsWhenResolvable(
|
validateMatchedTransitionsWhenResolvable(
|
||||||
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
|
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
|
||||||
|
validateOverLinkedPolymorphicEvents(
|
||||||
|
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
|
||||||
}
|
}
|
||||||
if (chain.getMatchedTransitions() != null) {
|
if (chain.getMatchedTransitions() != null) {
|
||||||
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
|
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
|
||||||
@@ -339,6 +341,74 @@ public final class AnalysisCanonicalFormValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void validateOverLinkedPolymorphicEvents(
|
||||||
|
String chainPrefix,
|
||||||
|
CallChain chain,
|
||||||
|
TriggerPoint trigger,
|
||||||
|
List<Transition> transitions,
|
||||||
|
String eventTypeFqn,
|
||||||
|
List<Violation> violations) {
|
||||||
|
if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> transitionEvents =
|
||||||
|
MachineEnumCanonicalizer.polymorphicEventsFromTransitions(transitions, eventTypeFqn);
|
||||||
|
if (transitionEvents.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> extras = new ArrayList<>();
|
||||||
|
for (String polyEvent : trigger.getPolymorphicEvents()) {
|
||||||
|
if (!transitionEvents.contains(polyEvent)) {
|
||||||
|
extras.add(polyEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!extras.isEmpty()) {
|
||||||
|
violations.add(new Violation(
|
||||||
|
chainPrefix + ".triggerPoint.polymorphicEvents",
|
||||||
|
String.valueOf(trigger.getPolymorphicEvents().size()),
|
||||||
|
"subset of configured transition events; unexpected: " + extras));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int matchingTransitionCount = countMatchingConfiguredTransitions(
|
||||||
|
transitions, trigger.getPolymorphicEvents(), eventTypeFqn);
|
||||||
|
if (matchingTransitionCount > 0
|
||||||
|
&& chain.getMatchedTransitions() != null
|
||||||
|
&& chain.getMatchedTransitions().size() > matchingTransitionCount) {
|
||||||
|
violations.add(new Violation(
|
||||||
|
chainPrefix + ".matchedTransitions",
|
||||||
|
String.valueOf(chain.getMatchedTransitions().size()),
|
||||||
|
"at most " + matchingTransitionCount + " for configured transitions"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countMatchingConfiguredTransitions(
|
||||||
|
List<Transition> transitions,
|
||||||
|
List<String> polymorphicEvents,
|
||||||
|
String eventTypeFqn) {
|
||||||
|
if (transitions == null || transitions.isEmpty() || polymorphicEvents == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
for (Transition transition : transitions) {
|
||||||
|
if (transition.getEvent() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String smEvent = transition.getEvent().fullIdentifier() != null
|
||||||
|
? transition.getEvent().fullIdentifier()
|
||||||
|
: transition.getEvent().rawName();
|
||||||
|
for (String polyEvent : polymorphicEvents) {
|
||||||
|
if (eventsMatch(polyEvent, smEvent, eventTypeFqn)) {
|
||||||
|
count++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
private static void validateMatchedTransitionsWhenResolvable(
|
private static void validateMatchedTransitionsWhenResolvable(
|
||||||
String chainPrefix,
|
String chainPrefix,
|
||||||
CallChain chain,
|
CallChain chain,
|
||||||
@@ -346,9 +416,6 @@ public final class AnalysisCanonicalFormValidator {
|
|||||||
List<Transition> transitions,
|
List<Transition> transitions,
|
||||||
String eventTypeFqn,
|
String eventTypeFqn,
|
||||||
List<Violation> violations) {
|
List<Violation> violations) {
|
||||||
if (trigger.isExternal() || trigger.isAmbiguous()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (LifecycleTriggerMarkers.isLifecycle(trigger.getEvent())) {
|
if (LifecycleTriggerMarkers.isLifecycle(trigger.getEvent())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,8 +201,10 @@ public class ExportService {
|
|||||||
&& click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
&& click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory
|
||||||
.contextContainsAnalysisSources(result, context)
|
.contextContainsAnalysisSources(result, context)
|
||||||
&& result.getMetadata() != null
|
&& result.getMetadata() != null
|
||||||
&& result.getMetadata().getEntryPoints() != null
|
&& ((result.getMetadata().getEntryPoints() != null
|
||||||
&& !result.getMetadata().getEntryPoints().isEmpty()) {
|
&& !result.getMetadata().getEntryPoints().isEmpty())
|
||||||
|
|| (result.getMetadata().getCallChains() != null
|
||||||
|
&& !result.getMetadata().getCallChains().isEmpty()))) {
|
||||||
JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, sourceRoot);
|
JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, sourceRoot);
|
||||||
enrichmentService.refreshCallChainsAndRelink(result, context, intelligence);
|
enrichmentService.refreshCallChainsAndRelink(result, context, intelligence);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -101,11 +101,22 @@ class StrictFqnMatchingEngineTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMatchWildcards() {
|
void shouldNotMatchWildcardAgainstEnumTransitionWithoutPolymorphicEvents() {
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder().event("event").build();
|
TriggerPoint triggerPoint = TriggerPoint.builder().event("event").build();
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotMatchDynamicMethodCallWithoutResolvedPolymorphicEvents() {
|
||||||
|
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||||
|
.event("event.getPayload()")
|
||||||
|
.eventTypeFqn("com.example.OrderEvents")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -187,13 +198,25 @@ class StrictFqnMatchingEngineTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMatchDynamicVariableWithMatchingTypeFqn() {
|
void shouldNotMatchDynamicVariableWithoutResolvedPolymorphicEvents() {
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||||
.event("myCustomEvt") // Not "event", "e", etc., but a custom variable name
|
.event("myCustomEvt")
|
||||||
.eventTypeFqn("com.example.OrderEvents")
|
.eventTypeFqn("com.example.OrderEvents")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchDynamicVariableWhenPolymorphicEventsAreResolved() {
|
||||||
|
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||||
|
.event("myCustomEvt")
|
||||||
|
.eventTypeFqn("com.example.OrderEvents")
|
||||||
|
.polymorphicEvents(List.of("com.example.OrderEvents.PAY"))
|
||||||
|
.build();
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,11 +232,22 @@ class StrictFqnMatchingEngineTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMatchDynamicVariableWithUnknownTypeFqn() {
|
void shouldNotMatchDynamicVariableWithUnknownTypeFqnWhenSmEventIsEnum() {
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||||
.event("myCustomEvt")
|
.event("myCustomEvt")
|
||||||
.eventTypeFqn(null) // Unknown type, fallback to true
|
.eventTypeFqn(null)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMatchDynamicVariableWithUnknownTypeFqnForStringEvents() {
|
||||||
|
Event smEvent = Event.of("PAY", "PAY");
|
||||||
|
TriggerPoint triggerPoint = TriggerPoint.builder()
|
||||||
|
.event("myCustomEvt")
|
||||||
|
.eventTypeFqn(null)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
||||||
@@ -250,15 +284,4 @@ class StrictFqnMatchingEngineTest {
|
|||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldMatchDynamicMethodCallAsVariable() {
|
|
||||||
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
|
|
||||||
TriggerPoint triggerPoint = TriggerPoint.builder()
|
|
||||||
.event("event.getPayload()") // Dynamic method call
|
|
||||||
.eventTypeFqn("com.example.OrderEvents")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,37 @@ class EnumMemberPredicatePolymorphicInferenceTest {
|
|||||||
assertThat(filtered).containsExactly("com.example.order.OrderCommand.PAY");
|
assertThat(filtered).containsExactly("com.example.order.OrderCommand.PAY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFilterEnumConstantsBySwitchBasedPredicate(@TempDir Path tempDir) throws Exception {
|
||||||
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderCommand {
|
||||||
|
PAY,
|
||||||
|
SHIP,
|
||||||
|
META;
|
||||||
|
public boolean isEvent() {
|
||||||
|
return switch (this) {
|
||||||
|
case PAY, SHIP -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||||
|
List.of(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.SHIP",
|
||||||
|
"com.example.order.OrderCommand.META"),
|
||||||
|
"eventType.isEvent()",
|
||||||
|
"com.example.order.OrderCommand",
|
||||||
|
context);
|
||||||
|
|
||||||
|
assertThat(filtered).containsExactly(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.SHIP");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldPreferTransitionEventsOverFullEnumOnDynamicTrigger(@TempDir Path tempDir) throws Exception {
|
void shouldPreferTransitionEventsOverFullEnumOnDynamicTrigger(@TempDir Path tempDir) throws Exception {
|
||||||
CodebaseContext context = scanEnumProject(tempDir, """
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
@@ -214,6 +245,196 @@ class EnumMemberPredicatePolymorphicInferenceTest {
|
|||||||
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
|
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNarrowEqualSizePolyListWithNonTransitionConstantsWhenConstraintIsNull(@TempDir Path tempDir) throws Exception {
|
||||||
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderCommand {
|
||||||
|
PAY,
|
||||||
|
SHIP,
|
||||||
|
META;
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.event("event")
|
||||||
|
.polymorphicEvents(List.of(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.SHIP",
|
||||||
|
"com.example.order.OrderCommand.META"))
|
||||||
|
.constraint(null)
|
||||||
|
.ambiguous(true)
|
||||||
|
.build();
|
||||||
|
MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
|
||||||
|
List<Transition> transitions = List.of(
|
||||||
|
transition("OrderCommand.PAY", "NEW", "PAID"),
|
||||||
|
transition("OrderCommand.SHIP", "PAID", "SHIPPED"));
|
||||||
|
|
||||||
|
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||||
|
trigger, types, context, transitions, true);
|
||||||
|
|
||||||
|
assertThat(enriched.getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.SHIP");
|
||||||
|
assertThat(enriched.getPolymorphicEvents())
|
||||||
|
.doesNotContain("com.example.order.OrderCommand.META");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFallBackToTransitionEventsWhenPredicateEvaluationIsInconclusive(@TempDir Path tempDir) throws Exception {
|
||||||
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderCommand {
|
||||||
|
PAY,
|
||||||
|
SHIP,
|
||||||
|
META;
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
TriggerPoint trigger = TriggerPoint.builder()
|
||||||
|
.event("OrderCommand.valueOf(name)")
|
||||||
|
.constraint("eventType.isEvent()")
|
||||||
|
.build();
|
||||||
|
MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
|
||||||
|
List<Transition> transitions = List.of(
|
||||||
|
transition("OrderCommand.PAY", "NEW", "PAID"),
|
||||||
|
transition("OrderCommand.SHIP", "PAID", "SHIPPED"));
|
||||||
|
|
||||||
|
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
|
||||||
|
trigger, types, context, transitions, true);
|
||||||
|
|
||||||
|
assertThat(enriched.getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.SHIP");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExcludeConstantsWhenPredicateMethodCannotBeEvaluated(@TempDir Path tempDir) throws Exception {
|
||||||
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderCommand {
|
||||||
|
PAY,
|
||||||
|
META;
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||||
|
List.of(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.META"),
|
||||||
|
"eventType.isEvent()",
|
||||||
|
"com.example.order.OrderCommand",
|
||||||
|
context);
|
||||||
|
|
||||||
|
assertThat(filtered).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEvaluateInterfaceDefaultPredicateDelegation(@TempDir Path tempDir) throws Exception {
|
||||||
|
Path javaRoot = tempDir.resolve("src/main/java/com/example");
|
||||||
|
Files.createDirectories(javaRoot.resolve("core"));
|
||||||
|
Files.createDirectories(javaRoot.resolve("order"));
|
||||||
|
Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
|
||||||
|
Files.writeString(javaRoot.resolve("core/TriggerClassifier.java"), """
|
||||||
|
package com.example.core;
|
||||||
|
public interface TriggerClassifier {
|
||||||
|
default boolean isEvent() { return isTrigger(); }
|
||||||
|
boolean isTrigger();
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(javaRoot.resolve("order/OrderEvent.java"), """
|
||||||
|
package com.example.order;
|
||||||
|
import com.example.core.TriggerClassifier;
|
||||||
|
public enum OrderEvent implements TriggerClassifier {
|
||||||
|
PAY(true),
|
||||||
|
LOG(false);
|
||||||
|
private final boolean trigger;
|
||||||
|
OrderEvent(boolean trigger) { this.trigger = trigger; }
|
||||||
|
@Override
|
||||||
|
public boolean isTrigger() { return this.trigger; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||||
|
List.of("com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.LOG"),
|
||||||
|
"eventType.isEvent()",
|
||||||
|
"com.example.order.OrderEvent",
|
||||||
|
context);
|
||||||
|
|
||||||
|
assertThat(filtered).containsExactly("com.example.order.OrderEvent.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInferPolymorphicEventsForNullPolyExternalTriggerWithConstraint(@TempDir Path tempDir) throws Exception {
|
||||||
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderCommand {
|
||||||
|
PAY(true),
|
||||||
|
SHIP(true),
|
||||||
|
META(false);
|
||||||
|
private final boolean event;
|
||||||
|
OrderCommand(boolean event) { this.event = event; }
|
||||||
|
public boolean isEvent() { return event; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.event("eventType")
|
||||||
|
.constraint("eventType.isEvent()")
|
||||||
|
.external(true)
|
||||||
|
.ambiguous(true)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Transition pay = transition("OrderCommand.PAY", "NEW", "PAID");
|
||||||
|
Transition ship = transition("OrderCommand.SHIP", "PAID", "SHIPPED");
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.eventTypeFqn("com.example.order.OrderCommand")
|
||||||
|
.transitions(List.of(pay, ship))
|
||||||
|
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
new TransitionLinkerEnricher().enrich(result, context, null);
|
||||||
|
|
||||||
|
TriggerPoint tp = result.getMetadata().getCallChains().get(0).getTriggerPoint();
|
||||||
|
assertThat(tp.getPolymorphicEvents())
|
||||||
|
.containsExactlyInAnyOrder(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.SHIP");
|
||||||
|
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEvaluateDirectFieldReturnInPredicateMethod(@TempDir Path tempDir) throws Exception {
|
||||||
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderCommand {
|
||||||
|
PAY(true),
|
||||||
|
META(false);
|
||||||
|
private final boolean event;
|
||||||
|
OrderCommand(boolean event) { this.event = event; }
|
||||||
|
public boolean isEvent() { return event; }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||||
|
List.of(
|
||||||
|
"com.example.order.OrderCommand.PAY",
|
||||||
|
"com.example.order.OrderCommand.META"),
|
||||||
|
"eventType.isEvent()",
|
||||||
|
"com.example.order.OrderCommand",
|
||||||
|
context);
|
||||||
|
|
||||||
|
assertThat(filtered).containsExactly("com.example.order.OrderCommand.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotMatchNonEventConstantsWhenPolyListWasOverBroad(@TempDir Path tempDir) throws Exception {
|
void shouldNotMatchNonEventConstantsWhenPolyListWasOverBroad(@TempDir Path tempDir) throws Exception {
|
||||||
CodebaseContext context = scanEnumProject(tempDir, """
|
CodebaseContext context = scanEnumProject(tempDir, """
|
||||||
|
|||||||
@@ -474,6 +474,54 @@ class CentralDispatcherResolutionTest {
|
|||||||
"OrderEvent.SHIP");
|
"OrderEvent.SHIP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void staticRoutingHelperShouldForwardLiteralsToCentralDispatcher(@TempDir Path tempDir) throws Exception {
|
||||||
|
String source = """
|
||||||
|
package com.example;
|
||||||
|
public class ApiController {
|
||||||
|
CommandGateway gateway;
|
||||||
|
public void pay() { gateway.routeAndPay(); }
|
||||||
|
public void ship() { gateway.routeAndShip(); }
|
||||||
|
}
|
||||||
|
class CommandGateway {
|
||||||
|
CentralDispatcher dispatcher;
|
||||||
|
void routeAndPay() { dispatcher.dispatch("ORDER", OrderRoutingHelper.payAction()); }
|
||||||
|
void routeAndShip() { dispatcher.dispatch("ORDER", OrderRoutingHelper.shipAction()); }
|
||||||
|
}
|
||||||
|
class OrderRoutingHelper {
|
||||||
|
static String payAction() { return "PAY"; }
|
||||||
|
static String shipAction() { return "SHIP"; }
|
||||||
|
}
|
||||||
|
class CentralDispatcher {
|
||||||
|
void dispatch(String domain, String action) {
|
||||||
|
StateMachine sm = new StateMachine();
|
||||||
|
sm.sendEvent(OrderEvent.valueOf(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enum OrderEvent { PAY, SHIP }
|
||||||
|
class StateMachine { public void sendEvent(OrderEvent e) {} }
|
||||||
|
class OrderStateMachineConfig {}
|
||||||
|
""";
|
||||||
|
CodebaseContext context = scanSource(source, tempDir);
|
||||||
|
|
||||||
|
CallChain payChain = resolveChain(context, CONTROLLER, "pay", STATE_MACHINE);
|
||||||
|
CallChain shipChain = resolveChain(context, CONTROLLER, "ship", STATE_MACHINE);
|
||||||
|
|
||||||
|
assertPolyEvents(payChain, "OrderEvent.PAY");
|
||||||
|
assertPolyEvents(shipChain, "OrderEvent.SHIP");
|
||||||
|
|
||||||
|
assertLinkedEvent(
|
||||||
|
linkEndpointToTransition(payChain, MACHINE_CONFIG,
|
||||||
|
transition("NEW", "PAID", "OrderEvent.PAY"),
|
||||||
|
transition("PAID", "SHIPPED", "OrderEvent.SHIP")),
|
||||||
|
"OrderEvent.PAY");
|
||||||
|
assertLinkedEvent(
|
||||||
|
linkEndpointToTransition(shipChain, MACHINE_CONFIG,
|
||||||
|
transition("NEW", "PAID", "OrderEvent.PAY"),
|
||||||
|
transition("PAID", "SHIPPED", "OrderEvent.SHIP")),
|
||||||
|
"OrderEvent.SHIP");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tier 1 — DTO field set via constructor in controller, read via getter in gateway.
|
* Tier 1 — DTO field set via constructor in controller, read via getter in gateway.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
||||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||||
@@ -145,6 +146,60 @@ class JsonRoundTripCanonicalizationTest {
|
|||||||
.isEqualTo("com.example.order.OrderState.NEW");
|
.isEqualTo("com.example.order.OrderState.NEW");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRefreshCallChainsWhenJsonLacksTopLevelEntryPoints(@TempDir Path tempDir) throws Exception {
|
||||||
|
writeSampleProject(tempDir);
|
||||||
|
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.entryPoint(EntryPoint.builder()
|
||||||
|
.type(EntryPoint.Type.CUSTOM)
|
||||||
|
.className("com.example.web.OrderController")
|
||||||
|
.methodName("pay")
|
||||||
|
.sourceFile("OrderController.java")
|
||||||
|
.build())
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.event("event")
|
||||||
|
.className("com.example.web.OrderStateMachine")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.sourceFile("OrderStateMachine.java")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
AnalysisResult shortForm = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.stateTypeFqn("com.example.order.OrderState")
|
||||||
|
.eventTypeFqn("com.example.order.OrderEvent")
|
||||||
|
.transitions(List.of(shortTransition(
|
||||||
|
"OrderEvent.PAY",
|
||||||
|
"OrderState.NEW",
|
||||||
|
"OrderState.PAID")))
|
||||||
|
.metadata(CodebaseMetadata.builder()
|
||||||
|
.callChains(List.of(chain))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Path jsonFile = tempDir.resolve("machine.json");
|
||||||
|
Files.writeString(jsonFile, new JsonExporter().export(shortForm, ExportOptions.builder().build()));
|
||||||
|
|
||||||
|
Path outputDir = tempDir.resolve("out");
|
||||||
|
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||||
|
exportService.runJsonExporter(jsonFile, outputDir, List.of("json"));
|
||||||
|
|
||||||
|
AnalysisResult roundTripped = new JsonImportService().importAnalysisResult(
|
||||||
|
outputDir.resolve("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.resolve("com.example.config.OrderStateMachineConfiguration.json"));
|
||||||
|
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains()).hasSize(1);
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getEntryPoint()).isNotNull();
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getEntryPoint().getClassName())
|
||||||
|
.isEqualTo("com.example.web.OrderController");
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getEntryPoint().getMethodName())
|
||||||
|
.isEqualTo("pay");
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
|
||||||
|
assertThat(roundTripped.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
|
||||||
|
.isEqualTo("com.example.order.OrderEvent.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDedupeStatesByFullIdentifierAfterPropertyResolution(@TempDir Path tempDir) throws Exception {
|
void shouldDedupeStatesByFullIdentifierAfterPropertyResolution(@TempDir Path tempDir) throws Exception {
|
||||||
writeSampleProject(tempDir);
|
writeSampleProject(tempDir);
|
||||||
@@ -189,8 +244,10 @@ class JsonRoundTripCanonicalizationTest {
|
|||||||
Path javaRoot = projectRoot.resolve("src/main/java");
|
Path javaRoot = projectRoot.resolve("src/main/java");
|
||||||
Path orderPkg = javaRoot.resolve("com/example/order");
|
Path orderPkg = javaRoot.resolve("com/example/order");
|
||||||
Path configPkg = javaRoot.resolve("com/example/config");
|
Path configPkg = javaRoot.resolve("com/example/config");
|
||||||
|
Path webPkg = javaRoot.resolve("com/example/web");
|
||||||
Files.createDirectories(orderPkg);
|
Files.createDirectories(orderPkg);
|
||||||
Files.createDirectories(configPkg);
|
Files.createDirectories(configPkg);
|
||||||
|
Files.createDirectories(webPkg);
|
||||||
Files.writeString(projectRoot.resolve("build.gradle"), "plugins { id 'java' }");
|
Files.writeString(projectRoot.resolve("build.gradle"), "plugins { id 'java' }");
|
||||||
Files.writeString(orderPkg.resolve("OrderState.java"),
|
Files.writeString(orderPkg.resolve("OrderState.java"),
|
||||||
"package com.example.order; public enum OrderState { NEW, PAID }");
|
"package com.example.order; public enum OrderState { NEW, PAID }");
|
||||||
@@ -206,5 +263,25 @@ class JsonRoundTripCanonicalizationTest {
|
|||||||
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
|
||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
|
Files.writeString(webPkg.resolve("OrderController.java"),
|
||||||
|
"""
|
||||||
|
package com.example.web;
|
||||||
|
import com.example.order.OrderEvent;
|
||||||
|
public class OrderController {
|
||||||
|
private final OrderStateMachine stateMachine = new OrderStateMachine();
|
||||||
|
public void pay() {
|
||||||
|
stateMachine.sendEvent(OrderEvent.PAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(webPkg.resolve("OrderStateMachine.java"),
|
||||||
|
"""
|
||||||
|
package com.example.web;
|
||||||
|
import com.example.order.OrderEvent;
|
||||||
|
public class OrderStateMachine {
|
||||||
|
public void sendEvent(OrderEvent event) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,6 +286,42 @@ class AnalysisCanonicalFormValidatorTest {
|
|||||||
.contains("metadata.callChains[0].matchedTransitions");
|
.contains("metadata.callChains[0].matchedTransitions");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWhenExternalAmbiguousTriggerHasResolvablePolymorphicEventsButNoMatchedTransitions() {
|
||||||
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
.name("com.example.config.OrderStateMachineConfiguration")
|
||||||
|
.stateTypeFqn("com.example.order.OrderState")
|
||||||
|
.eventTypeFqn("com.example.order.OrderEvent")
|
||||||
|
.transitions(List.of(transition(
|
||||||
|
"com.example.order.OrderEvent.PAY",
|
||||||
|
"com.example.order.OrderState.NEW",
|
||||||
|
"com.example.order.OrderState.PAID")))
|
||||||
|
.metadata(CodebaseMetadata.builder()
|
||||||
|
.callChains(List.of(CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.event("eventType")
|
||||||
|
.className("com.example.web.OrderController")
|
||||||
|
.methodName("dispatch")
|
||||||
|
.sourceFile("OrderController.java")
|
||||||
|
.polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
|
||||||
|
.external(true)
|
||||||
|
.ambiguous(true)
|
||||||
|
.build())
|
||||||
|
.build()))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<AnalysisCanonicalFormValidator.Violation> violations =
|
||||||
|
AnalysisCanonicalFormValidator.validateWithMachineTypes(
|
||||||
|
result,
|
||||||
|
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
|
||||||
|
"com.example.order.OrderState", "com.example.order.OrderEvent"));
|
||||||
|
|
||||||
|
assertThat(violations)
|
||||||
|
.extracting(AnalysisCanonicalFormValidator.Violation::path)
|
||||||
|
.contains("metadata.callChains[0].matchedTransitions");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAllowSymbolicPolymorphicEventsWithoutMatchedTransitions() {
|
void shouldAllowSymbolicPolymorphicEventsWithoutMatchedTransitions() {
|
||||||
AnalysisResult result = AnalysisResult.builder()
|
AnalysisResult result = AnalysisResult.builder()
|
||||||
|
|||||||
Reference in New Issue
Block a user