From 5c5bfd4051a7571657798d3046ddfd64f5a19eaa Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Fri, 23 Oct 2026 12:14:19 -0500 Subject: [PATCH] fix(php): a namespace alias matches whatever case it is written in PHP reads namespace names without regard to case, so after use App\Fields as Field; the name field\FirstName is the same class as Field\FirstName. The qualified-name resolver compared the alias as written, found no import, and read the name relative to the file's own namespace instead, so new field\FirstName() stayed unlinked. The alias is now matched without regard to case. This lands the first half of the contributor's PR. Its second half refused an alias that two namespace { } blocks of one file bind to different namespaces. That also dropped the first block's correct edge, because the use map is file-wide, and the case doesn't occur in real code: across 8 PHP repos (33,820 files), 185 files declare more than one namespace, and none binds one alias twice outside strings. The fix for those files is to scope each use statement to its namespace block, which needs block positions the index doesn't keep yet: a braced namespace gets no namespace node, and neither does a second unbraced one, so it is left for its own change. Validation: the new test fails on main and passes here, with the native kernel and with CODEGRAPH_KERNEL=0. Graphs of php-parser, composer, psalm, monica, laravel and phpunit are byte-identical to main, as are the controls express and flask. Index CPU on laravel, median of 3 interleaved: 53.3 s on main, 52.9 s here. The PHP and Laravel suites pass in both engines (10 files, 67 tests). Original PR #2369 by @danusha2345. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/php-namespace-alias-instantiation.test.ts | 7 +++++++ src/resolution/import-resolver.ts | 5 +++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0893bc..a4aa965c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,6 +159,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a base class like `Message` in `class DynamicMessage final : public Message` now links to the class C++ itself would pick: the one declared in the namespaces around the class, or brought in by the file's `using` declarations and namespace aliases, and the class a `typedef` or `using` alias names. Before, a base class was matched by its name alone, so it could land on a class of the same name in another namespace or another language: in protocolbuffers/protobuf, `DynamicMessage` derived from the JSON parser's internal `Message` class, and in google/leveldb and RocksDB, iterators derived from an unrelated nested `Iterator` class such as the skip list's. The wrong base showed up in `codegraph_explore`'s type hierarchy and in impact, and tied the base's methods to the wrong overrides. A template parameter used as a base no longer links to a class that shares its name, and a base written from the global scope, like `public ::testing::Test`, now links to its class. Re-index C++ projects after upgrading. - In C++ and Rust, a call written with a qualifier, like `Status::OK()`, now links to a method of the class it names, never to one of a class whose name only ends the same way. Before, facebook/rocksdb's calls to `Status::OK()`, `Status::InvalidArgument(…)` and `Status::Corruption(…)` linked to `IOStatus`'s methods of those names, a subclass calling its base, like `return FileSystemWrapper::NewWritableFile(…)` or google/leveldb's `return DB::Put(…)`, linked to another subclass or to the calling method itself, and BurntSushi/ripgrep's `Path::new(…)` on the standard library's `Path` linked to its own `PrinterPath::new`. When classes in several languages share the name, the one in the call's own language is taken, so protocolbuffers/protobuf's C++ `DescriptorPool::generated_pool()` calls, which met its Ruby namesake first and linked to nothing, now link to the C++ method. A C++ call through a template parameter, like `CacheShard::ComputeHash(key)` in rocksdb's `template class ShardedCache`, no longer links to a class that merely shares words with the parameter's name, unless that class is the parameter's default, like `TcParser` in `template `. A template's value parameter, like `const PrefetchOpts& kOpts`, is no longer mistaken for a type parameter, so references to `PrefetchOpts` inside it now link. Re-index C++ and Rust projects after upgrading. - In Ruby, a call on a class written with its namespace, like `Billing::ChargeCard.call(…)` or `::Billing::ChargeCard.call(…)`, now links the caller to that class and to the class method it calls, as `ChargeCard.call(…)` already linked to the class. Before, a class used only through such calls looked like it had no callers. The name is looked up the way Ruby does it, from the namespaces around the call outward, so `Users::Create.call` reaches `Users::Create` and never a `Billing::Create`, a call inside `module Api; module V2` reaches `Api::V2`'s class before another version's, and a library's class like `ActiveRecord::Base` is never taken for a project class of the same last name. Re-index Ruby projects after upgrading. Thanks @james-jang for the report and @drakeo338 for the pull request. (#2503) +- In PHP, a class written through a namespace alias in a different case than its `use` statement, like `new field\FirstName()` after `use App\Fields as Field;`, now links to that class, since PHP reads namespace names without regard to case. Before, the name was read relative to the file's own namespace and stayed unlinked. Re-index PHP projects after upgrading. Thanks @danusha2345. (#2369) - In C++, a call through a class template's specialization written with its arguments, like fmtlib/fmt's `formatter::format(42, ctx)`, no longer links to a member defined for another specialization, like a test's `formatter::format`, and when the template declares no such member, as for `formatter::parse(ctx)`, the call is no longer matched to another class's method of that name, like `native_formatter::parse`. A call to the specialization a member is defined for, and to a template's own members, links as before. Re-index C++ projects after upgrading. - In C++, a function is now named by its declarator, never by something written in the clauses after its parameters. nlohmann/json's constructor `basic_json(CompatibleType&& val) noexcept(noexcept(JSONSerializer::to_json(…)))` was missing from the index, replaced by a method `JSONSerializer::to_json` that doesn't exist and that other `JSONSerializer::to_json(…)` calls linked to; a method declared with `throw(std::bad_alloc)` was indexed as `std::bad_alloc`, and a function with a trailing `requires std::integral` as `std::integral`. Re-index C++ projects after upgrading. - In C and C++, a header or source file named like a test, such as protocolbuffers/protobuf's `conformance_test.h` and `test_runner.h`, now counts as part of the code that `#include`s it, directly or through other headers. Before, calls from that code into it were dropped, so protobuf's conformance suites' `suite_.ReportFailure(…)` and `RunValidInputTest(…)` calls, and its unit tests' calls to the shared `TestUtil` helpers, linked to nothing. A test file that only tests include is still kept out of reach of the rest of your code. Re-index C and C++ projects after upgrading. diff --git a/__tests__/php-namespace-alias-instantiation.test.ts b/__tests__/php-namespace-alias-instantiation.test.ts index 103235c7..009657b6 100644 --- a/__tests__/php-namespace-alias-instantiation.test.ts +++ b/__tests__/php-namespace-alias-instantiation.test.ts @@ -194,4 +194,11 @@ class Personal extends L\\Base `); expect(await edgesFrom('app/Personal.php')).toEqual([]); }); + + it('matches the namespace alias without regard to its case', async () => { + write('app/Blocks/Personal/Fields/FirstName.php', firstName('App\\Blocks\\Personal\\Fields')); + write('app/Blocks/Personal/Personal.php', + consumer('use App\\Blocks\\Personal\\Fields as Field;', 'new field\\FirstName()')); + expect(await instantiated()).toEqual(['App\\Blocks\\Personal\\Fields::FirstName']); + }); }); diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index c05f2682..e0f8f4df 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -2722,8 +2722,9 @@ export function resolvePhpQualifiedClassRef( if (separator === 0) { fqn = name.slice(1); } else { - const head = name.slice(0, separator); - const imp = context.getImportMappings(ref.filePath, ref.language).find((i) => i.localName === head); + // PHP matches a namespace alias without regard to case. + const head = name.slice(0, separator).toLowerCase(); + const imp = context.getImportMappings(ref.filePath, ref.language).find((i) => i.localName.toLowerCase() === head); if (imp) { fqn = imp.source.replace(/^\\/, '') + name.slice(separator); } else {