Skip to content

fix(php): prove an unaliased global class use absent from the project - #2705

Merged
DeusData merged 4 commits into
mainfrom
fix/php-global-use-known-absent
Oct 19, 2026
Merged

DeusData merged 4 commits into
mainfrom
fix/php-global-use-known-absent

Conversation

@DeusData

Copy link
Copy Markdown
Owner

What

PHP files record their import evidence as complete (php_imports_complete) only when every use either resolved to a project node or is proven absent from the project. Only then can the PHP LSP trust the bindings and type receivers through them (#1186 / #2520).

A global-namespace class use such as use Exception; had no absence proof. One such line, common in almost every PHP project, left the whole file's bindings unknown. Its project imports then reached the PHP LSP as File QNs again, so typed resolution fell back to name guesses.

An unaliased global class use is now proven absent when no PHP file in the project declares a class-like symbol of that name: Class, Interface, Trait, Enum, File or Module. Namespaced ones are included, so the proof stays conservative. Aliased global uses (use X as Y;) keep the old, unknown verdict.

Evidence

Corpus run on all 27 PHP corpus repos, against a clean build of main ea5c1f7.

venue CALLS attributes changed INHERITS USAGE TESTS
parallel +10,686 / -13,416 8,578 -360 -1,484 +4,770 / -5,892
sequential +10,500 / -13,587 8,732 -360 -1,404 +4,747 / -5,982

Also moving (par / seq): IMPLEMENTS -6 / -5, OVERRIDE -11 / -10, CONFIGURES +13 -63 / +13 -61, CALL_REFERENCE +3 / +3, HTTP_CALLS +1 / +1, SPAWNS 0 / +2. SEMANTICALLY_RELATED moves +1,047 -1,004 / +1,011 -889; that pass is TF-IDF over the whole repo, and these edges were not judged one by one. Every other edge type is unchanged on both venues. Parallel covers 27 repos and sequential 26: sebastianbergmann/phpunit hangs in sequential mode on main as well.

CALLS by strategy (par): removed suffix_match 12,008 and unique_name 1,394; added php_method_inherited 8,486, php_static_resolved 1,873 and php_self_static 307. Sequential also removes 446 import_map guesses that parallel never made (new NullOutput bound to Console\View\Components\Component, new ConstraintViolationList bound to Core\Validation\Validator), and adds 19 livewire Route::get registrations that parallel already had.

Judged sample: 105 edges, seed 20261017, 5 per type x strategy x direction, with source snippets (screened). Questionable cases were checked against the pinned source.

  • CALLS (51): 44 correct, 6 mixed, 1 leaning correct.
    • Removals are name guesses that bound a vendor or builtin receiver to an unrelated project method, for example $this->routeCollection->add to Bus\Batch::add, $session->get to BroadcastManager::get and Str::of to a global helper of. Also removed: parent:: calls bound to the class's own method, and $this->x() bound to magic __call when a used trait defines x. The trait calls now reach the trait method, at unique_name 0.38.
    • Additions are typed php_* edges, for example parent::setUp to the project TestCase, Server::get and $this->request->getParam to RequestInterface.
    • Mixed (snipe-it, 3 call sites, 6 rows): CustomTapeLabel extends Label and uses the trait HasCustomLabelSupports. The calls $this->getSupportAssetTag(), getSupportLogo() and getSupport1DBarcode() used to go to Label (php_method_inherited, 0.95). They now go to the trait's method of the same name (suffix_match, 0.02). PHP gives a trait method precedence over an inherited one, so the new target is the right one, but its confidence is low.
    • Leaning correct (PrestaShop, 1): Logger::addLog under use Logger;. No class named Logger exists anywhere in the repository, so the removed guess (PrestaShopLoggerCore::addLog, unique_name 0.38) named a different class.
  • Filament ComponentAttributeBag, checked against the source:
    • TextColumn and TextEntry import Illuminate\View\ComponentAttributeBag and alias filament's own class as FilamentComponentAttributeBag, so the removed edges to filament's ComponentAttributeBag::style were wrong.
    • In filament's ComponentAttributeBag::__call, parent::hasMacro and BaseComponentAttributeBag::hasMacro both call the vendor parent, so the removed edge to the class's own hasMacro was wrong as well.
  • Other edge types (34): all correct.
    • IMPLEMENTS, INHERITS, OVERRIDE and USAGE removals all name vendor or builtin types: Saloon Request/Authenticator, ApiPlatform ProviderInterface, Laminas FilterInterface, builtin Stringable, PHPUnit and Orchestra TestCase, Composer Event, Monolog Logger.
    • CONFIGURES, CALL_REFERENCE and HTTP_CALLS additions are typed. CONFIGURES also moves from implementation guesses (0.28) to the declared type (0.95): nextcloud ISharingRegistry (6 edges) and koel Preference. The added edges include first-class callables and the firefly-iii GitHub releases call.
  • TESTS (10): they follow the CALLS verdicts.

Tests (parallel, both resolvers):

  • parallel_php_lsp_types_imported_project_receivers_issue1186.
  • parallel_php_lsp_types_imported_project_receivers_unknown_bindings_issue1186: the same fixture plus use Exception;. Without the fix the typed php_* call to lib/Services/Svc.php is lost and a wrong name guess into lib/Other/Svc.php appears; with it, both are right.

Suites on this branch (a7f24ea), ASan test-runner: parallel, pipeline, php_lsp, extraction_imports, edge_imports, incremental, 1,026 passed. Lint: lint-memory-core, the NOLINT check, Homebrew clang-format and CI's cppcheck 2.20 on pass_pkgmap.c are all clean.

Head 4ae1909 merges main 27c3303. Main has not touched pass_pkgmap.c or tests/test_parallel.c since 658639e, and merge-tree is clean.

A PHP file's import evidence is written as complete only when every `use`
either resolved or is proven absent. A global-namespace class use such as
`use Exception;` had no absence proof, so one such line left the whole
file's bindings unknown, and its project imports reached the PHP LSP as
File QNs again (the #2520 path).

An unaliased global class use is now proven absent when no PHP file
declares a class-like symbol of that name (Class, Interface, Trait, Enum,
File or Module; namespaced ones included, so the proof stays
conservative). Aliased global uses keep the old, unknown verdict.

Signed-off-by: Martin Vogel <[email protected]>
@DeusData
DeusData merged commit d4980ad into main Oct 19, 2026
29 checks passed
DeusData pushed a commit that referenced this pull request Oct 20, 2026
When the PHP LSP knows a receiver's class but the class is not in the
index, it emits php_method_typed_unindexed at 0.55, below the LSP
confidence floor. The resolver skipped that row and the weak short-name
fallback bound the call to a same-named project method: `$file->getUid()`
on a vendor File became a CALLS edge to App\Item::getUid.

#2520 turns this row into php_external_receiver when a vendor `use`
proves the class external, but two cases still reach the fallback. A
global function import (`use function sprintf;`) leaves the file's import
bindings unproven, since #2705 proves only class imports absent. And a
vendor type that arrives through a return type
(`$repository->findFile()->getUid()`) has no `use` in the calling file
at all.

Both resolvers now drop a unique_name or suffix_match edge when the LSP
row names a receiver class that no project class-like symbol shares by
simple name. A project class with that name keeps the fallback, because
the PHP LSP does not follow every cross-file type.

The test pair covers both cases, through pass_calls.c and pass_parallel.c.
Each case is RED on main on its own (vendorCall, chainedVendorCall);
calls on a project-typed receiver and on $this still resolve. On a
211-file TYPO3 project PHP CALLS edges go from 1083 to 1059
(-18 suffix_match, -6 unique_name; typed php_* edges unchanged): 25
removed, and 1 that a removed edge on the same caller had absorbed.

Signed-off-by: Wolfgang Klinger <[email protected]>
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