Class CallableIdentity
module::Aggregate.member(T1,T2) for
a method, module::Aggregate.member for any other aggregate member, and the plain
fully-qualified name for everything else. Use this for any call-graph node key or call-edge target;
do NOT key on ISymbol.getFullyQualifiedName().
Why this exists. getFullyQualifiedName() composes the module scope with the
symbol's own name only (Symbol.getFullyQualifiedName), so for a method it yields
module::check — dropping the enclosing type. That is not a display wart: it made the node
key non-unique, so two same-named members on UNRELATED types collapsed into a single graph node.
The surviving node took one type's symbol and the other type's callers, and everything downstream
inherited it — dead-code detection silently missed the second member (it looked reachable because
its namesake was), and reachability, DI ordering and interprocedural lock analysis all read the
merged node. A module with five ? operators had one node for all five.
Deliberately NOT CoverageDisplayName.identityForSymbol, which is the join key for
coverage/variety/HEX reports and looks almost identical. It additionally maps any name beginning
_ (plus i_init/c_init) to its enclosing scope name, which is right for the
synthetic _call/_main it was written for but wrong here: a dynamic function is
named _Function_<hash>, and rewriting it to its enclosing scope's name would collide it
with the real callable declared there — trading one merge bug for another. Dynamic functions are
already unique by hash, so they pass through untouched.
The two schemes still AGREE on the shape that matters for joining reports to the graph — an
aggregate member is Aggregate.member in both — so a Ca row and a HEX row can finally be
matched on identity rather than on a name produced for humans.
🔑 The PARAMETER SIGNATURE was the unfinished half of that fix (added 2026-09-05). Adding
the aggregate separated same-named members on unrelated types, but left every OVERLOAD of one name
on ONE type sharing a node — and operators are the worst case, since _add(Integer) and
_add(String) on a single type were indistinguishable. The consequences are the same ones
the aggregate fix removed, one level down: dead-code scanning misses an unused overload because its
namesake is reachable, and purity impact attributes one overload's callers to another.
⚠️ Parameter types are FULLY QUALIFIED, deliberately. Simple names would read better and
would reintroduce exactly the bug being fixed the moment two same-named types from different
modules appear as the arguments of two overloads. This is an IDENTITY, not a label — anything
rendering it for a human should use MethodSymbol.getFriendlyName() instead.
⚠️ The RETURN type is deliberately absent: EK9 does not admit overloads differing only by return type, so it cannot discriminate, and including it would churn the key whenever a return is widened.
-
Method Summary
Modifier and TypeMethodDescriptionstatic StringdeclaringConstructOf(String callableIdentity) THE inverse offorSymbol(ISymbol): the declaring construct of a call-graph identity.static StringThe call-graph identity for a callable symbol.
-
Method Details
-
forSymbol
-
declaringConstructOf
THE inverse offorSymbol(ISymbol): the declaring construct of a call-graph identity.module::Aggregate.memberyieldsmodule::Aggregate; a module-level function or programmodule::nameyields itself. Use this to lift a callable-granular call-graph answer to construct granularity; do NOT hand-roll the split.It lives here, beside the code that mints the format, on purpose. Two byte-identical private copies had already appeared in
compiler-servicesextractors, and a parser sitting in a different module from the minter is precisely the drifting shortcut that breaks silently the day the format changes — nothing would fail to compile, the split would just quietly return the wrong construct.- Parameters:
callableIdentity- an identity as produced byforSymbol(ISymbol), or null- Returns:
- the declaring construct's fully-qualified name, or null when given null
-