Skip to content

Commit

Permalink
Support for object shapes
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Apr 6, 2023
1 parent d3753fc commit 882eabc
Show file tree
Hide file tree
Showing 5 changed files with 467 additions and 6 deletions.
48 changes: 48 additions & 0 deletions src/Ast/Type/ObjectShapeItemNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

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

class ObjectShapeItemNode implements TypeNode
{

use NodeAttributes;

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

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

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

/**
* @param ConstExprStringNode|IdentifierTypeNode $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;
}

}
28 changes: 28 additions & 0 deletions src/Ast/Type/ObjectShapeNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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 ObjectShapeItemNode[] */
public $items;

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

public function __toString(): string
{
$items = $this->items;

return 'object{' . implode(', ', $items) . '}';
}

}
68 changes: 66 additions & 2 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);

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

if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
Expand Down Expand Up @@ -582,4 +586,64 @@ private function parseArrayShapeKey(TokenIterator $tokens)
return $key;
}

/**
* @phpstan-impure
*/
private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);

$items = [];

do {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);

if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
return new Ast\Type\ObjectShapeNode($items);
}

$items[] = $this->parseObjectShapeItem($tokens);

$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));

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

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

/** @phpstan-impure */
private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode
{
$key = $this->parseObjectShapeKey($tokens);
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$value = $this->parse($tokens);

return new Ast\Type\ObjectShapeItemNode($key, $optional, $value);
}

/**
* @phpstan-impure
* @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
*/
private function parseObjectShapeKey(TokenIterator $tokens)
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
$tokens->next();

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
$tokens->next();

} else {
$key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
}

return $key;
}

}
8 changes: 4 additions & 4 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -901,16 +901,16 @@ public function provideVarTagsData(): Iterator

yield [
'invalid object shape',
'/** @psalm-type PARTSTRUCTURE_PARAM = object{attribute:string, value?:string} */',
'/** @psalm-type PARTSTRUCTURE_PARAM = objecttt{attribute:string, value?:string} */',
new PhpDocNode([
new PhpDocTagNode(
'@psalm-type',
new InvalidTagValueNode(
'Unexpected token "{", expected \'*/\' at offset 44',
'Unexpected token "{", expected \'*/\' at offset 46',
new ParserException(
'{',
Lexer::TOKEN_OPEN_CURLY_BRACKET,
44,
46,
Lexer::TOKEN_CLOSE_PHPDOC
)
)
Expand All @@ -926,7 +926,7 @@ public function provideVarTagsData(): Iterator
new ParserException(
'{',
Lexer::TOKEN_OPEN_CURLY_BRACKET,
44,
46,
Lexer::TOKEN_PHPDOC_EOL,
null
)
Expand Down

0 comments on commit 882eabc

Please sign in to comment.