feat(apex): parse Apex with tree-sitter, add calls edges - #3122
feat(apex): parse Apex with tree-sitter, add calls edges#3122rsoesemann wants to merge 2 commits into
Conversation
The Apex extractor never produced calls edges, and its docstring said why: no tree-sitter grammar on PyPI. That is out of date — aheber/tree-sitter-sfapex exists, is MIT, and uses the same node type and field names as tree-sitter-java for declarations and calls, so the shared engine walk drives it from a config. On my-org-butler, 55 Apex files: 592 calls edges where there were 0, across 225 distinct targets. Not the Java grammar: parsing those same files with tree-sitter-java yields 740 ERROR nodes in 53 of 55, silently. The sfapex grammar parses all 55 clean. The regex extractor stays as the fallback, like Pascal's, so Apex still extracts without the optional extra — minus calls. extends/implements, the SObject behind a trigger or a SOQL FROM, DML, and `new X()` are handled in the Apex extractor rather than by widening the Java-gated branches in the engine. Two filters keep collection built-ins from becoming god-nodes: `new List<>()` is not a dependency, and `rows.add(x)` must not bind to whichever class happens to declare add(). Shared core gains one optional LanguageConfig field for grammars only reachable through tree-sitter-language-pack, plus the loader branch using it.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds a tree-sitter path to the Apex extractor that runs when the sfapex/language-pack grammar is installed, falling back to the existing regex extractor (now _extract_apex_regex) unchanged when the grammar is missing or a file fails to parse, at lower fidelity and with no calls edges. The AST path drives declarations and calls through the shared generic engine using an _APEX_CONFIG shaped like Java, then _add_apex_specifics layers on Apex-only constructs — extends/implements, the SObject a trigger fires on, SOQL FROM/SOSL RETURNING type usage, DML, new Type() construction, and entry-point annotations. Filters _APEX_BUILTIN_METHODS and constructors from raw_calls so common collection/JSON/Http calls don't collapse into god-nodes or bind cross-file to a single class that happens to declare the same bare name.
Worth a look
- Apex parse-error roots do not trigger regex fallback —
graphify/extractors/apex.py:59· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Tree-sitter parse errors are treated as successful AST extraction —
graphify/extractors/apex.py:61· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Apex REST method annotations are not treated as external entry points —
graphify/extractors/apex.py:286· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Language-pack loader errors now escape _extract_generic —
graphify/extractors/engine.py:2885· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1720 functions depend on the 266 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 501 callers, 42 callees - new:
_rebuild_code()— 98 callers, 50 callees - new:
_extract_generic()— 20 callers, 24 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_js()— 83 callers, 3 callees - new:
dispatch_command()— 2 callers, 122 callees - new:
extract_objc()— 27 callers, 9 callees - new:
extract_julia()— 17 callers, 7 callees - …and 25 more — each is listed as a finding
Verification — 1720 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: 1070 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
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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
· 2 grounded finding(s) anchored inline below; 31 more finding(s) on lines outside this diff (see the check run).
| return result | ||
|
|
||
|
|
||
| def _extract_apex_regex(path: Path) -> dict: |
There was a problem hiding this comment.
_extract_apex_regex()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace") | ||
|
|
||
|
|
||
| def _add_apex_specifics(path: Path, root, source: bytes, result: dict) -> None: |
There was a problem hiding this comment.
_add_apex_specifics()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
… points tree-sitter is error-tolerant: a file it cannot handle comes back as a tree containing ERROR nodes rather than an exception, so the AST path returned a confidently wrong result instead of letting the regex path degrade predictably. Check root.has_error and fall back. Triggers on 0 of the 55 Apex files in the reference corpus, so it costs nothing on valid code. Apex REST verbs (@httpget and friends, plus @remoteaction) join @AuraEnabled and @InvocableMethod as entry points. Without them a @RestResource class looks dead, because its caller is an HTTP client rather than anything in the corpus. Both reported by the review bot on Graphify-Labs#3122.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds a tree-sitter path to the Apex extractor that drives the shared engine walk via an Apex LanguageConfig and falls back to the existing regex extractor when the apex grammar is missing or the file fails to parse (including when tree-sitter returns a tree containing ERROR nodes). Layers Apex-only extraction on top of the engine result in _add_apex_specifics — extends/implements edges, the SObject a trigger fires on, SObjects behind SOQL FROM/SOSL RETURNING, DML statements, and entry-point annotations — reusing the engine's node-id scheme so both halves land on the same nodes. Filters out collection/primitive constructors and common built-in method names (e.g. add, send, serialize) from raw_calls so the cross-file resolver doesn't invent dependencies by binding bare method names to a single class that happens to declare them.
Worth a look
- Builtin-method filter drops legitimate same-file resolved calls —
graphify/extractors/apex.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- raw_calls builtin filter drops legitimate same-file calls to methods named add/get —
graphify/extractors/apex.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Unterminated test file prevents pytest collection —
tests/test_apex_calls.py:331· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- DML statements never produce edges in AST path —
graphify/extractors/apex.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- language pack path bypasses TypeError guard for grammar load failures —
graphify/extractors/engine.py:2884· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1726 functions depend on the 272 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 501 callers, 42 callees - new:
_rebuild_code()— 98 callers, 50 callees - new:
_extract_generic()— 20 callers, 24 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_js()— 83 callers, 3 callees - new:
dispatch_command()— 2 callers, 122 callees - new:
extract_objc()— 27 callers, 9 callees - new:
extract_julia()— 17 callers, 7 callees - …and 25 more — each is listed as a finding
Verification — 1726 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: 1076 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
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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
· 2 grounded finding(s) anchored inline below; 31 more finding(s) on lines outside this diff (see the check run).
| return result | ||
|
|
||
|
|
||
| def _extract_apex_regex(path: Path) -> dict: |
There was a problem hiding this comment.
_extract_apex_regex()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace") | ||
|
|
||
|
|
||
| def _add_apex_specifics(path: Path, root, source: bytes, result: dict) -> None: |
There was a problem hiding this comment.
_add_apex_specifics()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
The Apex extractor has never produced
callsedges, and its docstring says why: no tree-sitter grammar on PyPI. That comment is out of date — aheber/tree-sitter-sfapex exists, is MIT, and was last touched a week ago.It happens to use the same node type and field names as tree-sitter-java for declarations and calls, so the shared engine walk drives it from a config with no special cases. On my-org-butler, 55 Apex files: 592 calls edges where there were 0, across 225 distinct targets, the busiest with 17 incoming — so nothing turned into a god-node.
AgentMemoryand what reaches it, in the graph graphify builds for that repo:The whole repo. Clustering finds 38 communities where the same corpus without calls edges fell into 80 mostly disconnected stars — and because the call structure gives each community a real hub, the hub labels come out as class names rather than
Community N:Not the Java grammar, though — that was the tempting shortcut and it is wrong. Parsing those same 55 files with tree-sitter-java yields 740 ERROR nodes across 53 of them, and it doesn't raise, it just returns a broken tree. The sfapex grammar parses all 55 with zero errors, including
??(Spring '24), user-mode DML andWITH USER_MODE(Spring '23).The regex extractor stays as the fallback, like Pascal's. Without the extra, Apex extracts exactly as before, minus calls. The full suite passes both ways: 4966 with the grammar, 4951 and 63 skipped without.
The dependency is the part worth arguing about. The grammar publishes bindings for npm, Cargo and WASM but nothing for Python, so
tree-sitter-language-packis the only route. It ships proper abi3 wheels, but it downloads the grammar's shared library on first use into a user cache — a fresh install needs network access once. That is weaker than the pinned wheels every other grammar here gets, and I would rather say so than have you find it. If it is a blocker the honest fix is a realtree-sitter-apexwheel, and I have asked upstream for Python bindings.Three things the generic walk cannot know are handled in the extractor rather than by widening the Java-gated branches in the engine:
extends/implements, the SObject a trigger fires on and the one behind a SOQLFROM, and DML statements.new Other()too, since its callee sits in thetypefield rather thanname.Two filters, both chosen from what real code does rather than guessed.
new List<Account>()appears in nearly every method and built aListnode collecting 30 edges. Androws.add('x')bound to whichever class happens to declareadd()— 34 of 41 edges onto one class were list appends from elsewhere. Note the asymmetry: a name declared by many classes is already safe, because the resolver refuses to bind an ambiguous name. That is whyexecuteis deliberately not filtered — every invocable class declares it, so it only ever resolves within a file.One behaviour change to call out: the AST path does not reproduce 11 things the regex path emitted. I classified every one — six were
new X(...)read as a method declaration, four were the word "Merge" in a comment, one wasDatabasetruncated fromDatabase.Batchable. All regex false positives, none a real loss. No existing test needed changing.Tests: 14 in
tests/test_apex_calls.py, checked by mutation — ten plausible wrong implementations, each caught by at least one test. Negative controls: a commented-out call, a local variable named after a method, DML in prose, a collection constructor, and a collection method that must not bind across files.Shared core is touched in two small places: one optional
LanguageConfigfield for grammars only reachable through the pack, and the loader branch that uses it. No existing language behaves differently.Independent of #3106 and #3117.