from ai two
This commit is contained in:
@@ -268,7 +268,16 @@ public class ConstantResolver {
|
||||
if (fragment.getInitializer() != null) {
|
||||
String rhsVal = resolveExpressionWithParams(fragment.getInitializer(), localVars, context);
|
||||
if (rhsVal == null) rhsVal = resolveInternal(fragment.getInitializer(), context, visited);
|
||||
if (rhsVal != null) localVars.put(varName, rhsVal);
|
||||
if (rhsVal != null) {
|
||||
localVars.put(varName, rhsVal);
|
||||
} else {
|
||||
java.util.Map<String, String> bFields = extractBuilderFields(fragment.getInitializer(), localVars, context, visited);
|
||||
if (bFields != null) {
|
||||
for (java.util.Map.Entry<String, String> e : bFields.entrySet()) {
|
||||
localVars.put(varName + "." + e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,9 +330,19 @@ public class ConstantResolver {
|
||||
} else if (lhs instanceof FieldAccess fa) {
|
||||
varName = "this." + fa.getName().getIdentifier();
|
||||
}
|
||||
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars, context);
|
||||
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||
if (varName != null && rhsVal != null) {
|
||||
if (varName != null) {
|
||||
String rhsVal = resolveExpressionWithParams(assignment.getRightHandSide(), localVars, context);
|
||||
if (rhsVal == null) rhsVal = resolveInternal(assignment.getRightHandSide(), context, visited);
|
||||
if (rhsVal != null) {
|
||||
localVars.put(varName, rhsVal);
|
||||
} else {
|
||||
java.util.Map<String, String> bFields = extractBuilderFields(assignment.getRightHandSide(), localVars, context, visited);
|
||||
if (bFields != null) {
|
||||
for (java.util.Map.Entry<String, String> e : bFields.entrySet()) {
|
||||
localVars.put(varName + "." + e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (varName.startsWith("this.")) {
|
||||
localVars.put(varName, rhsVal);
|
||||
String bareName = varName.substring(5);
|
||||
@@ -577,6 +596,22 @@ public class ConstantResolver {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mi.arguments().isEmpty() && mi.getExpression() instanceof SimpleName sn) {
|
||||
String objName = sn.getIdentifier();
|
||||
String methodName = mi.getName().getIdentifier();
|
||||
String propName = null;
|
||||
if (methodName.startsWith("get") && methodName.length() > 3) {
|
||||
propName = methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
|
||||
} else {
|
||||
propName = methodName;
|
||||
}
|
||||
String val = paramValues.get(objName + "." + propName);
|
||||
if (val == null && !propName.equals(methodName)) {
|
||||
val = paramValues.get(objName + "." + methodName);
|
||||
}
|
||||
if (val != null) return val;
|
||||
}
|
||||
|
||||
MethodDeclaration sideEffectMd = findInvokedMethod(mi, context);
|
||||
if (sideEffectMd != null && sideEffectMd.getBody() != null) {
|
||||
TypeDeclaration currentTd = findEnclosingType(sideEffectMd);
|
||||
@@ -613,6 +648,29 @@ public class ConstantResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
private java.util.Map<String, String> extractBuilderFields(Expression expr, java.util.Map<String, String> localVars, CodebaseContext context, Set<String> visited) {
|
||||
if (!(expr instanceof MethodInvocation mi)) return null;
|
||||
java.util.Map<String, String> fields = new java.util.HashMap<>();
|
||||
MethodInvocation current = mi;
|
||||
while (current != null) {
|
||||
String methodName = current.getName().getIdentifier();
|
||||
if (!"build".equals(methodName) && !"builder".equals(methodName) && !"toBuilder".equals(methodName)) {
|
||||
if (current.arguments().size() == 1) {
|
||||
Expression arg = (Expression) current.arguments().get(0);
|
||||
String val = resolveExpressionWithParams(arg, localVars, context);
|
||||
if (val == null) val = resolveInternal(arg, context, visited);
|
||||
if (val != null) fields.put(methodName, val);
|
||||
}
|
||||
}
|
||||
if (current.getExpression() instanceof MethodInvocation next) {
|
||||
current = next;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fields.isEmpty() ? null : fields;
|
||||
}
|
||||
|
||||
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
|
||||
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
|
||||
|
||||
|
||||
@@ -66,4 +66,59 @@ class BuilderLocalVariableTest {
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder("OrderEvents.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTraceFluentBuilder(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class MyController {
|
||||
private MyService service;
|
||||
public void processEvent() {
|
||||
MyEvent event = MyEvent.builder().type(MyEnum.STATE_2).build();
|
||||
service.process(event.getType());
|
||||
}
|
||||
}
|
||||
|
||||
class MyService {
|
||||
public void process(MyEnum event) {}
|
||||
}
|
||||
|
||||
class MyEvent {
|
||||
private MyEnum type;
|
||||
public static Builder builder() { return new Builder(); }
|
||||
public MyEnum getType() { return type; }
|
||||
public static class Builder {
|
||||
private MyEnum type;
|
||||
public Builder type(MyEnum type) { this.type = type; return this; }
|
||||
public MyEvent build() { return new MyEvent(); }
|
||||
}
|
||||
}
|
||||
|
||||
enum MyEnum { STATE_1, STATE_2 }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("MyConfig.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
|
||||
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.className("com.example.MyController")
|
||||
.methodName("processEvent")
|
||||
.build();
|
||||
|
||||
TriggerPoint trigger = TriggerPoint.builder()
|
||||
.className("com.example.MyService")
|
||||
.methodName("process")
|
||||
.event("event")
|
||||
.build();
|
||||
|
||||
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
|
||||
|
||||
assertThat(chains).hasSize(1);
|
||||
CallChain chain = chains.get(0);
|
||||
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
|
||||
.containsExactlyInAnyOrder("MyEnum.STATE_2");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,22 @@
|
||||
stroke-width: 3.5px;
|
||||
}
|
||||
|
||||
.active-path-ambiguous {
|
||||
stroke: #f97316 !important;
|
||||
stroke-width: 2.5px !important;
|
||||
stroke-dasharray: 6, 4;
|
||||
filter: drop-shadow(0 0 8px rgba(249, 115, 22, 0.4));
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.active-path-ambiguous text, a.active-path-ambiguous text {
|
||||
fill: #000 !important;
|
||||
font-weight: 900 !important;
|
||||
paint-order: stroke;
|
||||
stroke: #fff;
|
||||
stroke-width: 3.5px;
|
||||
}
|
||||
|
||||
.dimmed {
|
||||
opacity: 0.2 !important;
|
||||
filter: grayscale(1);
|
||||
@@ -418,7 +434,7 @@
|
||||
if (c.entryPoint.className === ep.className && c.entryPoint.methodName === ep.methodName) {
|
||||
if (c.matchedTransitions && c.matchedTransitions.length > 0) {
|
||||
c.matchedTransitions.forEach(t => {
|
||||
steps.push({ event: t.event, source: t.sourceState });
|
||||
steps.push({ event: t.event, source: t.sourceState, ambiguous: c.triggerPoint && c.triggerPoint.ambiguous });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -446,10 +462,12 @@
|
||||
let targetEvent = "";
|
||||
let targetSource = "";
|
||||
let targetAnon = "";
|
||||
let isAmbiguous = false;
|
||||
|
||||
if (typeof step === 'object') {
|
||||
targetEvent = normalize(step.event);
|
||||
targetSource = normalize(step.source);
|
||||
isAmbiguous = step.ambiguous;
|
||||
} else if (step.includes("->")) {
|
||||
const parts = step.split("->");
|
||||
targetAnon = "#link_anon_" + normalize(parts[0]) + "_" + normalize(parts[1]);
|
||||
@@ -475,7 +493,7 @@
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
highlightLinkGroup(link);
|
||||
highlightLinkGroup(link, isAmbiguous);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -494,8 +512,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
function highlightLinkGroup(link) {
|
||||
link.classList.add('active-path');
|
||||
function highlightLinkGroup(link, isAmbiguous) {
|
||||
const activeClass = isAmbiguous ? 'active-path-ambiguous' : 'active-path';
|
||||
link.classList.add(activeClass);
|
||||
link.querySelectorAll('*').forEach(child => child.classList.remove('dimmed'));
|
||||
|
||||
// Find path and polygon immediately before the <a> tag (interleaved)
|
||||
@@ -505,11 +524,11 @@
|
||||
while (prev) {
|
||||
if (prev.tagName === 'path' && !foundPath) {
|
||||
prev.classList.remove('dimmed');
|
||||
prev.classList.add('active-path');
|
||||
prev.classList.add(activeClass);
|
||||
foundPath = true;
|
||||
} else if (prev.tagName === 'polygon' && !foundPolygon && prev.points?.numberOfItems !== 4) {
|
||||
prev.classList.remove('dimmed');
|
||||
prev.classList.add('active-path');
|
||||
prev.classList.add(activeClass);
|
||||
foundPolygon = true;
|
||||
} else if (['rect', 'circle', 'ellipse'].includes(prev.tagName) ||
|
||||
(prev.tagName === 'polygon' && prev.points?.numberOfItems === 4)) {
|
||||
@@ -522,8 +541,8 @@
|
||||
}
|
||||
|
||||
function clearHighlights() {
|
||||
document.querySelectorAll('.dimmed, .active-path').forEach(el => {
|
||||
el.classList.remove('dimmed', 'active-path');
|
||||
document.querySelectorAll('.dimmed, .active-path, .active-path-ambiguous').forEach(el => {
|
||||
el.classList.remove('dimmed', 'active-path', 'active-path-ambiguous');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -629,9 +648,11 @@
|
||||
html += `<div style="font-size:0.7rem; font-weight:800; color:#94a3b8; text-transform:uppercase; margin-bottom:10px; border-top:1px solid #eee; padding-top:10px">Triggered by</div>`;
|
||||
chains.forEach(c => {
|
||||
const triggerInfo = c.triggerPoint.sourceFile ? ` <span style="color:#94a3b8; font-weight:400; font-size:0.6rem;">(${c.triggerPoint.sourceFile}:${c.triggerPoint.lineNumber})</span>` : '';
|
||||
const ambiguousLabel = c.triggerPoint.ambiguous ? `<div style="display:inline-block; margin-top:4px; padding:2px 6px; background:#fff7ed; color:#ea580c; border:1px solid #fed7aa; border-radius:4px; font-size:0.6rem; font-weight:700;">⚠️ Ambiguous Resolution (Over-approximated)</div>` : '';
|
||||
html += `<div style="margin-bottom:20px">
|
||||
<div style="font-weight:700; font-size:0.85rem; color:var(--text);">${c.entryPoint.name}</div>
|
||||
<div style="font-size:0.65rem; color:#64748b; font-family:monospace; margin-top:2px;">Trigger: ${c.triggerPoint.methodName}${triggerInfo}</div>
|
||||
${ambiguousLabel}
|
||||
${renderParameters(c.entryPoint.parameters)}
|
||||
<div style="margin-left:10px; border-left:2px solid #e2e8f0; padding-left:12px; margin-top:10px">
|
||||
${c.methodChain.map((m, i) => `<div style="font-size:0.7rem; color:#475569; margin-top:4px; font-family:'JetBrains Mono', monospace;"><b>${i+1}</b> ${m}</div>`).join('')}
|
||||
|
||||
Reference in New Issue
Block a user