Skip to content
67 changes: 65 additions & 2 deletions src/main/java/com/projectlightpath/analysis/java/CallEdge.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,62 @@ public record CallEdge(
int line,
String sourcePath,
SemanticCallFacts.Certainty certaintyHint,
String relationshipType) {
String relationshipType,
TargetContext targetContext) {
public enum TargetOrigin {
APPLICATION("application"),
DEPENDENCY("dependency");

private final String id;

TargetOrigin(String id) {
this.id = id;
}

public String id() {
return id;
}
}

/** Describes the resolved input that supplied a call target. */
public record TargetContext(
TargetOrigin origin,
String dependencyClasspathEntry,
String targetBytecodePath,
String targetSourcePath) {
public TargetContext {
origin = origin == null ? TargetOrigin.APPLICATION : origin;
dependencyClasspathEntry = blankIfNull(dependencyClasspathEntry);
targetBytecodePath = blankIfNull(targetBytecodePath);
targetSourcePath = blankIfNull(targetSourcePath);
if (origin == TargetOrigin.APPLICATION) {
dependencyClasspathEntry = "";
}
if (origin == TargetOrigin.DEPENDENCY && dependencyClasspathEntry.isBlank()) {
throw new IllegalArgumentException("dependency target context requires a classpath entry");
}
}

public static TargetContext application() {
return new TargetContext(TargetOrigin.APPLICATION, "", "", "");
}

private static String blankIfNull(String value) {
return value == null ? "" : value;
}
}

/** Compatibility constructor for existing provider graph fixtures. */
public CallEdge(
MethodSymbol caller,
MethodSymbol callee,
int line,
String sourcePath,
SemanticCallFacts.Certainty certaintyHint,
String relationshipType) {
this(caller, callee, line, sourcePath, certaintyHint, relationshipType, TargetContext.application());
}

public CallEdge {
if (line < -1) {
throw new IllegalArgumentException("call line must be -1 or non-negative");
Expand All @@ -17,9 +72,17 @@ public record CallEdge(
}
certaintyHint = certaintyHint == null ? SemanticCallFacts.Certainty.DEFINITE : certaintyHint;
relationshipType = relationshipType == null || relationshipType.isBlank() ? "invokes" : relationshipType;
targetContext = targetContext == null ? TargetContext.application() : targetContext;
}

public static CallEdge definite(MethodSymbol caller, MethodSymbol callee, int line, String sourcePath) {
return new CallEdge(caller, callee, line, sourcePath, SemanticCallFacts.Certainty.DEFINITE, "invokes");
return new CallEdge(
caller,
callee,
line,
sourcePath,
SemanticCallFacts.Certainty.DEFINITE,
"invokes",
TargetContext.application());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand All @@ -16,69 +18,136 @@ final class ProviderFactNormalizer {
private ProviderFactNormalizer() {}

static List<SemanticCallFacts.Fact> normalize(AnalysisContext context, AnalysisGraph graph) {
List<SemanticCallFacts.Fact> facts = new ArrayList<>();
Set<String> ids = new HashSet<>();
Map<String, Integer> baseIdCounts = new HashMap<>();
for (CallEdge edge : graph.calls()) {
baseIdCounts.merge(legacyCallId(edge), 1, Integer::sum);
CallReferences references = collectCallReferences(graph);
Map<Integer, String> callIds = callIds(references, ids);
List<SemanticCallFacts.Fact> facts = callFacts(context, references.edges(), callIds);
facts.addAll(dynamicFacts(context, graph.dynamicFeatures(), ids));
return List.copyOf(facts);
}

/** Collects graph edges and the application collision counts used for stable IDs. */
private static CallReferences collectCallReferences(AnalysisGraph graph) {
List<EdgeRef> edgeRefs = new ArrayList<>();
Map<String, Integer> applicationBaseIdCounts = new HashMap<>();
for (int index = 0; index < graph.calls().size(); index++) {
CallEdge edge = graph.calls().get(index);
edgeRefs.add(new EdgeRef(index, edge, edgeIdentity(edge)));
if (isApplicationEdge(edge)) {
applicationBaseIdCounts.merge(legacyCallId(edge), 1, Integer::sum);
}
}
for (CallEdge edge : graph.calls()) {
String callIdentity = edge.caller().displayName() + "|" + edge.caller().descriptor()
+ "->" + edge.callee().displayName() + "|" + edge.callee().descriptor()
+ "@" + edge.line() + "|" + edge.sourcePath() + "|" + edge.relationshipType();
String baseId = legacyCallId(edge);
String id = baseIdCounts.get(baseId) == 1
? uniqueId(ids, baseId, callIdentity)
: uniqueId(ids, baseId + "-" + sha256(callIdentity), callIdentity);
boolean openWorldDynamic = DynamicCertaintyPolicy.isOpenWorldProviderRelationship(
edge.relationshipType());
SemanticCallFacts.Certainty certainty = openWorldDynamic
? SemanticCallFacts.Certainty.UNRESOLVED
: edge.certaintyHint();
String limitation = openWorldDynamic
? DynamicCertaintyPolicy.LIMITATION
: certainty == SemanticCallFacts.Certainty.POSSIBLE
? "CHA exposes a bounded implementation/callback candidate; runtime selection is not unique."
: null;
facts.add(SemanticCallFacts.Fact.provider(
id,
context,
edge.caller().displayName(),
edge.callee().displayName(),
edge.relationshipType(),
certainty,
List.of(SemanticCallFacts.Evidence.sootup(
edge.sourcePath(),
edge.line(),
edge.caller(),
edge.callee(),
"SootUp CHA application call")),
limitation));
return new CallReferences(List.copyOf(edgeRefs), Map.copyOf(applicationBaseIdCounts));
}

/** Assigns deterministic IDs without changing the graph's output order. */
private static Map<Integer, String> callIds(CallReferences references, Set<String> ids) {
Map<Integer, String> idsByIndex = new HashMap<>();
references.edges().stream()
.sorted(Comparator.comparing(EdgeRef::identity).thenComparingInt(EdgeRef::index))
.forEach(ref -> idsByIndex.put(
ref.index(), callId(ref, references.applicationBaseIdCounts(), ids)));
return idsByIndex;
}

/** Selects the origin-aware ID namespace for one call edge. */
private static String callId(
EdgeRef ref, Map<String, Integer> applicationBaseIdCounts, Set<String> ids) {
if (!isApplicationEdge(ref.edge())) {
return uniqueId(ids, dependencyCallId(ref.edge()), ref.identity());
}
for (DynamicFeature feature : graph.dynamicFeatures()) {
String relationship = feature.kind() == DynamicFeature.Kind.REFLECTION ? "reflection" : "proxy_dispatch";
String baseId = dynamicFeatureId(feature);
String id = baseId;
int duplicate = 1;
while (!ids.add(id)) {
id = baseId + "-" + duplicate++;
}
facts.add(SemanticCallFacts.Fact.provider(
id,
context,
feature.caller().displayName(),
feature.target(),
relationship,
SemanticCallFacts.Certainty.UNRESOLVED,
List.of(new SemanticCallFacts.Evidence(
feature.kind() == DynamicFeature.Kind.REFLECTION ? "reflection_api" : "dynamic_proxy_call",
feature.sourcePath(),
feature.symbols(),
feature.sourceLine() > 0 ? feature.sourceLine() : null,
dynamicEvidenceAttributes(feature))),
feature.limitation()));
String baseId = legacyCallId(ref.edge());
if (applicationBaseIdCounts.get(baseId) == 1) {
return uniqueId(ids, baseId, ref.identity());
}
return List.copyOf(facts);
return uniqueId(ids, baseId + "-" + sha256(ref.identity()), ref.identity());
}

/** Normalizes ordinary call edges in their original provider order. */
private static List<SemanticCallFacts.Fact> callFacts(
AnalysisContext context, List<EdgeRef> edgeRefs, Map<Integer, String> callIds) {
List<SemanticCallFacts.Fact> facts = new ArrayList<>();
for (EdgeRef ref : edgeRefs) {
facts.add(callFact(context, ref.edge(), callIds.get(ref.index())));
}
return facts;
}

/** Normalizes one ordinary provider edge with the centralized certainty policy. */
private static SemanticCallFacts.Fact callFact(
AnalysisContext context, CallEdge edge, String id) {
boolean openWorldDynamic = DynamicCertaintyPolicy.isOpenWorldProviderRelationship(
edge.relationshipType());
SemanticCallFacts.Certainty certainty = openWorldDynamic
? SemanticCallFacts.Certainty.UNRESOLVED
: edge.certaintyHint();
return SemanticCallFacts.Fact.provider(
id,
context,
edge.caller().displayName(),
edge.callee().displayName(),
edge.relationshipType(),
certainty,
List.of(sootupEvidence(edge)),
limitation(openWorldDynamic, certainty));
}

/** Normalizes source-backed dynamic observations after ordinary call IDs exist. */
private static List<SemanticCallFacts.Fact> dynamicFacts(
AnalysisContext context, List<DynamicFeature> features, Set<String> ids) {
List<SemanticCallFacts.Fact> facts = new ArrayList<>();
for (DynamicFeature feature : features) {
facts.add(dynamicFact(context, feature, nextDynamicId(feature, ids)));
}
return facts;
}

/** Creates one unresolved fact for a reflection or proxy observation. */
private static SemanticCallFacts.Fact dynamicFact(
AnalysisContext context, DynamicFeature feature, String id) {
String relationship = feature.kind() == DynamicFeature.Kind.REFLECTION
? "reflection" : "proxy_dispatch";
String evidenceKind = feature.kind() == DynamicFeature.Kind.REFLECTION
? "reflection_api" : "dynamic_proxy_call";
return SemanticCallFacts.Fact.provider(
id,
context,
feature.caller().displayName(),
feature.target(),
relationship,
SemanticCallFacts.Certainty.UNRESOLVED,
List.of(new SemanticCallFacts.Evidence(
evidenceKind,
feature.sourcePath(),
feature.symbols(),
feature.sourceLine() > 0 ? feature.sourceLine() : null,
dynamicEvidenceAttributes(feature))),
feature.limitation());
}

/** Allocates a deterministic dynamic ID without colliding with call IDs. */
private static String nextDynamicId(DynamicFeature feature, Set<String> ids) {
String baseId = dynamicFeatureId(feature);
String id = baseId;
int duplicate = 1;
while (!ids.add(id)) {
id = baseId + "-" + duplicate++;
}
return id;
}

/** Applies open-world abstention and bounded-candidate limitations. */
private static String limitation(boolean openWorldDynamic, SemanticCallFacts.Certainty certainty) {
if (openWorldDynamic) {
return DynamicCertaintyPolicy.LIMITATION;
}
return certainty == SemanticCallFacts.Certainty.POSSIBLE
? "CHA exposes a bounded implementation/callback candidate; runtime selection is not unique."
: null;
}

private static boolean isApplicationEdge(CallEdge edge) {
return edge.targetContext().origin() == CallEdge.TargetOrigin.APPLICATION;
}

/** Uses the complete source identity so separate dynamic sites cannot silently collide. */
Expand All @@ -100,6 +169,54 @@ private static String legacyCallId(CallEdge edge) {
(edge.caller().descriptor() + "->" + edge.callee().descriptor() + "@" + edge.line()).hashCode());
}

/** Gives dependency targets an origin-aware ID namespace without changing application IDs. */
private static String dependencyCallId(CallEdge edge) {
return "sootup-dependency-call-" + sha256(dependencyCallIdentity(edge));
}

/** Retains the pre-existing identity string for application call edges. */
private static String applicationCallIdentity(CallEdge edge) {
return edge.caller().displayName() + "|" + edge.caller().descriptor()
+ "->" + edge.callee().displayName() + "|" + edge.callee().descriptor()
+ "@" + edge.line() + "|" + edge.sourcePath() + "|" + edge.relationshipType();
}

/** Separates dependency target origin from the optional evidence path. */
private static String dependencyCallIdentity(CallEdge edge) {
return applicationCallIdentity(edge)
+ "|" + edge.targetContext().origin().id()
+ "|" + edge.targetContext().dependencyClasspathEntry();
}

private static String edgeIdentity(CallEdge edge) {
return edge.targetContext().origin() == CallEdge.TargetOrigin.APPLICATION
? applicationCallIdentity(edge)
: dependencyCallIdentity(edge);
}

private static SemanticCallFacts.Evidence sootupEvidence(CallEdge edge) {
Map<String, String> attributes = new LinkedHashMap<>();
attributes.put("basis", "SootUp CHA application call");
attributes.put("caller", edge.caller().displayName());
attributes.put("callee", edge.callee().displayName());
attributes.put("callerSignature", edge.caller().descriptor());
attributes.put("calleeSignature", edge.callee().descriptor());
if (edge.targetContext().origin() == CallEdge.TargetOrigin.DEPENDENCY) {
attributes.put("calleeOwner", edge.callee().owner());
attributes.put("calleeMethod", edge.callee().name());
attributes.put("targetOrigin", edge.targetContext().origin().id());
attributes.put("dependencyClasspathEntry", edge.targetContext().dependencyClasspathEntry());
if (!edge.targetContext().targetBytecodePath().isBlank()) {
attributes.put("targetBytecodePath", edge.targetContext().targetBytecodePath());
}
if (!edge.targetContext().targetSourcePath().isBlank()) {
attributes.put("targetSourcePath", edge.targetContext().targetSourcePath());
}
}
return new SemanticCallFacts.Evidence(
"sootup_call", edge.sourcePath(), List.of(), edge.line(), attributes);
}

/** Keeps legacy IDs stable for ordinary edges while making collisions explicit and safe. */
private static String uniqueId(Set<String> ids, String baseId, String identity) {
if (ids.add(baseId)) {
Expand Down Expand Up @@ -137,4 +254,8 @@ private static String sha256(String value) {
}
}

private record EdgeRef(int index, CallEdge edge, String identity) {}

private record CallReferences(
List<EdgeRef> edges, Map<String, Integer> applicationBaseIdCounts) {}
}
Loading
Loading