Skip to content

Commit

Permalink
Fix parsing Doctrine strings
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 3, 2023
1 parent 4a1ab8e commit 5745775
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 38 deletions.
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ parameters:
- tests
excludePaths:
- tests/PHPStan/*/data/*
- tests/PHPStan/Parser/Doctrine/ApiResource.php
level: 8
ignoreErrors:
- '#^Dynamic call to static method PHPUnit\\Framework\\Assert#'
42 changes: 42 additions & 0 deletions src/Ast/ConstExpr/DoctrineConstExprStringNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\ConstExpr;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;
use function str_replace;
use function strlen;
use function substr;

class DoctrineConstExprStringNode extends ConstExprStringNode
{

use NodeAttributes;

/** @var string */
public $value;

public function __construct(string $value)
{
parent::__construct($value);
$this->value = $value;
}

public function __toString(): string
{
return self::escape($this->value);
}

public static function unescape(string $value): string
{
// from https://github.com/doctrine/annotations/blob/a9ec7af212302a75d1f92fa65d3abfbd16245a2a/lib/Doctrine/Common/Annotations/DocLexer.php#L103-L107
return str_replace('""', '"', substr($value, 1, strlen($value) - 2));
}

private static function escape(string $value): string
{
// from https://github.com/phpstan/phpdoc-parser/issues/205#issuecomment-1662323656
return sprintf('"%s"', str_replace('"', '""', $value));
}

}
29 changes: 16 additions & 13 deletions src/Lexer/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,20 @@ class Lexer
public const TOKEN_INTEGER = 20;
public const TOKEN_SINGLE_QUOTED_STRING = 21;
public const TOKEN_DOUBLE_QUOTED_STRING = 22;
public const TOKEN_IDENTIFIER = 23;
public const TOKEN_THIS_VARIABLE = 24;
public const TOKEN_VARIABLE = 25;
public const TOKEN_HORIZONTAL_WS = 26;
public const TOKEN_PHPDOC_EOL = 27;
public const TOKEN_OTHER = 28;
public const TOKEN_END = 29;
public const TOKEN_COLON = 30;
public const TOKEN_WILDCARD = 31;
public const TOKEN_OPEN_CURLY_BRACKET = 32;
public const TOKEN_CLOSE_CURLY_BRACKET = 33;
public const TOKEN_NEGATED = 34;
public const TOKEN_ARROW = 35;
public const TOKEN_DOCTRINE_ANNOTATION_STRING = 23;
public const TOKEN_IDENTIFIER = 24;
public const TOKEN_THIS_VARIABLE = 25;
public const TOKEN_VARIABLE = 26;
public const TOKEN_HORIZONTAL_WS = 27;
public const TOKEN_PHPDOC_EOL = 28;
public const TOKEN_OTHER = 29;
public const TOKEN_END = 30;
public const TOKEN_COLON = 31;
public const TOKEN_WILDCARD = 32;
public const TOKEN_OPEN_CURLY_BRACKET = 33;
public const TOKEN_CLOSE_CURLY_BRACKET = 34;
public const TOKEN_NEGATED = 35;
public const TOKEN_ARROW = 36;

public const TOKEN_LABELS = [
self::TOKEN_REFERENCE => '\'&\'',
Expand Down Expand Up @@ -79,6 +80,7 @@ class Lexer
self::TOKEN_INTEGER => 'TOKEN_INTEGER',
self::TOKEN_SINGLE_QUOTED_STRING => 'TOKEN_SINGLE_QUOTED_STRING',
self::TOKEN_DOUBLE_QUOTED_STRING => 'TOKEN_DOUBLE_QUOTED_STRING',
self::TOKEN_DOCTRINE_ANNOTATION_STRING => 'TOKEN_DOCTRINE_ANNOTATION_STRING',
self::TOKEN_IDENTIFIER => 'type',
self::TOKEN_THIS_VARIABLE => '\'$this\'',
self::TOKEN_VARIABLE => 'variable',
Expand Down Expand Up @@ -180,6 +182,7 @@ private function generateRegexp(): string

if ($this->parseDoctrineAnnotations) {
$patterns[self::TOKEN_DOCTRINE_TAG] = '@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*';
$patterns[self::TOKEN_DOCTRINE_ANNOTATION_STRING] = '"(?:""|[^"])*+"';
}

// anything but TOKEN_CLOSE_PHPDOC or TOKEN_HORIZONTAL_WS or TOKEN_EOL
Expand Down
68 changes: 68 additions & 0 deletions src/Parser/ConstExprParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class ConstExprParser
/** @var bool */
private $useIndexAttributes;

/** @var bool */
private $parseDoctrineStrings;

/**
* @param array{lines?: bool, indexes?: bool} $usedAttributes
*/
Expand All @@ -36,6 +39,24 @@ public function __construct(
$this->quoteAwareConstExprString = $quoteAwareConstExprString;
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
$this->parseDoctrineStrings = false;
}

/**
* @internal
*/
public function toDoctrine(): self
{
$self = new self(
$this->unescapeStrings,
$this->quoteAwareConstExprString,
[
'lines' => $this->useLinesAttributes,
'indexes' => $this->useIndexAttributes,
]
);
$self->parseDoctrineStrings = true;
return $self;
}

public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode
Expand Down Expand Up @@ -66,7 +87,41 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con
);
}

