Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ indent_size = 2
[composer.json]
indent_style = tab
indent_size = 4

[*.abnf]
indent_style = tab
indent_size = 4
18 changes: 15 additions & 3 deletions doc/grammars/type.abnf
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,23 @@ Array
= 1*(TokenSquareBracketOpen TokenSquareBracketClose)

ArrayShape
= TokenCurlyBracketOpen ArrayShapeItem *(TokenComma ArrayShapeItem) TokenCurlyBracketClose
= Shape

ArrayShapeItem
= (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type
/ Type
= ShapeItem

Shape
= TokenCurlyBracketOpen ShapeItem *(TokenComma ShapeItem) TokenCurlyBracketClose

ShapeItem
= (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type
/ Type
Comment on lines +71 to +73
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the comment, this only makes sense for array shapes. For object shapes you need sth like

ObjectShapeItem = TokenIdentifier [TokenNullable] TokenColon Type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the feedback

silly oversight there with allowing for object{string, string}, i'll rework this tomorrow if i can


ObjectShape
= Shape

ObjectShapeItem
= ShapeItem

; ---------------------------------------------------------------------------- ;
; ConstantExpr ;
Expand Down
43 changes: 1 addition & 42 deletions src/Ast/Type/ArrayShapeItemNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,7 @@

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ArrayShapeItemNode implements TypeNode
class ArrayShapeItemNode extends ShapeItemNode implements TypeNode
{

use NodeAttributes;

/** @var ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null */
public $keyName;

/** @var bool */
public $optional;

/** @var TypeNode */
public $valueType;

/**
* @param ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null $keyName
*/
public function __construct($keyName, bool $optional, TypeNode $valueType)
{
$this->keyName = $keyName;
$this->optional = $optional;
$this->valueType = $valueType;
}


public function __toString(): string
{
if ($this->keyName !== null) {
return sprintf(
'%s%s: %s',
(string) $this->keyName,
$this->optional ? '?' : '',
(string) $this->valueType
);
}

return (string) $this->valueType;
}

}
8 changes: 8 additions & 0 deletions src/Ast/Type/ObjectShapeItemNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

class ObjectShapeItemNode extends ShapeItemNode implements TypeNode
{

}
31 changes: 31 additions & 0 deletions src/Ast/Type/ObjectShapeNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function implode;

class ObjectShapeNode implements TypeNode
{

use NodeAttributes;

/** @var IdentifierTypeNode $identitier */
public $identifier;

/** @var ShapeItemNode[] */
public $items;

public function __construct(IdentifierTypeNode $identifier, array $items)
{
$this->identifier = $identifier;
$this->items = $items;
}


public function __toString(): string
{
return "{$this->identifier}{" . implode(', ', $this->items) . '}';
}

}
49 changes: 49 additions & 0 deletions src/Ast/Type/ShapeItemNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ShapeItemNode implements TypeNode
{

use NodeAttributes;

/** @var ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null */
public $keyName;

/** @var bool */
public $optional;

/** @var TypeNode */
public $valueType;

/**
* @param ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null $keyName
*/
public function __construct($keyName, bool $optional, TypeNode $valueType)
{
$this->keyName = $keyName;
$this->optional = $optional;
$this->valueType = $valueType;
}


public function __toString(): string
{
if ($this->keyName !== null) {
return sprintf(
'%s%s: %s',
(string) $this->keyName,
$this->optional ? '?' : '',
(string) $this->valueType
);
}

return (string) $this->valueType;
}

}
43 changes: 34 additions & 9 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
} elseif ($type->name === 'object' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
$type = $this->parseObjectShape($tokens, $type);
}

return $type;
Expand Down Expand Up @@ -468,53 +470,76 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ
return $type;
}


/** @phpstan-impure */
private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode
private function parseShape(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): array
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
return new Ast\Type\ArrayShapeNode([]);
return [];
}

$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$items = [$this->parseArrayShapeItem($tokens)];
$items = [$this->parseShapeItem($tokens, $type)];

$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
// trailing comma case
return new Ast\Type\ArrayShapeNode($items);
return $items;
}

$items[] = $this->parseArrayShapeItem($tokens);
$items[] = $this->parseShapeItem($tokens, $type);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
}

$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);

return $items;
}

/** @phpstan-impure */
private function parseArrayShape(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): Ast\Type\ArrayShapeNode
{
$items = $this->parseShape($tokens, $type);

return new Ast\Type\ArrayShapeNode($items);
}

/** @phpstan-impure */
private function parseObjectShape(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): Ast\Type\ObjectShapeNode
{
$items = $this->parseShape($tokens, $type);

return new Ast\Type\ObjectShapeNode($type, $items);
}


/** @phpstan-impure */
private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode
private function parseShapeItem(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): Ast\Type\ShapeItemNode
{
try {
$tokens->pushSavePoint();
$key = $this->parseArrayShapeKey($tokens);
$key = $this->parseShapeKey($tokens);
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$value = $this->parse($tokens);
$tokens->dropSavePoint();

if ($type->name === 'object') {
return new Ast\Type\ObjectShapeItemNode($key, $optional, $value);
}

return new Ast\Type\ArrayShapeItemNode($key, $optional, $value);
} catch (ParserException $e) {
$tokens->rollback();
$value = $this->parse($tokens);

if ($type->name === 'object') {
return new Ast\Type\ObjectShapeItemNode(null, false, $value);
}

return new Ast\Type\ArrayShapeItemNode(null, false, $value);
}
}
Expand All @@ -523,7 +548,7 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape
* @phpstan-impure
* @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
*/
private function parseArrayShapeKey(TokenIterator $tokens)
private function parseShapeKey(TokenIterator $tokens)
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
$key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue());
Expand Down
25 changes: 18 additions & 7 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
Expand Down Expand Up @@ -880,13 +882,22 @@ public function provideVarTagsData(): Iterator
new PhpDocNode([
new PhpDocTagNode(
'@psalm-type',
new InvalidTagValueNode(
'Unexpected token "{", expected \'*/\' at offset 44',
new ParserException(
'{',
Lexer::TOKEN_OPEN_CURLY_BRACKET,
44,
Lexer::TOKEN_CLOSE_PHPDOC
new TypeAliasTagValueNode(
'PARTSTRUCTURE_PARAM',
new ObjectShapeNode(
new IdentifierTypeNode('object'),
[
new ObjectShapeItemNode(
new IdentifierTypeNode('attribute'),
false,
new IdentifierTypeNode('string')
),
new ObjectShapeItemNode(
new IdentifierTypeNode('value'),
true,
new IdentifierTypeNode('string')
),
]
)
)
),
Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Parser/TypeParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
Expand Down Expand Up @@ -1266,6 +1268,27 @@ public function provideParseData(): array
)
),
],

[
'object{a: int, b?: ?int}',
new ObjectShapeNode(
new IdentifierTypeNode('object'),
[
new ObjectShapeItemNode(
new IdentifierTypeNode('a'),
false,
new IdentifierTypeNode('int')
),
new ObjectShapeItemNode(
new IdentifierTypeNode('b'),
true,
new NullableTypeNode(
new IdentifierTypeNode('int')
)
),
]
),
],
];
}

Expand Down