Skip to content

Commit 882eabc

Browse files
committedApr 6, 2023
Support for object shapes
1 parent d3753fc commit 882eabc

File tree

5 files changed

+467
-6
lines changed

5 files changed

+467
-6
lines changed
 

‎src/Ast/Type/ObjectShapeItemNode.php

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
6+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
7+
use function sprintf;
8+
9+
class ObjectShapeItemNode implements TypeNode
10+
{
11+
12+
use NodeAttributes;
13+
14+
/** @var ConstExprStringNode|IdentifierTypeNode */
15+
public $keyName;
16+
17+
/** @var bool */
18+
public $optional;
19+
20+
/** @var TypeNode */
21+
public $valueType;
22+
23+
/**
24+
* @param ConstExprStringNode|IdentifierTypeNode $keyName
25+
*/
26+
public function __construct($keyName, bool $optional, TypeNode $valueType)
27+
{
28+
$this->keyName = $keyName;
29+
$this->optional = $optional;
30+
$this->valueType = $valueType;
31+
}
32+
33+
34+
public function __toString(): string
35+
{
36+
if ($this->keyName !== null) {
37+
return sprintf(
38+
'%s%s: %s',
39+
(string) $this->keyName,
40+
$this->optional ? '?' : '',
41+
(string) $this->valueType
42+
);
43+
}
44+
45+
return (string) $this->valueType;
46+
}
47+
48+
}

‎src/Ast/Type/ObjectShapeNode.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
use function implode;
7+
8+
class ObjectShapeNode implements TypeNode
9+
{
10+
11+
use NodeAttributes;
12+
13+
/** @var ObjectShapeItemNode[] */
14+
public $items;
15+
16+
public function __construct(array $items)
17+
{
18+
$this->items = $items;
19+
}
20+
21+
public function __toString(): string
22+
{
23+
$items = $this->items;
24+
25+
return 'object{' . implode(', ', $items) . '}';
26+
}
27+
28+
}

‎src/Parser/TypeParser.php

+66-2
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,12 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
124124
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
125125
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
126126

127-
} elseif (in_array($type->name, ['array', 'list'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
128-
$type = $this->parseArrayShape($tokens, $type, $type->name);
127+
} elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
128+
if ($type->name === 'object') {
129+
$type = $this->parseObjectShape($tokens);
130+
} else {
131+
$type = $this->parseArrayShape($tokens, $type, $type->name);
132+
}
129133

130134
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
131135
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
@@ -582,4 +586,64 @@ private function parseArrayShapeKey(TokenIterator $tokens)
582586
return $key;
583587
}
584588

589+
/**
590+
* @phpstan-impure
591+
*/
592+
private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode
593+
{
594+
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
595+
596+
$items = [];
597+
598+
do {
599+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
600+
601+
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
602+
return new Ast\Type\ObjectShapeNode($items);
603+
}
604+
605+
$items[] = $this->parseObjectShapeItem($tokens);
606+
607+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
608+
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
609+
610+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
611+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
612+
613+
return new Ast\Type\ObjectShapeNode($items);
614+
}
615+
616+
/** @phpstan-impure */
617+
private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode
618+
{
619+
$key = $this->parseObjectShapeKey($tokens);
620+
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
621+
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
622+
$value = $this->parse($tokens);
623+
624+
return new Ast\Type\ObjectShapeItemNode($key, $optional, $value);
625+
}
626+
627+
/**
628+
* @phpstan-impure
629+
* @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
630+
*/
631+
private function parseObjectShapeKey(TokenIterator $tokens)
632+
{
633+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
634+
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
635+
$tokens->next();
636+
637+
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
638+
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
639+
$tokens->next();
640+
641+
} else {
642+
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
643+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
644+
}
645+
646+
return $key;
647+
}
648+
585649
}

‎tests/PHPStan/Parser/PhpDocParserTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -901,16 +901,16 @@ public function provideVarTagsData(): Iterator
901901

902902
yield [
903903
'invalid object shape',
904-
'/** @psalm-type PARTSTRUCTURE_PARAM = object{attribute:string, value?:string} */',
904+
'/** @psalm-type PARTSTRUCTURE_PARAM = objecttt{attribute:string, value?:string} */',
905905
new PhpDocNode([
906906
new PhpDocTagNode(
907907
'@psalm-type',
908908
new InvalidTagValueNode(
909-
'Unexpected token "{", expected \'*/\' at offset 44',
909+
'Unexpected token "{", expected \'*/\' at offset 46',
910910
new ParserException(
911911
'{',
912912
Lexer::TOKEN_OPEN_CURLY_BRACKET,
913-
44,
913+
46,
914914
Lexer::TOKEN_CLOSE_PHPDOC
915915
)
916916
)
@@ -926,7 +926,7 @@ public function provideVarTagsData(): Iterator
926926
new ParserException(
927927
'{',
928928
Lexer::TOKEN_OPEN_CURLY_BRACKET,
929-
44,
929+
46,
930930
Lexer::TOKEN_PHPDOC_EOL,
931931
null
932932
)

0 commit comments

Comments
 (0)