Skip to content

fix(extract): stop a built-in base class from inheriting across languages (#2812) - #2939

Open
ousamabenyounes wants to merge 1 commit into
Graphify-Labs:v8from
ousamabenyounes:fix/issue-2812
Open

fix(extract): stop a built-in base class from inheriting across languages (#2812)#2939
ousamabenyounes wants to merge 1 commit into
Graphify-Labs:v8from
ousamabenyounes:fix/issue-2812

Conversation

@ousamabenyounes

Copy link
Copy Markdown
Contributor

Fixes #2812.

The bug

class FooApiException extends \Exception in PHP mints a sourceless Exception
stub, because the base class is not defined in the corpus. _rewire_unique_stub_nodes
then binds that stub to the one same-labelled real definition found anywhere in
the corpus — and in a PHP + TypeScript monorepo that definition is an unrelated
TypeScript class Exception. The reporter's ~6k-file repo got 12 phantom
inherits edges from first-party PHP exception classes onto one TS class, which
pushed it into the top-10 god nodes and produced a false cross-community bridge
question.

The fix

The rewire now refuses a supertype edge when the referring file's language names
its own built-in throwable and the resolved target lives in a different language
family. Three deliberate choices:

  • Per edge, not per stub. One sourceless Exception stub collects referrers
    from every language that names it. Deciding on the stub's referrer families as a
    set would let a single TypeScript referrer re-open the cross-language bind for
    the PHP ones. The check reads edge["source_file"], and sits next to the
    existing C#-scoped carve-out in the same loop.
  • Built-in throwables only, not a blanket family gate on the type path. A corpus
    really can declare BookStore in one language and subclass it from another —
    test_extract_rewires_unique_inheritance_stub_to_real_definition pins exactly
    that, and this PR leaves it passing untouched. But Exception named from a PHP
    file is always PHP's own, so a same-named class in another language cannot be
    what it refers to.
  • Case-folded for case-insensitive languages. PHP resolves class names
    case-insensitively, so extends \exception names the same built-in
    (mirrors the existing real_by_label / real_by_label_ci pair, Case-folded cross-file resolution collides distinct identifiers (Path/PATH) → false super-hubs in case-sensitive languages #1581).

_LANGUAGE_BUILTIN_GLOBALS was not reused: it is a flat set consulted at call
sites and has no per-language scoping, which is what this path needs.

Relationship to #2365

#2365 proposes hoisting the interop family gate to every unique match, which
would also close this issue but changes existing behaviour — it has to rewrite
test_extract_rewires_unique_inheritance_stub_to_real_definition, whose
Python↔C# rewire is currently intended. This PR is additive and takes the
narrower position that only a language's own built-ins can never be foreign.
If you prefer the blanket gate, #2365 supersedes this and I'll close it.

Test verification (RED → GREEN)

RED — new tests against unmodified v8 (b2cd362), production file reverted:

$ git checkout origin/v8 -- graphify/extract.py
$ uv run --frozen pytest tests/test_extract.py -q -k rewire --tb=line
FAILED tests/test_extract.py::test_rewire_does_not_bind_supertype_stub_across_language
FAILED tests/test_extract.py::test_rewire_builtin_supertype_guard_folds_case_insensitive_languages
FAILED tests/test_extract.py::test_rewire_builtin_supertype_guard_is_per_edge_not_per_stub
3 failed, 8 passed, 191 deselected, 1 warning in 0.54s

$ uv run --frozen pytest tests/test_multilang.py::test_php_builtin_base_class_never_inherits_from_cross_language_class -q --tb=line
FAILED tests/test_multilang.py::test_php_builtin_base_class_never_inherits_from_cross_language_class
1 failed, 1 warning in 0.31s

The failure is the reported leak verbatim:

E   AssertionError: PHP supertype leaked cross-language:
    {'source': 'packages_fooapiexception_fooapiexception',
     'target': 'app_exception_exception', 'relation': 'inherits', ...}

GREEN — same tests with the fix:

$ uv run --frozen pytest tests/test_extract.py -q -k rewire
11 passed, 191 deselected, 1 warning in 0.56s

$ uv run --frozen pytest tests/test_multilang.py::test_php_builtin_base_class_never_inherits_from_cross_language_class -q
1 passed, 1 warning in 0.26s

Full suite, unchanged against the v8 baseline (the 4 failures are pre-existing
in my local environment — 3 test_ollama backend-detection tests read env vars I
have set, and test_collect_files_skips_hidden — all 4 fail identically on clean
v8):

$ uv run --frozen pytest tests/ -q
4 failed, 4882 passed, 11 skipped, 4 warnings in 131.39s

ruff check clean on the touched files, and the five tools.skillgen
validators (--check, --audit-coverage, --schema-singleton,
--monolith-roundtrip, --always-on-roundtrip) all pass.

…ages (Graphify-Labs#2812)

A class that extends one of its own language's built-in exception types —
`class FooApiException extends \Exception` in PHP — mints a sourceless
`Exception` stub, and `_rewire_unique_stub_nodes` bound that stub to the one
same-labelled real definition anywhere in the corpus. In a PHP + TypeScript
monorepo that definition was an unrelated TypeScript `Exception` class, so PHP
exception classes inherited from a TS one and inflated it into a god node.

The rewire now refuses a supertype edge whose referring file names its own
language's built-in throwable while the resolved target lives in another
language family. The decision is per edge, next to the existing C#-scoped
carve-out, so one shared stub still resolves for referrers whose language does
own that class, and PHP's case-insensitive identifiers are folded. Legitimate
cross-language type rewires are unchanged.

@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

Blocks cross-language stub rewiring for built-in exception types in _rewire_unique_stub_nodes: a PHP class FooApiException extends \Exception no longer fuses onto a same-named TypeScript Exception class. Adds per-family built-in throwable tables (_LANGUAGE_BUILTIN_BASE_CLASSES plus a case-folded companion) and a per-edge _names_own_builtin_base guard that keeps the stub as the external base only when the referring and target families differ, leaving same-language rewires intact.

No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2038 functions depend on the 860 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 475 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_js() — 80 callers, 3 callees
  • new: dispatch_command() — 2 callers, 119 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: collect_files() — 17 callers, 6 callees
  • …and 23 more — each is listed as a finding

Verification — 2038 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: 1890 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_rewire\_unique\_stub\_nodes.

The verifier did not have enough to check \_rewire\_unique\_stub\_nodes, 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: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

· 31 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.

PHP 'extends Exception' (bare built-in) resolves onto an unrelated same-named TS class — cross-language inherits collision

1 participant