fix(extract): resolve Kotlin member calls by receiver type - #1965
fix(extract): resolve Kotlin member calls by receiver type#1965oleksii-tumanov wants to merge 12 commits into
Conversation
…aphify-Labs#1965) Ports oleksii-tumanov's upstream fix (Graphify-Labs#1965) into this fork's extraction/type-resolution layer, extending the annotation-argument capture and Kotlin annotation collector added in this branch. - collect method-scoped Kotlin receiver types from class properties, constructor parameters, getter returns, and local constructor/getter bindings, threaded through walk_calls alongside the existing Graphify-Labs#2241 extra_locals plumbing - register a corpus-level kotlin_member_calls resolver (_resolve_kotlin_member_calls) that resolves a member call only when the receiver type and the selected method's owner are unambiguous, guarded by package/import scope, inheritance, extension-function dispatch, companion binding, and lexical shadowing - preserve existing Kotlin dispatch for super, object/companion-qualified calls, private visibility, and inner-class dispatch receivers This closes the receiver-typing gap tracked by Graphify-Labs#1965: a Kotlin Spring Controller -> Service -> Repository chain (constructor injection) now produces resolved calls edges end-to-end instead of 0, per the new test_controller_service_repository_chain_resolves_end_to_end. Co-authored-by: fred-agent <fred-agent@bizzabo.com>
0dc2c1d to
0bddff6
Compare
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 Kotlin corpus-wide member-call resolution and an incremental invalidation preflight so unchanged callers get re-queued when type/method/factory inventories change. _kotlin_incremental_member_callers reads the existing graph, compares persisted per-file symbol inventories against fresh extraction, and returns dependant Kotlin files to re-extract; it fails closed by refreshing the whole live Kotlin corpus on incomplete parses, pre-v1 graphs, or path rehoming. extract_kotlin now stamps a _kotlin_member_symbol_inventory_complete marker, dispatch_command wires the preflight into incremental extract and carries the new _kotlin_* markers through the reconciliation context, and _resolve_kotlin_member_calls resolves member calls through a single exactly-matched receiver type.
Worth a look
- Sentinel-node write assigns nodes to detached dict for zero-node results —
graphify/extract.py:6129· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Kotlin resolver treats dotted relative receiver types as absolute FQNs —
graphify/extract.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
- per_file mutation may write to shared/None list without persisting into per_file —
graphify/extract.py:5953· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Stamping loop mutates per_file entries without writing them back —
graphify/extract.py:5960· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Sentinel-node fabrication loop also drops the created dict for None entries —
graphify/extract.py:6135· 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 — 2040 functions depend on the 597 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 478 callers, 42 callees - new:
_rebuild_code()— 105 callers, 51 callees - new:
_extract_generic()— 18 callers, 27 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
dispatch_command()— 2 callers, 120 callees - new:
extract_js()— 80 callers, 3 callees - new:
_get_extractor()— 26 callers, 6 callees - …and 40 more — each is listed as a finding
Verification — 2040 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: 1993 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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 `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_kotlin.
The verifier did not have enough to check extract\_kotlin, 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
Could not verify: Could not verify \_rebuild\_code.
The verifier did not have enough to check \_rebuild\_code, 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 `watch_path` is annotated `Path` — outside the synthesizable primitive/collection set
· 5 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| }) | ||
|
|
||
|
|
||
| def _kotlin_incremental_member_callers( |
There was a problem hiding this comment.
_kotlin_incremental_member_callers()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
| tables: dict[tuple[int, int], dict[str, str]] = {} | ||
|
|
||
| def process_owner(owner_node) -> None: |
There was a problem hiding this comment.
process_owner()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert len(_incremental_member_edges(root)) == 1 | ||
|
|
||
|
|
||
| def test_watch_requeues_caller_when_factory_is_added_and_removed( |
There was a problem hiding this comment.
test_watch_requeues_caller_when_factory_is_added_and_removed()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert not _factory_call_targets(root) & {widget_render, product_render} | ||
|
|
||
|
|
||
| def test_watch_inventory_requeue_survives_invocation_style_changes( |
There was a problem hiding this comment.
test_watch_inventory_requeue_survives_invocation_style_changes()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert len(_incremental_member_edges(root)) == 1 | ||
|
|
||
|
|
||
| def test_cli_requeues_caller_when_factory_is_added_and_removed( |
There was a problem hiding this comment.
test_cli_requeues_caller_when_factory_is_added_and_removed()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
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 Kotlin incremental member-call invalidation via _kotlin_incremental_member_callers in extract.py, wired into dispatch_command to re-queue unchanged caller files when a type/method/factory symbol inventory changes. Tags extract_kotlin output with a _kotlin_member_symbol_inventory_complete marker and propagates Kotlin inventory fields (_kotlin_fqn, inventory version/contents) through the reconciliation context node builder. Introduces _resolve_kotlin_member_calls for fail-closed corpus-wide receiver-type resolution.
Worth a look
- Stamp step also writes to a discarded dict for None/empty entries —
graphify/extract.py:5984· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- per_file mutation via
or {}discards sentinel node when entry is falsy —graphify/extract.py:6160· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Docs-only build now eagerly imports AST extractor —
graphify/cli.py:3405· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Kotlin call deduplication suppresses calls when any edge already exists —
graphify/extract.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
- Root-relative Kotlin paths are dropped by direct is_file checks —
graphify/extract.py:3899· 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 — 2045 functions depend on the 602 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 478 callers, 42 callees - new:
_rebuild_code()— 105 callers, 51 callees - new:
_extract_generic()— 18 callers, 29 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
dispatch_command()— 2 callers, 120 callees - new:
extract_js()— 80 callers, 3 callees - new:
_get_extractor()— 26 callers, 6 callees - …and 40 more — each is listed as a finding
Verification — 2045 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: 1998 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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 `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_kotlin.
The verifier did not have enough to check extract\_kotlin, 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
Could not verify: Could not verify \_rebuild\_code.
The verifier did not have enough to check \_rebuild\_code, 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 `watch_path` is annotated `Path` — outside the synthesizable primitive/collection set
· 5 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| }) | ||
|
|
||
|
|
||
| def _kotlin_incremental_member_callers( |
There was a problem hiding this comment.
_kotlin_incremental_member_callers()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
| tables: dict[tuple[int, int], dict[str, str]] = {} | ||
|
|
||
| def process_owner(owner_node) -> None: |
There was a problem hiding this comment.
process_owner()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert len(_incremental_member_edges(root)) == 1 | ||
|
|
||
|
|
||
| def test_watch_requeues_caller_when_factory_is_added_and_removed( |
There was a problem hiding this comment.
test_watch_requeues_caller_when_factory_is_added_and_removed()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert not _factory_call_targets(root) & {widget_render, product_render} | ||
|
|
||
|
|
||
| def test_watch_inventory_requeue_survives_invocation_style_changes( |
There was a problem hiding this comment.
test_watch_inventory_requeue_survives_invocation_style_changes()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert len(_incremental_member_edges(root)) == 1 | ||
|
|
||
|
|
||
| def test_cli_requeues_caller_when_factory_is_added_and_removed( |
There was a problem hiding this comment.
test_cli_requeues_caller_when_factory_is_added_and_removed()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
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 Kotlin member-call incremental invalidation via _kotlin_incremental_member_callers, which reads the existing graph, compares per-file symbol inventories, and requeues unchanged callers when the corpus-wide type/method/factory inventory changes; dispatch_command wires this into extract before AST extraction and prints a re-queue notice. Stamps a completeness sentinel (_kotlin_member_symbol_inventory_complete) in extract_kotlin and carries the new _kotlin_* markers through the incremental context graph. Centralizes Kotlin-file detection in _is_kotlin_source_file and handles a one-time migration for pre-inventory graphs plus path-rehoming and parse-failure fallbacks that refresh the live Kotlin corpus.
Worth a look
- Kotlin extraction reads a different package variable than it assigns —
graphify/extractors/engine.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
- Kotlin extraction references an undefined package variable —
graphify/extractors/engine.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
- kotlin_package referenced when only _pkg computed —
graphify/extractors/engine.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
- Incomplete test function leaves file syntactically invalid —
tests/test_kotlin_member_calls.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
- changed-file inventory comparison keyed by identity(path) not persisted_identity —
graphify/extract.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
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2087 functions depend on the 662 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 478 callers, 42 callees - new:
_rebuild_code()— 105 callers, 52 callees - new:
_extract_generic()— 18 callers, 29 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
dispatch_command()— 2 callers, 120 callees - new:
extract_js()— 80 callers, 3 callees - new:
_get_extractor()— 26 callers, 6 callees - …and 40 more — each is listed as a finding
Verification — 2087 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: 2040 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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 `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_kotlin.
The verifier did not have enough to check extract\_kotlin, 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 \_resolve\_kotlin\_import\_targets.
The verifier did not have enough to check \_resolve\_kotlin\_import\_targets, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
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
Could not verify: Could not verify run\_language\_resolvers.
The verifier did not have enough to check run\_language\_resolvers, 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 `paths` is annotated `Sequence` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_rebuild\_code.
The verifier did not have enough to check \_rebuild\_code, 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 `watch_path` is annotated `Path` — outside the synthesizable primitive/collection set
· 5 grounded finding(s) anchored inline below; 43 more finding(s) on lines outside this diff (see the check run).
| return str(value).lower().endswith((".kt", ".kts")) | ||
|
|
||
|
|
||
| def _kotlin_incremental_member_callers( |
There was a problem hiding this comment.
_kotlin_incremental_member_callers()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| tables: dict[tuple[int, int], dict[str, str]] = {} | ||
| owner_tables: dict[tuple[int, int], dict[str, str]] = {} | ||
|
|
||
| def process_owner(owner_node) -> None: |
There was a problem hiding this comment.
process_owner()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert len(_incremental_member_edges(root)) == 1 | ||
|
|
||
|
|
||
| def test_watch_requeues_caller_when_factory_is_added_and_removed( |
There was a problem hiding this comment.
test_watch_requeues_caller_when_factory_is_added_and_removed()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert not _factory_call_targets(root) & {widget_render, product_render} | ||
|
|
||
|
|
||
| def test_watch_inventory_requeue_survives_invocation_style_changes( |
There was a problem hiding this comment.
test_watch_inventory_requeue_survives_invocation_style_changes()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert len(_incremental_member_edges(root)) == 1 | ||
|
|
||
|
|
||
| def test_cli_requeues_caller_when_factory_is_added_and_removed( |
There was a problem hiding this comment.
test_cli_requeues_caller_when_factory_is_added_and_removed()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Hi @safishamsi, I refreshed this PR onto the current It now uses method-scoped receiver facts and fails closed for ambiguous or shadowed bindings, including incremental CLI and watch behavior. The current head is green on Python 3.10/3.12, skillgen, security, and the Graphify gate. Ready for review when convenient. Thanks! |
Summary
.kt/.ktsinputs, CLI updates, and watch rebuildsFixes #1699
Result
The #1699 fixture now emits one inferred function-level
callsedge for each reported receiver shape, with confidence0.85. Ambiguous, reassigned, external, builtin, namespace-colliding, and lexically shadowed receivers remain unresolved instead of producing a potentially wrong edge. Receiver facts stay isolated across nested classes, companions, object literals, and enum-entry bodies.Testing
pytest -q tests/test_kotlin_member_calls.py tests/test_kotlin_grammar.py tests/test_language_resolvers.py(58 passed)pytest -q tests/test_languages.py -k kotlin(12 passed, 368 deselected)pytest -q tests/(4916 passed, 11 skipped)ruff check .(passed)python -m tools.skillgen --check(passed)