if ($this->parseDoctrineStrings && $tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$value = $tokens->currentTokenValue();
$tokens->next();

return $this->enrichWithAttributes(
$tokens,
new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($value)),
$startLine,
$startIndex
);
}

if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
if ($this->parseDoctrineStrings) {
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
throw new ParserException(
$tokens->currentTokenValue(),
$tokens->currentTokenType(),
$tokens->currentTokenOffset(),
Lexer::TOKEN_DOUBLE_QUOTED_STRING,
null,
$tokens->currentTokenLine()
);
}

$value = $tokens->currentTokenValue();
$tokens->next();

return $this->enrichWithAttributes(
$tokens,
$this->parseDoctrineString($value, $tokens),
$startLine,
$startIndex
);
}
$value = $tokens->currentTokenValue();
$type = $tokens->currentTokenType();
if ($trimStrings) {
Expand Down Expand Up @@ -214,6 +269,19 @@ private function parseArray(TokenIterator $tokens, int $endToken, int $startInde
}


public function parseDoctrineString(string $value, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode
{
$text = $value;

while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$text .= $tokens->currentTokenValue();
$tokens->next();
}

return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text));
}


private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
{
$startLine = $tokens->currentTokenLine();
Expand Down
17 changes: 11 additions & 6 deletions src/Parser/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class PhpDocParser
/** @var ConstExprParser */
private $constantExprParser;

/** @var ConstExprParser */
private $doctrineConstantExprParser;

/** @var bool */
private $requireWhitespaceBeforeDescription;

Expand Down Expand Up @@ -68,6 +71,7 @@ public function __construct(
{
$this->typeParser = $typeParser;
$this->constantExprParser = $constantExprParser;
$this->doctrineConstantExprParser = $constantExprParser->toDoctrine();
$this->requireWhitespaceBeforeDescription = $requireWhitespaceBeforeDescription;
$this->preserveTypeAliasesWithInvalidTypes = $preserveTypeAliasesWithInvalidTypes;
$this->parseDoctrineAnnotations = $parseDoctrineAnnotations;
Expand Down Expand Up @@ -688,7 +692,7 @@ private function parseDoctrineArgumentValue(TokenIterator $tokens)
);

try {
$constExpr = $this->constantExprParser->parse($tokens, true);
$constExpr = $this->doctrineConstantExprParser->parse($tokens, true);
if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
throw $exception;
}
Expand Down Expand Up @@ -750,14 +754,15 @@ private function parseDoctrineArrayKey(TokenIterator $tokens)
$key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
$tokens->next();

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
$key = new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($tokens->currentTokenValue()));

$tokens->next();

} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);

$value = $tokens->currentTokenValue();
$tokens->next();
$key = $this->doctrineConstantExprParser->parseDoctrineString($value, $tokens);

} else {
$currentTokenValue = $tokens->currentTokenValue();
Expand Down Expand Up @@ -786,7 +791,7 @@ private function parseDoctrineArrayKey(TokenIterator $tokens)
}

$tokens->rollback();
$constExpr = $this->constantExprParser->parse($tokens, true);
$constExpr = $this->doctrineConstantExprParser->parse($tokens, true);
if (!$constExpr instanceof Ast\ConstExpr\ConstFetchNode) {
throw new ParserException(
$tokens->currentTokenValue(),
Expand Down
34 changes: 34 additions & 0 deletions tests/PHPStan/Parser/Doctrine/ApiResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser\Doctrine;

/**
* ApiResource annotation.
*
* @author Kévin Dunglas <[email protected]>
*
* @Annotation
* @Target({"CLASS"})
*/
final class ApiResource
{

/** @var string */
public $shortName;

/** @var string */
public $description;

/** @var string */
public $iri;

/** @var array */
public $itemOperations;

/** @var array */
public $collectionOperations;

/** @var array */
public $attributes = [];

}
Loading

0 comments on commit 5745775

Please sign in to comment.