Skip to content

Commit 0c22f13

Browse files
authored
Support multiple anonymous class definitions on the same line
1 parent 85351dd commit 0c22f13

13 files changed

+298
-6
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ services:
311311
options:
312312
preserveOriginalNames: true
313313

314+
-
315+
class: PHPStan\Parser\AnonymousClassVisitor
316+
tags:
317+
- phpstan.parser.richParserNodeVisitor
318+
314319
-
315320
class: PHPStan\Parser\ArrayFilterArgVisitor
316321
tags:

src/Analyser/NodeScopeResolver.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
use PHPStan\Node\UnreachableStatementNode;
118118
use PHPStan\Node\VariableAssignNode;
119119
use PHPStan\Node\VarTagChangedExpressionTypeNode;
120+
use PHPStan\Parser\AnonymousClassVisitor;
120121
use PHPStan\Parser\ArrowFunctionArgVisitor;
121122
use PHPStan\Parser\ClosureArgVisitor;
122123
use PHPStan\Parser\Parser;
@@ -855,7 +856,7 @@ private function processStmtNode(
855856
if ($stmt->name === null) {
856857
throw new ShouldNotHappenException();
857858
}
858-
if ($stmt->getAttribute('anonymousClass', false) === false) {
859+
if ($stmt->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS, false) === false) {
859860
$classReflection = $this->reflectionProvider->getClass($stmt->name->toString());
860861
} else {
861862
$classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope);

src/Broker/AnonymousClassNameHelper.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node;
66
use PHPStan\File\FileHelper;
77
use PHPStan\File\RelativePathHelper;
8+
use PHPStan\Parser\AnonymousClassVisitor;
89
use PHPStan\ShouldNotHappenException;
910
use function md5;
1011
use function sprintf;
@@ -32,9 +33,17 @@ public function getAnonymousClassName(
3233
$this->fileHelper->normalizePath($filename, '/'),
3334
);
3435

36+
/** @var int|null $lineIndex */
37+
$lineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX);
38+
if ($lineIndex === null) {
39+
$hash = md5(sprintf('%s:%s', $filename, $classNode->getStartLine()));
40+
} else {
41+
$hash = md5(sprintf('%s:%s:%d', $filename, $classNode->getStartLine(), $lineIndex));
42+
}
43+
3544
return sprintf(
3645
'AnonymousClass%s',
37-
md5(sprintf('%s:%s', $filename, $classNode->getStartLine())),
46+
$hash,
3847
);
3948
}
4049

src/Parser/AnonymousClassVisitor.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
use function count;
8+
9+
class AnonymousClassVisitor extends NodeVisitorAbstract
10+
{
11+
12+
public const ATTRIBUTE_ANONYMOUS_CLASS = 'anonymousClass';
13+
public const ATTRIBUTE_LINE_INDEX = 'anonymousClassLineIndex';
14+
15+
/** @var array<int, non-empty-list<Node\Stmt\Class_>> */
16+
private array $nodesPerLine = [];
17+
18+
public function beforeTraverse(array $nodes): ?array
19+
{
20+
$this->nodesPerLine = [];
21+
return null;
22+
}
23+
24+
public function enterNode(Node $node): ?Node
25+
{
26+
if (!$node instanceof Node\Stmt\Class_ || !$node->isAnonymous()) {
27+
return null;
28+
}
29+
30+
$node->setAttribute(self::ATTRIBUTE_ANONYMOUS_CLASS, true);
31+
$this->nodesPerLine[$node->getStartLine()][] = $node;
32+
33+
return null;
34+
}
35+
36+
public function afterTraverse(array $nodes): ?array
37+
{
38+
foreach ($this->nodesPerLine as $nodesOnLine) {
39+
if (count($nodesOnLine) === 1) {
40+
continue;
41+
}
42+
for ($i = 0; $i < count($nodesOnLine); $i++) {
43+
$nodesOnLine[$i]->setAttribute(self::ATTRIBUTE_LINE_INDEX, $i + 1);
44+
}
45+
}
46+
47+
$this->nodesPerLine = [];
48+
return null;
49+
}
50+
51+
}

src/Reflection/BetterReflection/BetterReflectionProvider.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PHPStan\File\FileHelper;
2525
use PHPStan\File\FileReader;
2626
use PHPStan\File\RelativePathHelper;
27+
use PHPStan\Parser\AnonymousClassVisitor;
2728
use PHPStan\Php\PhpVersion;
2829
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
2930
use PHPStan\PhpDoc\StubPhpDocProvider;
@@ -201,7 +202,6 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $
201202
$scopeFile,
202203
);
203204
$classNode->name = new Node\Identifier($className);
204-
$classNode->setAttribute('anonymousClass', true);
205205

