Skip to content

fix: resolve PHP use imports to file paths#573

Open
stefanws0 wants to merge 1 commit into
tirth8205:mainfrom
stefanws0:fix/php-use-import-resolution
Open

fix: resolve PHP use imports to file paths#573
stefanws0 wants to merge 1 commit into
tirth8205:mainfrom
stefanws0:fix/php-use-import-resolution

Conversation

@stefanws0

@stefanws0 stefanws0 commented Jun 23, 2026

Copy link
Copy Markdown

Linked issue

Closes #574.

Found while using the graph on a Symfony/PHP codebase: importers_of (and the
upstream side of impact-radius) returned 0 for PHP classes that are imported
by hundreds of files.

What & why

PHP use statements had no branch in the parser's _extract_import, so they
fell through to the raw-text fallback (imports.append(text)) and stored the
entire statement as the IMPORTS_FROM edge target. For example,
"use App\Domain\Entity\Job;" was stored instead of the FQN
App\Domain\Entity\Job.

Consequences:

  • importers_of, tests_for, and inheritors_of returned nothing for PHP
    classes, because the query matches edge targets by resolved file path, which
    the raw use ...; text never matches.
  • The upstream side of get_impact_radius saw no PHP importers.
  • resolve_bare_call_targets uses IMPORTS_FROM targets to disambiguate
    cross-file CALLS, so the unparsed targets silently degraded PHP call
    resolution too.

Every other FQN-style language already has an import branch, and Java
additionally resolves import a.b.C; to the class's .java file in
_do_resolve_module. This PR brings PHP to parity:

  • _extract_import: new php branch recording the fully-qualified name of
    each imported symbol, handling as aliases (records the FQN, not the alias),
    grouped use A\B\{C, D as E} (prefix-joined), use function / use const,
    and a leading \.
  • _do_resolve_module: new php branch mirroring Java. It converts \ to
    /, appends .php, and walks up from the importing file to the source root.
    Vendor/global classes with no local file (such as \Exception) stay as the
    bare FQN, exactly like JDK imports.

No new dependencies, no schema changes, and no post-build resolver. The fix
lives entirely in the existing per-language parser paths, consistent with Java.

Impact

In the codebase where this surfaced, importers_of for a core entity returned
0 despite 452 files doing use App\Domain\Entity\Job; (confirmed via
grep). With the fix those use statements resolve to the entity's file. The
behavior is covered by a self-contained test (TestPHPImportResolution) on a
mini PSR-4 project.

How it was tested

uv run pytest tests/test_multilang.py::TestPHPImportResolution -q   # 4 passed (new)
uv run pytest tests/ --tb=short -q                                  # 1388 passed, 1 skipped, 2 xpassed
uv run ruff check code_review_graph/                                # All checks passed!
uv run mypy code_review_graph/parser.py --ignore-missing-imports --no-strict-optional  # Success

Also verified end to end by building a graph for a mini PSR-4 project with the
patched code and confirming query_graph("importers_of", <Job.php>) returns the
two importing files and excludes a non-importing file. It returned nothing
before the change.

Checklist

  • Tests added for new functionality
  • All tests pass: uv run pytest tests/ --tb=short -q
  • Linting passes: uv run ruff check code_review_graph/
  • Type checking passes: uv run mypy code_review_graph/ --ignore-missing-imports --no-strict-optional
  • Lines are at most 100 characters
  • Docs updated where behavior changed (CHANGELOG)

PHP `use` statements had no branch in `_extract_import`, so they fell
through to the raw-text fallback and stored the entire statement
("use App\Domain\Entity\Job;") as the IMPORTS_FROM edge target. As a
result `importers_of`, `tests_for`, `inheritors_of`, and the upstream
side of `get_impact_radius` returned nothing for PHP classes, and the
unresolved targets also degraded cross-file CALLS disambiguation in
`resolve_bare_call_targets`.

Add a PHP branch to `_extract_import` that records the fully-qualified
name of each imported symbol, handling `as` aliases, grouped
`use A\{B, C}`, `use function`/`use const`, and a leading `\`. Add a PHP
branch to `_do_resolve_module` that maps the FQN to an absolute `.php`
path by walking up from the importing file, mirroring the existing Java
resolver. Vendor/global classes with no local file stay as the bare FQN.

Adds TestPHPImportResolution covering project-import resolution, the
vendor/unresolved fallback, alias-records-FQN, and grouped-use expansion.
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 use imports stored as raw statement text; importers_of and impact-radius return 0 for PHP classes

2 participants