Skip to content

Commit c6c9626

Browse files
committed
Add support for wrapping class constant array shape keys in quotes for legacy phpdoc-parser compatibility in DocblockNormalizer
1 parent fa01170 commit c6c9626

4 files changed

Lines changed: 21 additions & 29 deletions

File tree

src/Internal/DocblockNormalizer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ final class DocblockNormalizer
2828
*/
2929
public static function normalize(string $doc): string
3030
{
31+
$doc = preg_replace('/(\\\\?[a-zA-Z_\x80-\xff][\\\\a-zA-Z0-9_\x80-\xff]*::[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*(\??:)/', '"$1"$2', $doc) ?? $doc;
32+
3133
if (! str_contains($doc, '{')) {
3234
return $doc;
3335
}

src/Internal/Visitor/PropertyHookInjector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,4 @@ public function enterNode(Node $node): int|null
9696

9797
return $newStmts;
9898
}
99-
}
99+
}

src/Resolver/SpecialTypeResolver.php

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct
9191

9292
if ($node instanceof GenericTypeNode) {
9393
$genericType = self::resolve($node->type, $ref, $thisObj);
94-
$innerTypes = array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes);
94+
$innerTypes = array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->genericTypes);
9595

9696
return new GenericTypeNode(
9797
$genericType instanceof IdentifierTypeNode ? $genericType : $node->type,
@@ -145,11 +145,11 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct
145145
}
146146

147147
if ($node instanceof UnionTypeNode) {
148-
return new UnionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types));
148+
return new UnionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types));
149149
}
150150

151151
if ($node instanceof IntersectionTypeNode) {
152-
return new IntersectionTypeNode(array_map(fn($t) => self::resolve($t, $ref, $thisObj), $node->types));
152+
return new IntersectionTypeNode(array_map(fn ($t) => self::resolve($t, $ref, $thisObj), $node->types));
153153
}
154154

155155
return $node;
@@ -181,7 +181,7 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
181181

182182
if ($node instanceof GenericTypeNode) {
183183
$genericType = self::resolveForFile($node->type, $file);
184-
$innerTypes = array_map(fn($t) => self::resolveForFile($t, $file), $node->genericTypes);
184+
$innerTypes = array_map(fn ($t) => self::resolveForFile($t, $file), $node->genericTypes);
185185

186186
return new GenericTypeNode(
187187
$genericType instanceof IdentifierTypeNode ? $genericType : $node->type,
@@ -235,11 +235,11 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
235235
}
236236

237237
if ($node instanceof UnionTypeNode) {
238-
return new UnionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types));
238+
return new UnionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types));
239239
}
240240

241241
if ($node instanceof IntersectionTypeNode) {
242-
return new IntersectionTypeNode(array_map(fn($t) => self::resolveForFile($t, $file), $node->types));
242+
return new IntersectionTypeNode(array_map(fn ($t) => self::resolveForFile($t, $file), $node->types));
243243
}
244244

245245
return clone $node;
@@ -359,26 +359,13 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass
359359
$className = null;
360360
$constName = null;
361361

362-
$debugClass = $keyName !== null ? get_class($keyName) : 'null';
363-
$debugVal = $keyName !== null && method_exists($keyName, '__toString') ? (string) $keyName : 'unknown';
364-
fwrite(STDERR, "\n[DEBUG] ArrayShapeItem KeyType: {$debugClass} | Value: {$debugVal}\n");
365-
366-
if ($keyName instanceof IdentifierTypeNode) {
367-
fwrite(STDERR, "[DEBUG] IdentifierName: {$keyName->name}\n");
368-
}
369-
370362
if ($keyName instanceof ConstFetchNode && $keyName->className !== '') {
371363
$className = $keyName->className;
372364
$constName = $keyName->name;
373365
} elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) {
374-
// Fallback for phpstan/phpdoc-parser v1.x/v2.0 which parses constants as Identifiers
375366
[$className, $constName] = explode('::', $keyName->name, 2);
376-
}
377-
378-
if ($className !== null && $constName !== null) {
379-
fwrite(STDERR, "[DEBUG] Split into Class: {$className} | Const: {$constName}\n");
380-
} else {
381-
fwrite(STDERR, "[DEBUG] Did NOT split into Class and Const.\n");
367+
} elseif ($keyName instanceof ConstExprStringNode && str_contains($keyName->value, '::')) {
368+
[$className, $constName] = explode('::', $keyName->value, 2);
382369
}
383370

384371
if ($className !== null && $constName !== null) {
@@ -394,14 +381,9 @@ private static function resolveArrayShape(ArrayShapeNode $node, \ReflectionClass
394381
$resolvedClass = self::resolveFqcn($className, $ref);
395382
}
396383

397-
fwrite(STDERR, "[DEBUG] Resolved Target Class for Reflection: {$resolvedClass}\n");
398-
399384
$resolvedKeyNode = self::resolveConstantKeyValue($resolvedClass, $constName);
400385
if ($resolvedKeyNode !== null) {
401-
fwrite(STDERR, "[DEBUG] Successfully Reflected Constant! New Value: {$resolvedKeyNode->value}\n");
402386
$keyName = $resolvedKeyNode;
403-
} else {
404-
fwrite(STDERR, "[DEBUG] FAILED to Reflect Constant!\n");
405387
}
406388
}
407389

@@ -462,8 +444,6 @@ private static function resolveCallable(CallableTypeNode $node, \ReflectionClass
462444
return new CallableTypeNode($node->identifier, $resolvedParameters, $resolvedReturnType, $node->templateTypes);
463445
}
464446

465-
// --- Private Helper Extractions for File Context ---
466-
467447
private static function resolveConstTypeForFile(ConstTypeNode $node, string $file): ConstTypeNode
468448
{
469449
if ($node->constExpr instanceof ConstFetchNode && $node->constExpr->className !== '') {
@@ -524,6 +504,8 @@ private static function resolveArrayShapeForFile(ArrayShapeNode $node, string $f
524504
$constName = $keyName->name;
525505
} elseif ($keyName instanceof IdentifierTypeNode && str_contains($keyName->name, '::')) {
526506
[$className, $constName] = explode('::', $keyName->name, 2);
507+
} elseif ($keyName instanceof ConstExprStringNode && str_contains($keyName->value, '::')) {
508+
[$className, $constName] = explode('::', $keyName->value, 2);
527509
}
528510

529511
if ($className !== null && $constName !== null) {

tests/Internal/DocblockNormalizerTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,12 @@
8686

8787
expect(DocblockNormalizer::normalize($doc))->toBe($expected);
8888
});
89+
90+
test('wraps class constant array shape keys in quotes for legacy phpdoc-parser compatibility', function () {
91+
$doc = '/** @param array{self::KEY_ID: int, App\Constants::ROLE: string} $payload */';
92+
93+
$expected = '/** @param array{"self::KEY_ID": int, "App\Constants::ROLE": string} $payload */';
94+
95+
expect(DocblockNormalizer::normalize($doc))->toBe($expected);
96+
});
8997
});

0 commit comments

Comments
 (0)