Add ClassMap::getAmbiguousFolders to detect paths only differing in casing - #49
Merged
Merged
Conversation
getAmbiguousNamespaces() compared namespace strings and never looked at the paths the ClassMap already holds, so it reported namespaces which only differ in casing even when they live in folders that do not fold into each other. That fires on aws/aws-sdk-php, which uses the Aws namespace, together with its required aws/aws-crt-php dependency, which uses AWS. Their folders are aws-sdk-php/src and aws-crt-php/src/AWS, so nothing collides on any filesystem, yet every project depending on the AWS SDK would have been warned about it, and with composer/composer#13007 dump-autoload --strict-ambiguous would have exited 2. Fold path prefixes rather than namespace prefixes, which reports nothing on the aws tree while still catching the case this is meant to catch. Renamed to getAmbiguousFolders() as it now returns the paths involved, which also lets the consumer point at the folders to rename. Comparing paths additionally catches files that only differ in casing (Foo\Bar and Foo\bar are the same class to PHP, and neither getAmbiguousClasses() nor the old method saw those) and PSR-0 underscore pseudo-namespaces, which never contained a backslash to split on. Also: - stop at the topmost difference in a path, one renamed folder no longer gets reported once per namespace below it - sort the output, it followed Symfony Finder traversal order and so varied between machines - accept a $duplicatesFilter like getAmbiguousClasses() does, so ambiguity in dependencies you do not control can be filtered out. This is only possible now that paths are what gets compared - fold ASCII only, strtolower() is locale dependent before PHP 8.2 and this library supports 7.2+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MZmJZgxdTKD4zekKJDrmi9
Covers the problems found in review: order dependent results, paths of ambiguous classes being ignored, nested differences being hidden behind the topmost one, drive letters and stream wrapper schemes being compared case sensitively, the duplicates filter hiding conflicts with production folders, and the default filter not matching relative paths.
The regex required a slash before the folder name, so a top-level tests folder in a relative path was never ignored. The default is now shared between getAmbiguousClasses and getAmbiguousFolders.
ASCII folding preserves offsets, so the folded prefixes can be cut from the folded path in lockstep with the original.
The map follows filesystem traversal order, which is not stable across machines, and the first prefix seen for a folder decided what was reported for every later one.
A folder renamed to a different casing with stale copies left behind defines the same classes twice, so the second copy of every file only ended up in the ambiguous classes and the folders were never compared.
Renaming the topmost folder does not make its children match each other, so src/Foo/Bar and src/foo/bar still collide after src/foo is renamed and the user would just get a new warning. Prefixes are now grouped by folded path and by the casing of their own name, which reports every level that differs by itself while not repeating a parent's difference for children written the same way.
Absolute paths keep the casing they were given, so two autoload rules using C:/ and c:/ ended up reported as ambiguous although drive letters and stream wrapper schemes are never case sensitive.
Skipping filtered paths before comparing them hid a test folder merging with a production folder, which is still a problem for the latter. Sets are now left out only when every path below them is filtered.
substr returns false instead of an empty string on PHP < 8 when the offset reaches the end of the string, which made PHPStan lose the non-empty-string type of the reported paths on PHP 7.2.
…thing The sorted paths let each one skip the folders it shares with the previous path, only the first casing seen per folder is stored until a second one shows up, and the duplicates filter is only evaluated once something was found.
This was referenced Sep 25, 2026
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
ClassMap::getAmbiguousFolders(), which returns sets of paths that only differ in casing and therefore merge into a single file or folder on case-insensitive filesystems (Windows, macOS). This supports composer/composer#13007.This builds on @PrinsFrank's #44 (its two commits are included as-is) and supersedes it, with review fixes on top:
getAmbiguousClasses()too.The first commit adds failing tests for all of the above, and each following commit fixes one of them.