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

+5
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

+2-1
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

+10-1
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

+51
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

+10-2
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

+3-2
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

+21
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

+38
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 [
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;
+10
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;
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1); // lint >= 8.0
2+
3+
namespace Bug5597;
4+
5+
interface InterfaceA {}
6+
7+
class ClassA implements InterfaceA {}
8+
9+
class ClassB
10+
{
11+
public function __construct(
12+
private InterfaceA $parameterA,
13+
) {
14+
}
15+
16+
public function test() : InterfaceA
17+
{
18+
return $this->parameterA;
19+
}
20+
}
21+
22+
$classA = new class() extends ClassA {};
23+
$thisWorks = new class($classA) extends ClassB {};
24+
25+
$thisFailsWithTwoErrors = new class(new class() extends ClassA {}) extends ClassB {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Stmt\Class_;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Parser\AnonymousClassVisitor;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Testing\RuleTestCase;
12+
use function implode;
13+
use function sprintf;
14+
15+
/**
16+
* @extends RuleTestCase<Rule<Class_>>
17+
*/
18+
class AnonymousClassReflectionTest extends RuleTestCase
19+
{
20+
21+
/**
22+
* @return Rule<Class_>
23+
*/
24+
protected function getRule(): Rule
25+
{
26+
return new /** @implements Rule<Class_> */ class (self::createReflectionProvider()) implements Rule {
27+
28+
public function __construct(private ReflectionProvider $reflectionProvider)
29+
{
30+
}
31+
32+
public function getNodeType(): string
33+
{
34+
return Class_::class;
35+
}
36+
37+
public function processNode(Node $node, Scope $scope): array
38+
{
39+
if (!(bool) $node->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS)) {
40+
return [];
41+
}
42+
43+
$classReflection = $this->reflectionProvider->getAnonymousClassReflection($node, $scope);
44+
45+
return [
46+
RuleErrorBuilder::message(sprintf(
47+
"name: %s\ndisplay name: %s",
48+
$classReflection->getName(),
49+
$classReflection->getDisplayName(),
50+
))->identifier('test.anonymousClassReflection')->build(),
51+
];
52+
}
53+
54+
};
55+
}
56+
57+
public function testReflection(): void
58+
{
59+
$this->analyse([__DIR__ . '/data/anonymous-classes.php'], [
60+
[
61+
implode("\n", [
62+
'name: AnonymousClass0c307d7b8501323d1d30b0afea7e0578',
63+
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:5',
64+
]),
65+
5,
66+
],
67+
[
68+
implode("\n", [
69+
'name: AnonymousClassa16017c480192f8fbf3c03e17840e99c',
70+
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:1',
71+
]),
72+
7,
73+
],
74+
[
75+
implode("\n", [
76+
'name: AnonymousClassd68d75f1cdac379350e3027c09a7c5a0',
77+
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:2',
78+
]),
79+
7,
80+
],
81+
[
82+
implode("\n", [
83+
'name: AnonymousClass75aa798fed4f30306c14dcf03a50878c',
84+
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:3',
85+
]),
86+
7,
87+
],
88+
[
89+
implode("\n", [
90+
'name: AnonymousClass4fcabdc52bfed5f8c101f3f89b2180bd',
91+
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:1',
92+
]),
93+
9,
94+
],
95+
[
96+
implode("\n", [
97+
'name: AnonymousClass0e77d7995f4c47dcd5402817970fd7e0',
98+
'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:2',
99+
]),
100+
9,
101+
],
102+
]);
103+
}
104+
105+
}

0 commit comments

Comments
 (0)