Skip to content

Commit b352cf0

Browse files
authored
Fix namespaces for PHP 8.4 (#112)
* Parse namespaces from tokens, don't relying on getNamespaceName() ReflectionFunction::getNamespaceName() no longer returns a meaningful namespace for closures starting with PHP 8.4. * Add tests for namespace parsing Especially for resolving relative namespaces in PHP 8.4. * Fix wrongly considering 'use' statements from other namespaces The only case not handled is to ignore 'use' statements that appear after usage within the same namespace. * .styleci.yml: Exclude tests The unused 'use' statements are essential for the tests.
1 parent 1327ae3 commit b352cf0

File tree

5 files changed

+177
-23
lines changed

5 files changed

+177
-23
lines changed

.styleci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ php:
44
finder:
55
not-name:
66
- Php84Test.php
7+
- ReflectionClosureBracedNamespaceTest.php
8+
- ReflectionClosureNonBracedNamespaceTest.php
79
- ReflectionClosurePhp80Test.php
810
- ReflectionClosurePhp81Test.php
911
- SerializerPhp81Test.php

phpunit.xml.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<file>tests/ReflectionClosure3Test.php</file>
1717
<file>tests/ReflectionClosure4Test.php</file>
1818
<file>tests/ReflectionClosure5Test.php</file>
19+
<file>tests/ReflectionClosureBracedNamespaceTest.php</file>
20+
<file>tests/ReflectionClosureNonBracedNamespaceTest.php</file>
1921
<file>tests/SerializerTest.php</file>
2022
<file>tests/SignedSerializerTest.php</file>
2123
</testsuite>

src/Support/ReflectionClosure.php

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -865,13 +865,18 @@ protected function getTokens()
865865
*/
866866
protected function getClasses()
867867
{
868-
$key = $this->getHashedFileName();
869-
870-
if (! isset(static::$classes[$key])) {
871-
$this->fetchItems();
868+
$line = $this->getStartLine();
869+
870+
foreach ($this->getStructures() as $struct) {
871+
if ($struct['type'] === 'namespace' &&
872+
$struct['start'] <= $line &&
873+
$struct['end'] >= $line
874+
) {
875+
return $struct['classes'];
876+
}
872877
}
873878

874-
return static::$classes[$key];
879+
return [];
875880
}
876881

877882
/**
@@ -945,14 +950,36 @@ protected function fetchItems()
945950
$alias = '';
946951
$isFunc = $isConst = false;
947952

948-
$startLine = $endLine = 0;
953+
$startLine = $lastKnownLine = 0;
949954
$structType = $structName = '';
950955
$structIgnore = false;
951956

957+
$namespace = '';
958+
$namespaceStartLine = 0;
959+
$namespaceBraced = false;
960+
$namespaceClasses = [];
961+
952962
foreach ($tokens as $token) {
963+
if (is_array($token)) {
964+
$lastKnownLine = $token[2];
965+
}
966+
953967
switch ($state) {
954968
case 'start':
955969
switch ($token[0]) {
970+
case T_NAMESPACE:
971+
$structures[] = [
972+
'type' => 'namespace',
973+
'name' => $namespace,
974+
'start' => $namespaceStartLine,
975+
'end' => $token[2] - 1,
976+
'classes' => $namespaceClasses,
977+
];
978+
$namespace = '';
979+
$namespaceClasses = [];
980+
$state = 'namespace';
981+
$namespaceStartLine = $token[2];
982+
break;
956983
case T_CLASS:
957984
case T_INTERFACE:
958985
case T_TRAIT:
@@ -978,6 +1005,33 @@ protected function fetchItems()
9781005
case T_DOUBLE_COLON:
9791006
$state = 'invoke';
9801007
break;
1008+
case '}':
1009+
if ($namespaceBraced) {
1010+
$structures[] = [
1011+
'type' => 'namespace',
1012+
'name' => $namespace,
1013+
'start' => $namespaceStartLine,
1014+
'end' => $lastKnownLine,
1015+
'classes' => $namespaceClasses,
1016+
];
1017+
$namespaceBraced = false;
1018+
$namespace = '';
1019+
$namespaceClasses = [];
1020+
}
1021+
break;
1022+
}
1023+
break;
1024+
case 'namespace':
1025+
switch ($token[0]) {
1026+
case T_STRING:
1027+
case T_NAME_QUALIFIED:
1028+
$namespace = $token[1];
1029+
break;
1030+
case ';':
1031+
case '{':
1032+
$state = 'start';
1033+
$namespaceBraced = $token[0] === '{';
1034+
break;
9811035
}
9821036
break;
9831037
case 'use':
@@ -1022,6 +1076,7 @@ protected function fetchItems()
10221076
$constants[$alias] = $name;
10231077
} else {
10241078
$classes[strtolower($alias)] = $name;
1079+
$namespaceClasses[strtolower($alias)] = $name;
10251080
}
10261081
}
10271082
$name = $alias = '';
@@ -1061,6 +1116,7 @@ protected function fetchItems()
10611116
$constants[$alias] = $prefix.$name;
10621117
} else {
10631118
$classes[strtolower($alias)] = $prefix.$name;
1119+
$namespaceClasses[strtolower($alias)] = $prefix.$name;
10641120
}
10651121
}
10661122
$name = $alias = '';
@@ -1118,22 +1174,26 @@ protected function fetchItems()
11181174
'type' => $structType,
11191175
'name' => $structName,
11201176
'start' => $startLine,
1121-
'end' => $endLine,
1177+
'end' => $lastKnownLine,
11221178
];
11231179
}
11241180
$structIgnore = false;
11251181
$state = 'start';
11261182
}
11271183
break;
1128-
default:
1129-
if (is_array($token)) {
1130-
$endLine = $token[2];
1131-
}
11321184
}
11331185
break;
11341186
}
11351187
}
11361188

1189+
$structures[] = [
1190+
'type' => 'namespace',
1191+
'name' => $namespace,
1192+
'start' => $namespaceStartLine,
1193+
'end' => PHP_INT_MAX,
1194+
'classes' => $namespaceClasses,
1195+
];
1196+
11371197
static::$classes[$key] = $classes;
11381198
static::$functions[$key] = $functions;
11391199
static::$constants[$key] = $constants;
@@ -1147,20 +1207,19 @@ protected function fetchItems()
11471207
*/
11481208
protected function getClosureNamespaceName()
11491209
{
1150-
$ns = $this->getNamespaceName();
1151-
1152-
$name = $this->getName();
1153-
1154-
// First class callables...
1155-
if ($name !== '{closure}'
1156-
&& ! str_contains($name, '{closure:/')
1157-
&& ! str_contains($name, '{closure:\\')
1158-
&& empty($ns)
1159-
&& ! is_null($this->getClosureScopeClass())) {
1160-
$ns = $this->getClosureScopeClass()->getNamespaceName();
1210+
$startLine = $this->getStartLine();
1211+
$endLine = $this->getEndLine();
1212+
1213+
foreach ($this->getStructures() as $struct) {
1214+
if ($struct['type'] === 'namespace' &&
1215+
$struct['start'] <= $startLine &&
1216+
$struct['end'] >= $endLine
1217+
) {
1218+
return $struct['name'];
1219+
}
11611220
}
11621221

1163-
return $ns;
1222+
return '';
11641223
}
11651224

11661225
/**
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Space {
4+
test('relative namespace (braced)', function () {
5+
$f1 = fn (Foo $foo): Foo => new Foo();
6+
$e1 = 'fn (\Space\Foo $foo): \Space\Foo => new \Space\Foo()';
7+
8+
$f2 = fn (Foo\Bar $fooBar): Foo\Bar => new Foo\Bar();
9+
$e2 = 'fn (\Space\Foo\Bar $fooBar): \Space\Foo\Bar => new \Space\Foo\Bar()';
10+
11+
expect($f1)->toBeCode($e1);
12+
expect($f2)->toBeCode($e2);
13+
});
14+
}
15+
16+
namespace Irrelevant {}
17+
18+
namespace Sub\Space {
19+
test('relative other namespace (braced)', function () {
20+
$f1 = fn (Foo $foo): Foo => new Foo();
21+
$e1 = 'fn (\Sub\Space\Foo $foo): \Sub\Space\Foo => new \Sub\Space\Foo()';
22+
23+
expect($f1)->toBeCode($e1);
24+
});
25+
}
26+
27+
namespace {
28+
test('back to global namespace (braced)', function () {
29+
$f1 = fn (Foo $foo): Foo => new Foo();
30+
$e1 = 'fn (\Foo $foo): \Foo => new \Foo()';
31+
expect($f1)->toBeCode($e1);
32+
});
33+
}
34+
35+
namespace Irrelevant {
36+
// Shouldn't be used below, as not in the same namespace
37+
use Wrong as Qux;
38+
}
39+
40+
namespace Space {
41+
test('no using use from other namespace', function () {
42+
$f1 = fn (Qux $qux) => true;
43+
$e1 = 'fn (\Space\Qux $qux) => true';
44+
45+
expect($f1)->toBeCode($e1);
46+
});
47+
48+
// Shouldn't be used above, as declared after usage. Not currently supported though.
49+
// use AlsoWrong as Qux;
50+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Space;
4+
5+
test('relative namespace (non-braced)', function () {
6+
$f1 = fn (Foo $foo): Foo => new Foo();
7+
$e1 = 'fn (\Space\Foo $foo): \Space\Foo => new \Space\Foo()';
8+
9+
$f2 = fn (Foo\Bar $fooBar): Foo\Bar => new Foo\Bar();
10+
$e2 = 'fn (\Space\Foo\Bar $fooBar): \Space\Foo\Bar => new \Space\Foo\Bar()';
11+
12+
expect($f1)->toBeCode($e1);
13+
expect($f2)->toBeCode($e2);
14+
});
15+
16+
namespace Irrelevant;
17+
namespace Sub\Space;
18+
19+
test('relative other namespace (non-braced)', function () {
20+
$f1 = fn (Foo $foo): Foo => new Foo();
21+
$e1 = 'fn (\Sub\Space\Foo $foo): \Sub\Space\Foo => new \Sub\Space\Foo()';
22+
23+
expect($f1)->toBeCode($e1);
24+
});
25+
26+
namespace Irrelevant;
27+
28+
// Shouldn't be used below, as not in the same namespace
29+
use Wrong as Qux;
30+
31+
namespace Space;
32+
33+
test('not using use from other namespace', function () {
34+
$f1 = fn (Qux $qux) => true;
35+
$e1 = 'fn (\Space\Qux $qux) => true';
36+
37+
expect($f1)->toBeCode($e1);
38+
});
39+
40+
// Shouldn't be used above, as declared after usage. Not currently supported though.
41+
// use AlsoWrong as Qux;

0 commit comments

Comments
 (0)