Skip to content

Commit 48a7bbe

Browse files
SanderMullerclaude
authored andcommitted
Do not autoload a class whose name is an already defined function
AutoloadFunctionsSourceLocator runs the autoloaders collected from bootstrap files to find a class, without first checking whether the name is already a defined function. A catch-all bootstrap autoloader then resolves a name like VeeWee\Xml\Dom\Builder\value to the file that defines the function and plain-includes it. That file was already loaded once - veewee/xml ships one function per PSR-4 path and requires it from its files-autoload bootstrap - so the second include fatally redeclares the function and the worker dies with "Cannot redeclare function ...". The common real-world autoloader here is PHP_CodeSniffer's, loaded via bootstrapFiles because the package ships no Composer autoload metadata; it falls back to Composer's findFile() for any name and includes with a plain include. The name is probed as a class and the file holds a function, so running the autoloader cannot find a class there; it only re-includes the file. Return early when the name is already a defined function, the same way the locator already returns early for an existing class, interface or trait. A class that genuinely exists under that name in another file is still found by the later source locators in the chain. Wrapping the autoloader call in FileReadTrapStreamWrapper (as AutoloadSourceLocator does) was considered but is not viable here: these bootstrap autoloaders run for real, and some read a file for its return value (a loader delegating via "$loader = include ...; $loader->loadClass()") or write cache files, which the trap breaks. e2e/bug-14988 reproduces it: an unguarded one-function-per-file, a bootstrap that requires it, and a catch-all autoloader shaped like PHP_CodeSniffer's. Before the change the worker fatals with the redeclare; after it, analysis completes. It also covers a class and a function that share a name in separate files (both reachable only through that bootstrap autoloader, not Composer), asserting the class still resolves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b88ca9c commit 48a7bbe

12 files changed

Lines changed: 157 additions & 0 deletions

File tree

.github/workflows/e2e-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ jobs:
128128
cd e2e/bug-14514
129129
composer install
130130
../../bin/phpstan analyze bug-14515.php
131+
- script: |
132+
cd e2e/bug-14988
133+
composer install
134+
../../bin/phpstan analyse
131135
- script: |
132136
cd e2e/bug-14724
133137
composer install

e2e/bug-14988/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/vendor

e2e/bug-14988/classes/gadget.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Redeclare\Builder;
4+
5+
class gadget
6+
{
7+
8+
public int $size = 3;
9+
10+
}

e2e/bug-14988/composer.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"autoload": {
3+
"files": [
4+
"pkg/bootstrap.php"
5+
]
6+
}
7+
}

e2e/bug-14988/composer.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Redeclare;
4+
5+
use function is_file;
6+
use function spl_autoload_register;
7+
use function strlen;
8+
use function strncmp;
9+
use function substr;
10+
11+
/**
12+
* Stands in for the real-world autoloader that reproduces phpstan/phpstan#14988: PHP_CodeSniffer's
13+
* own autoloader, loaded via bootstrapFiles for a package that ships no Composer autoload metadata.
14+
* It shares the three properties that make it fatal:
15+
*
16+
* 1. It is registered as a string callable ("Class::method"), which spl_autoload_functions()
17+
* normalises to ['Class', 'method'] - an array whose first element is a string, not a
18+
* ClassLoader object - so it survives bin/phpstan's Composer ClassLoader exclusion.
19+
* 2. It is catch-all: it resolves names outside its own namespace.
20+
* 3. It uses a plain include (not include_once), so a file already loaded is executed again.
21+
*/
22+
final class Autoloader
23+
{
24+
25+
public static function load(string $class): void
26+
{
27+
if (strncmp($class, 'Redeclare\\Builder\\', 18) !== 0) {
28+
return;
29+
}
30+
31+
$name = substr($class, strlen('Redeclare\\Builder\\'));
32+
foreach (['/classes/', '/pkg/'] as $dir) {
33+
$file = __DIR__ . $dir . $name . '.php';
34+
if (is_file($file)) {
35+
include $file;
36+
return;
37+
}
38+
}
39+
}
40+
41+
}
42+
43+
spl_autoload_register('Redeclare\\Autoloader::load', true, true);

e2e/bug-14988/phpstan.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
parameters:
2+
level: 0
3+
paths:
4+
- src
5+
bootstrapFiles:
6+
- custom-autoloader.php
7+
ignoreErrors:
8+
# The probe deliberately references a name that is a function, not a class. The point of
9+
# this fixture is that analysis completes instead of fatally re-including the function file.
10+
-
11+
message: '#^Class Redeclare\\Builder\\thing not found\.$#'
12+
path: src/probe.php

e2e/bug-14988/pkg/bootstrap.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php declare(strict_types=1);
2+
3+
if (!function_exists('Redeclare\Builder\thing')) {
4+
require_once __DIR__ . '/thing.php';
5+
}
6+
7+
if (!function_exists('Redeclare\Builder\gadget')) {
8+
require_once __DIR__ . '/gadget.php';
9+
}

e2e/bug-14988/pkg/gadget.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Redeclare\Builder;
4+
5+
function gadget(): int
6+
{
7+
return 1;
8+
}

e2e/bug-14988/pkg/thing.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Redeclare\Builder;
4+
5+
function thing(): string
6+
{
7+
return 'thing';
8+
}

0 commit comments

Comments
 (0)