3 Commits

Author SHA1 Message Date
7807df57ca 2 2026-07-06 17:48:21 +02:00
82719489aa 1 2026-07-06 17:43:34 +02:00
c390f02389 heuristic code 2026-07-05 22:00:26 +02:00
6 changed files with 136 additions and 14 deletions

BIN
TestHeuristic.class Normal file

Binary file not shown.

17
TestHeuristic.java Normal file
View File

@@ -0,0 +1,17 @@
import java.util.*;
public class TestHeuristic {
public static void main(String[] args) {
System.out.println(isFullyQualifiedMethod("TransformerImpl.transform"));
System.out.println(isFullyQualifiedMethod("Transformer.transform"));
}
private static boolean isFullyQualifiedMethod(String methodFqn) {
if (methodFqn == null || !methodFqn.contains(".")) return false;
int lastDot = methodFqn.lastIndexOf('.');
String classPart = methodFqn.substring(0, lastDot);
if (classPart.contains(".")) return true;
if (!classPart.isEmpty() && Character.isUpperCase(classPart.charAt(0))) return true;
return false;
}
}

View File

@@ -194,20 +194,38 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
} }
} }
private final java.util.Map<String, String> simplifyCache = new java.util.HashMap<>();
private final java.util.Map<String, Boolean> guardedCache = new java.util.HashMap<>();
private static final java.util.regex.Pattern SUFFIX_PATTERN = java.util.regex.Pattern.compile("(?i)(.+)(Event|Action|Transition|Command)(s)?$");
private static final java.util.regex.Pattern FQN_PATTERN = java.util.regex.Pattern.compile("^.*\\.([A-Z0-9_]+)$");
private String simplify(String name) { private String simplify(String name) {
if (name == null) return null; if (name == null) return null;
if (simplifyCache.containsKey(name)) return simplifyCache.get(name);
// Strip common suffixes // Strip common suffixes
String simplified = name.replaceAll("(?i)(.+)(Event|Action|Transition|Command)(s)?$", "$1"); String simplified = SUFFIX_PATTERN.matcher(name).replaceAll("$1");
if (simplified.isEmpty()) { if (simplified.isEmpty()) {
simplified = name; simplified = name;
} }
// Simplify full identifiers to just the last part (enum name) // Simplify full identifiers to just the last part (enum name)
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1"); simplified = FQN_PATTERN.matcher(simplified).replaceAll("$1");
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify
simplifyCache.put(name, simplified);
return simplified; return simplified;
} }
private boolean isGuardedContains(String smEvent, String triggerEvent) { private boolean isGuardedContains(String smEvent, String triggerEvent) {
if (smEvent == null || triggerEvent == null) return false;
String cacheKey = smEvent + "|" + triggerEvent;
if (guardedCache.containsKey(cacheKey)) return guardedCache.get(cacheKey);
boolean result = calculateGuardedContains(smEvent, triggerEvent);
guardedCache.put(cacheKey, result);
return result;
}
private boolean calculateGuardedContains(String smEvent, String triggerEvent) {
// Only fire if it looks like a simple identifier (no spaces, no parens) // Only fire if it looks like a simple identifier (no spaces, no parens)
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event" // Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") || if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
@@ -225,7 +243,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
// Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators // Guard: If the matching part is very short (< 4 chars), it must be bounded by word separators
// to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE") // to avoid matching completely unrelated words (e.g., "PAY" matching "PAYMENT" or "E" matching "UPDATE")
if (shorter.length() < 4) { if (shorter.length() < 4) {
return longer.matches(".*(^|_|-)" + shorter + "(_|-|$).*"); return longer.matches(".*(^|_|-)" + java.util.regex.Pattern.quote(shorter) + "(_|-|$).*");
} }
// For longer strings, a straight contains is usually safe enough // For longer strings, a straight contains is usually safe enough
return true; return true;

View File

@@ -87,8 +87,22 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
return isGuardedContains(smEvent, triggerEvent); return isGuardedContains(smEvent, triggerEvent);
} }
private final java.util.Map<String, String> simplifyCache = new java.util.HashMap<>();
private final java.util.Map<String, Boolean> guardedCache = new java.util.HashMap<>();
private static final java.util.regex.Pattern SUFFIX_PATTERN = java.util.regex.Pattern.compile("(?i)(.+)(Event|Action|Transition|Command)(s)?$");
private static final java.util.regex.Pattern FQN_PATTERN = java.util.regex.Pattern.compile("^.*\\.([A-Z0-9_]+)$");
private boolean isGuardedContains(String smEvent, String triggerEvent) { private boolean isGuardedContains(String smEvent, String triggerEvent) {
if (smEvent == null || triggerEvent == null) return false; if (smEvent == null || triggerEvent == null) return false;
String cacheKey = smEvent + "|" + triggerEvent;
if (guardedCache.containsKey(cacheKey)) return guardedCache.get(cacheKey);
boolean result = calculateGuardedContains(smEvent, triggerEvent);
guardedCache.put(cacheKey, result);
return result;
}
private boolean calculateGuardedContains(String smEvent, String triggerEvent) {
// Only fire if it looks like a simple identifier (no spaces, no parens) // Only fire if it looks like a simple identifier (no spaces, no parens)
// Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event" // Prevents unresolved AST nodes (e.g. "event.getPayload()") from accidentally matching "Event"
if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") || if (smEvent.contains(" ") || smEvent.contains("(") || smEvent.contains(")") ||
@@ -137,11 +151,15 @@ public class HeuristicEventMatchingEngine implements EventMatchingEngine {
private String simplify(String name) { private String simplify(String name) {
if (name == null) return null; if (name == null) return null;
String simplified = name.replaceAll("(?i)(.+)(Event|Action|Transition|Command)(s)?$", "$1"); if (simplifyCache.containsKey(name)) return simplifyCache.get(name);
String simplified = SUFFIX_PATTERN.matcher(name).replaceAll("$1");
if (simplified.isEmpty()) { if (simplified.isEmpty()) {
simplified = name; simplified = name;
} }
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1"); simplified = FQN_PATTERN.matcher(simplified).replaceAll("$1");
simplifyCache.put(name, simplified);
return simplified; return simplified;
} }
} }

View File

@@ -9,6 +9,7 @@ import java.util.*;
@Slf4j @Slf4j
public class CallGraphPathFinder { public class CallGraphPathFinder {
private final CodebaseContext context; private final CodebaseContext context;
private final Map<String, Boolean> heuristicCache = new HashMap<>();
public CallGraphPathFinder(CodebaseContext context) { public CallGraphPathFinder(CodebaseContext context) {
this.context = context; this.context = context;
@@ -49,15 +50,25 @@ public class CallGraphPathFinder {
} }
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) { public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
return findAllPaths(start, target, graph, visited, new HashSet<>(), new boolean[]{false});
}
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited, Set<String> unreachable, boolean[] parentHitCycle) {
List<List<String>> result = new ArrayList<>(); List<List<String>> result = new ArrayList<>();
if (unreachable.contains(start)) {
return result;
}
if (start.equals(target)) { if (start.equals(target)) {
result.add(new ArrayList<>(List.of(start))); result.add(new ArrayList<>(List.of(start)));
return result; return result;
} }
if (!visited.add(start)) { if (!visited.add(start)) {
parentHitCycle[0] = true;
return result; return result;
} }
boolean[] localHitCycle = new boolean[]{false};
List<CallEdge> neighbors = graph.get(start); List<CallEdge> neighbors = graph.get(start);
if (neighbors != null) { if (neighbors != null) {
for (CallEdge edge : neighbors) { for (CallEdge edge : neighbors) {
@@ -67,7 +78,7 @@ public class CallGraphPathFinder {
List<String> path = new ArrayList<>(List.of(start, target)); List<String> path = new ArrayList<>(List.of(start, target));
result.add(path); result.add(path);
} else { } else {
List<List<String>> subPaths = findAllPaths(neighbor, target, graph, visited); List<List<String>> subPaths = findAllPaths(neighbor, target, graph, visited, unreachable, localHitCycle);
for (List<String> subPath : subPaths) { for (List<String> subPath : subPaths) {
List<String> path = new ArrayList<>(); List<String> path = new ArrayList<>();
path.add(start); path.add(start);
@@ -91,7 +102,7 @@ public class CallGraphPathFinder {
if (superclass != null) { if (superclass != null) {
String superMethod = superclass + "." + methodName; String superMethod = superclass + "." + methodName;
if (graph.containsKey(superMethod)) { if (graph.containsKey(superMethod)) {
List<List<String>> superPaths = findAllPaths(superMethod, target, graph, visited); List<List<String>> superPaths = findAllPaths(superMethod, target, graph, visited, unreachable, localHitCycle);
for (List<String> superPath : superPaths) { for (List<String> superPath : superPaths) {
List<String> path = new ArrayList<>(); List<String> path = new ArrayList<>();
path.add(start); path.add(start);
@@ -115,7 +126,7 @@ public class CallGraphPathFinder {
for (String impl : impls) { for (String impl : impls) {
String implMethod = impl + "." + methodName; String implMethod = impl + "." + methodName;
if (graph.containsKey(implMethod)) { if (graph.containsKey(implMethod)) {
List<List<String>> implPaths = findAllPaths(implMethod, target, graph, visited); List<List<String>> implPaths = findAllPaths(implMethod, target, graph, visited, unreachable, localHitCycle);
for (List<String> implPath : implPaths) { for (List<String> implPath : implPaths) {
List<String> path = new ArrayList<>(); List<String> path = new ArrayList<>();
path.add(start); path.add(start);
@@ -127,6 +138,13 @@ public class CallGraphPathFinder {
} }
} }
visited.remove(start); visited.remove(start);
if (localHitCycle[0]) {
parentHitCycle[0] = true;
} else if (result.isEmpty()) {
unreachable.add(start);
}
return result; return result;
} }
@@ -151,6 +169,19 @@ public class CallGraphPathFinder {
if (neighbor == null || target == null) return false; if (neighbor == null || target == null) return false;
if (neighbor.equals(target)) return true; if (neighbor.equals(target)) return true;
String cacheKey = neighbor + "|" + target;
Boolean cached = heuristicCache.get(cacheKey);
if (cached != null) return cached;
boolean result = calculateHeuristicMatch(neighbor, target);
heuristicCache.put(cacheKey, result);
return result;
}
private boolean calculateHeuristicMatch(String neighbor, String target) {
System.out.println("calculateHeuristicMatch: neighbor=" + neighbor + ", target=" + target);
if (neighbor.equals(target)) return true;
if (isFullyQualifiedMethod(neighbor) && isFullyQualifiedMethod(target)) { if (isFullyQualifiedMethod(neighbor) && isFullyQualifiedMethod(target)) {
int lastDotNeighbor = neighbor.lastIndexOf('.'); int lastDotNeighbor = neighbor.lastIndexOf('.');
int lastDotTarget = target.lastIndexOf('.'); int lastDotTarget = target.lastIndexOf('.');
@@ -172,7 +203,6 @@ public class CallGraphPathFinder {
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget; String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
if (simpleClassNeighbor.equals(simpleClassTarget)) return true; if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
if (simpleClassNeighbor.contains(simpleClassTarget) || simpleClassTarget.contains(simpleClassNeighbor)) return true;
List<String> impls = context.getImplementations(classTarget); List<String> impls = context.getImplementations(classTarget);
if (impls != null) { if (impls != null) {
@@ -192,7 +222,21 @@ public class CallGraphPathFinder {
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target; String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor; String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
return simpleNeighbor.equals(simpleTarget); if (!simpleNeighbor.equals(simpleTarget)) return false;
String classNeighbor = neighbor.contains(".") ? neighbor.substring(0, neighbor.lastIndexOf('.')) : null;
String classTarget = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : null;
String simpleClassTarget = classTarget != null && classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
String simpleClassNeighbor = classNeighbor != null && classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
if (simpleClassNeighbor != null && simpleClassTarget != null) {
if (simpleClassNeighbor.equalsIgnoreCase(simpleClassTarget)) return true;
// e.g. "this" vs "com.example.Machine"
if (simpleClassNeighbor.equals("this") || simpleClassNeighbor.equals("super")) return true;
return false;
}
return true;
} }
private boolean isFullyQualifiedMethod(String methodFqn) { private boolean isFullyQualifiedMethod(String methodFqn) {

View File

@@ -525,14 +525,39 @@ public class VariableTracer {
if (simpleClassNeighbor.equals(simpleClassTarget)) return true; if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
List<String> impls = context.getImplementations(classTarget); List<String> impls = context.getImplementations(classTarget);
if (impls != null && impls.contains(classNeighbor)) return true; if (impls != null) {
for (String impl : impls) {
if (impl.equals(classNeighbor) || impl.endsWith("." + classNeighbor)) return true;
}
}
List<String> implsN = context.getImplementations(classNeighbor); List<String> implsN = context.getImplementations(classNeighbor);
if (implsN != null && implsN.contains(classTarget)) return true; if (implsN != null) {
for (String impl : implsN) {
if (impl.equals(classTarget) || impl.endsWith("." + classTarget)) return true;
}
} }
return false; return false;
} }
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
if (!simpleNeighbor.equals(simpleTarget)) return false;
String classNeighbor = neighbor.contains(".") ? neighbor.substring(0, neighbor.lastIndexOf('.')) : null;
String classTarget = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : null;
String simpleClassTarget = classTarget != null && classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
String simpleClassNeighbor = classNeighbor != null && classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
if (simpleClassNeighbor != null && simpleClassTarget != null) {
if (simpleClassNeighbor.equalsIgnoreCase(simpleClassTarget)) return true;
if (simpleClassNeighbor.equals("this") || simpleClassNeighbor.equals("super")) return true;
return false;
}
return true;
}
private MethodDeclaration findEnclosingMethod(ASTNode node) { private MethodDeclaration findEnclosingMethod(ASTNode node) {
ASTNode parent = node.getParent(); ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof MethodDeclaration)) { while (parent != null && !(parent instanceof MethodDeclaration)) {