Skip to content

fix(apex): don't treat referenced types as definitions - #3106

Open
rsoesemann wants to merge 1 commit into
Graphify-Labs:v8from
rsoesemann:fix/apex-referenced-type-placeholders
Open

fix(apex): don't treat referenced types as definitions#3106
rsoesemann wants to merge 1 commit into
Graphify-Labs:v8from
rsoesemann:fix/apex-referenced-type-placeholders

Conversation

@rsoesemann

Copy link
Copy Markdown

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.cls writes extends Bar or [SELECT Id FROM Account], the extractor mints a node for Bar/Account carrying Foo.cls as its source_file. That reads as a definition, so _disambiguate_colliding_node_ids salts the id with the referencing file's path. One class referenced from five files becomes five unconnected nodes, and none of the extends/uses edges 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 Foo produced no node at all, and its methods were attributed to whatever inner class came next.

Referenced types now get a sourceless placeholder with no contains edge, which _rewire_unique_stub_nodes collapses 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_key falls back to origin_file when source_file is 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:

before   explain DataLibrary → Ambiguous: 2 nodes
                               DataLibrary_Test.cls, SearchDataLibrary_Test.cls
                               (the class itself is absent)

after    explain DataLibrary → DataLibrary, DataLibrary.cls:L1, degree 12
                               <-- MockDataLibrary      [extends]
                               <-- ThrowingDataLibrary  [extends]
                               <-- DataLibrarySpy       [extends]

Type labels split across more than one node went from 11 to 4. The remaining 4 are correct — Output really 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.py following 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 a contains edge, 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 kind base.py warns about, and that's a separate design question. And the method pattern matches new Memory__c(Name = …) as a declaration, producing a bogus .Memory__c() node — pre-existing, unchanged count before and after, worth its own fix.

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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant