fix(apex): don't treat referenced types as definitions - #3106
Conversation
A type a file only references (`extends Bar`, `[SELECT Id FROM Account]`) was stamped with the referencing file's source_file, so it read as a definition and got its id salted per referencing file. One class referenced from five files became five unconnected nodes and no edge reached the real definition — the Graphify-Labs#1402/Graphify-Labs#2324 pattern already fixed for SQL and OCaml. Referenced types now get a sourceless placeholder with no contains edge, so _rewire_unique_stub_nodes collapses it onto the unique real definition. No origin_file: _node_disambiguation_source_key falls back to it and would re-introduce the same per-file salting. Also match class modifiers order-independently. Apex allows them in any order and any number, so `public abstract with sharing class Foo` produced no node at all under the fixed access/sharing/modifier sequence.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes Apex type resolution so cross-file extends/implements/SObject references bind to real definitions instead of fragmenting: referenced-but-not-declared types now emit sourceless stub nodes via type_ref/add_stub (no source_file, no origin_file, no contains edge), letting the corpus-level rewire connect one shared node instead of salting a separate node per referencing file. Also rewrites the class/interface/enum/method regexes to accept modifiers in any order and any count via _MODIFIERS, so declarations like public with sharing abstract class Foo are no longer silently dropped.
No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 548 functions depend on the 548 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_apex()— 19 callers, 5 callees
Verification — 548 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 548 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_apex.
The verifier did not have enough to check extract\_apex, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 more finding(s) on lines outside this diff (see the check run).
Two things are broken in the Apex extractor, and they compound. Asking what depends on a class comes back empty even when the class is used all over the codebase.
A referenced type is stamped as if it were defined here. When
Foo.clswritesextends Baror[SELECT Id FROM Account], the extractor mints a node forBar/AccountcarryingFoo.clsas itssource_file. That reads as a definition, so_disambiguate_colliding_node_idssalts the id with the referencing file's path. One class referenced from five files becomes five unconnected nodes, and none of theextends/usesedges reach the real definition. This is the same bug already fixed for SQL tables (#1402, #2324) and OCaml modules — Apex was the odd one out.The class regex assumes a fixed modifier order. Apex allows modifiers in any order and any number, but the pattern matched access → sharing → modifier, one each. So
public abstract with sharing class Fooproduced no node at all, and its methods were attributed to whatever inner class came next.Referenced types now get a sourceless placeholder with no
containsedge, which_rewire_unique_stub_nodescollapses onto the unique real definition. Modifiers are matched as a repeatable, order-independent group.One difference from the SQL/CommonLisp stubs worth flagging: those set
origin_file, and doing that here silently defeats the fix —_node_disambiguation_source_keyfalls back toorigin_filewhensource_fileis empty, so the per-file salting comes right back. I left it off and said why in the docstring.Measured on my-org-butler, 55 Apex classes. Two classes are declared
public virtual with sharing, so neither existed as a node — and both were also the names showing up as multiple orphan placeholders:Type labels split across more than one node went from 11 to 4. The remaining 4 are correct —
Outputreally is declared 12 times, once per Invocable class. Method nodes went 272 → 315, and the whole increase sits in the two files whose declaration was previously unmatched; no other file changed by a single node, so the relaxed method pattern added no false positives.Tests: 5 new ones, all of which fail without this change. The cross-file cases live in
tests/test_apex_type_resolution.pyfollowing the Java/Go pattern, since the fragmentation only appears once several files are merged — an extractor-level test can't see it. Two negative controls: a referenced type must not get acontainsedge, and a type defined nowhere in the corpus must stay a sourceless leaf instead of binding to something unrelated. Full suite green.Two things this deliberately does not touch. DML pseudo-nodes (
insert,update, …) still fragment per file; making them sourceless would build a god-node of the kindbase.pywarns about, and that's a separate design question. And the method pattern matchesnew Memory__c(Name = …)as a declaration, producing a bogus.Memory__c()node — pre-existing, unchanged count before and after, worth its own fix.