206206
if (isset(self::$anonymousClasses[$className])) {
207207
return self::$anonymousClasses[$className];
@@ -214,6 +214,14 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $
214214
null,
215215
);
216216

217+
/** @var int|null $classLineIndex */
218+
$classLineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX);
219+
if ($classLineIndex === null) {
220+
$displayName = sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine());
221+
} else {
222+
$displayName = sprintf('class@anonymous/%s:%s:%d', $filename, $classNode->getStartLine(), $classLineIndex);
223+
}
224+
217225
self::$anonymousClasses[$className] = new ClassReflection(
218226
$this->reflectionProviderProvider->getReflectionProvider(),
219227
$this->initializerExprTypeResolver,
@@ -227,7 +235,7 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $
227235
$this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(),
228236
$this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(),
229237
$this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(),
230-
sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()),
238+
$displayName,
231239
new ReflectionClass($reflectionClass),
232240
$scopeFile,
233241
null,

src/Type/FileTypeMapper.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPStan\BetterReflection\Util\GetLastDocComment;
99
use PHPStan\Broker\AnonymousClassNameHelper;
1010
use PHPStan\File\FileHelper;
11+
use PHPStan\Parser\AnonymousClassVisitor;
1112
use PHPStan\Parser\Parser;
1213
use PHPStan\PhpDoc\PhpDocNodeResolver;
1314
use PHPStan\PhpDoc\PhpDocStringResolver;
@@ -260,7 +261,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
260261
}
261262

262263
$className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
263-
} elseif ((bool) $node->getAttribute('anonymousClass', false)) {
264+
} elseif ((bool) $node->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS, false)) {
264265
$className = $node->name->name;
265266
} else {
266267
if ($traitFound) {
@@ -451,7 +452,7 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
451452
}
452453

453454
$className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
454-
} elseif ((bool) $node->getAttribute('anonymousClass', false)) {
455+
} elseif ((bool) $node->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS, false)) {
455456
$className = $node->name->name;
456457
} else {
457458
if ($traitFound) {

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,27 @@ public function testBug11297(): void
14161416
$this->assertNoErrors($errors);
14171417
}
14181418

1419+
public function testBug5597(): void
1420+
{
1421+
if (PHP_VERSION_ID < 80000) {
1422+
$this->markTestSkipped('Test requires PHP 8.0.');
1423+
}
1424+
1425+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-5597.php');
1426+
$this->assertNoErrors($errors);
1427+
}
1428+
1429+
public function testBug11511(): void
1430+
{
1431+
if (PHP_VERSION_ID < 80000) {
1432+
$this->markTestSkipped('Test requires PHP 8.0.');
1433+
}
1434+
1435+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-11511.php');
1436+
$this->assertCount(1, $errors);
1437+
$this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage());
1438+
}
1439+
14191440
/**
14201441
* @param string[]|null $allAnalysedFiles
14211442
* @return Error[]

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8409,6 +8409,44 @@ public function testAnonymousClassNameInTrait(
84098409
);
84108410
}
84118411

8412+
public function dataAnonymousClassNameSameLine(): array
8413+
{
8414+
return [
8415+
[
8416+
'AnonymousClass0d7d08272ba2f0a6ef324bb65c679e02',
8417+
'$foo',
8418+
'$bar',
8419+
],
8420+
[
8421+
'AnonymousClass464f64cbdca25b4af842cae65615bca9',
8422+
'$bar',
8423+
'$baz',
8424+
],
8425+
[
8426+
'AnonymousClassa9fb472ec9acc5cae3bee4355c296bfa',
8427+
'$baz',
8428+
'die',
8429+
],
8430+
];
8431+
}
8432+
8433+
/**
8434+
* @dataProvider dataAnonymousClassNameSameLine
8435+
*/
8436+
public function testAnonymousClassNameSameLine(
8437+
string $description,
8438+
string $expression,
8439+
string $evaluatedPointExpression,
8440+
): void
8441+
{
8442+
$this->assertTypes(
8443+
__DIR__ . '/data/anonymous-class-name-same-line.php',
8444+
$description,
8445+
$expression,
8446+
$evaluatedPointExpression,
8447+
);
8448+
}
8449+
84128450
public function dataDynamicConstants(): array
84138451
{
84148452
return [
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace AnonymousClassNameSameLine;
4+
5+
$foo = new class {}; $bar = new class {}; $baz = new class {}; die;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1); // lint >= 8.0
2+
3+
namespace Bug11511;
4+
5+
$myObject = new class (new class { public string $bar = 'test'; }) {
6+
public function __construct(public object $foo)
7+
{
8+
}
9+
};
10+
echo $myObject->foo->bar;

0 commit comments

Comments
 (0)