Skip to content

Commit 5e2f2e0

Browse files
committed
QuoteAwareConstExprStringNode with correct quotes and escaping in __toString()
Currently opt-in with new ConstExprParser and TypeParser constructor argument
1 parent 80e5c87 commit 5e2f2e0

File tree

5 files changed

+128
-12
lines changed

5 files changed

+128
-12
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\ConstExpr;
4+
5+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
use function addcslashes;
7+
use function assert;
8+
use function dechex;
9+
use function ord;
10+
use function preg_replace_callback;
11+
use function sprintf;
12+
use function str_pad;
13+
use function strlen;
14+
15+
class QuoteAwareConstExprStringNode implements ConstExprNode
16+
{
17+
18+
public const SINGLE_QUOTED = 1;
19+
public const DOUBLE_QUOTED = 2;
20+
21+
use NodeAttributes;
22+
23+
/** @var string */
24+
public $value;
25+
26+
/** @var self::SINGLE_QUOTED|self::DOUBLE_QUOTED */
27+
public $quoteType;
28+
29+
/**
30+
* @param self::SINGLE_QUOTED|self::DOUBLE_QUOTED $quoteType
31+
*/
32+
public function __construct(string $value, int $quoteType)
33+
{
34+
$this->value = $value;
35+
$this->quoteType = $quoteType;
36+
}
37+
38+
39+
public function __toString(): string
40+
{
41+
if ($this->quoteType === self::SINGLE_QUOTED) {
42+
// from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1007
43+
return sprintf("'%s'", addcslashes($this->value, '\'\\'));
44+
}
45+
46+
// from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1010-L1040
47+
return sprintf('"%s"', $this->escapeDoubleQuotedString());
48+
}
49+
50+
private function escapeDoubleQuotedString() {
51+
$quote = '"';
52+
$escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . "\\");
53+
54+
// Escape control characters and non-UTF-8 characters.
55+
// Regex based on https://stackoverflow.com/a/11709412/385378.
56+
$regex = '/(
57+
[\x00-\x08\x0E-\x1F] # Control characters
58+
| [\xC0-\xC1] # Invalid UTF-8 Bytes
59+
| [\xF5-\xFF] # Invalid UTF-8 Bytes
60+
| \xE0(?=[\x80-\x9F]) # Overlong encoding of prior code point
61+
| \xF0(?=[\x80-\x8F]) # Overlong encoding of prior code point
62+
| [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
63+
| [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
64+
| [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
65+
| (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
66+
| (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]|[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
67+
| (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
68+
| (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
69+
| (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
70+
)/x';
71+
return preg_replace_callback($regex, function ($matches) {
72+
assert(strlen($matches[0]) === 1);
73+
$hex = dechex(ord($matches[0]));;
74+
return '\\x' . str_pad($hex, 2, '0', \STR_PAD_LEFT);
75+
}, $escaped);
76+
}
77+
78+
}

src/Ast/Type/ArrayShapeItemNode.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
66
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
7+
use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode;
78
use PHPStan\PhpDocParser\Ast\NodeAttributes;
89
use function sprintf;
910

@@ -12,7 +13,7 @@ class ArrayShapeItemNode implements TypeNode
1213

1314
use NodeAttributes;
1415

15-
/** @var ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null */
16+
/** @var ConstExprIntegerNode|QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode|null */
1617
public $keyName;
1718

1819
/** @var bool */
@@ -22,7 +23,7 @@ class ArrayShapeItemNode implements TypeNode
2223
public $valueType;
2324

2425
/**
25-
* @param ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null $keyName
26+
* @param ConstExprIntegerNode|QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode|null $keyName
2627
*/
2728
public function __construct($keyName, bool $optional, TypeNode $valueType)
2829
{

src/Ast/Type/ObjectShapeItemNode.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\PhpDocParser\Ast\Type;
44

55
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
6+
use PHPStan\PhpDocParser\Ast\ConstExpr\QuoteAwareConstExprStringNode;
67
use PHPStan\PhpDocParser\Ast\NodeAttributes;
78
use function sprintf;
89

@@ -11,7 +12,7 @@ class ObjectShapeItemNode implements TypeNode
1112

1213
use NodeAttributes;
1314

14-
/** @var ConstExprStringNode|IdentifierTypeNode */
15+
/** @var QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode */
1516
public $keyName;
1617

1718
/** @var bool */
@@ -21,7 +22,7 @@ class ObjectShapeItemNode implements TypeNode
2122
public $valueType;
2223

2324
/**
24-
* @param ConstExprStringNode|IdentifierTypeNode $keyName
25+
* @param QuoteAwareConstExprStringNode|ConstExprStringNode|IdentifierTypeNode $keyName
2526
*/
2627
public function __construct($keyName, bool $optional, TypeNode $valueType)
2728
{

src/Parser/ConstExprParser.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@ class ConstExprParser
2828
/** @var bool */
2929
private $unescapeStrings;
3030

31-
public function __construct(bool $unescapeStrings = false)
31+
/** @var bool */
32+
private $quoteAwareConstExprString;
33+
34+
public function __construct(bool $unescapeStrings = false, bool $quoteAwareConstExprString = false)
3235
{
3336
$this->unescapeStrings = $unescapeStrings;
37+
$this->quoteAwareConstExprString = $quoteAwareConstExprString;
3438
}
3539

3640
public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode
@@ -49,6 +53,7 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con
4953

5054
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
5155
$value = $tokens->currentTokenValue();
56+
$type = $tokens->currentTokenType();
5257
if ($trimStrings) {
5358
if ($this->unescapeStrings) {
5459
$value = self::unescapeString($value);
@@ -57,6 +62,16 @@ public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\Con
5762
}
5863
}
5964
$tokens->next();
65+
66+
if ($this->quoteAwareConstExprString) {
67+
return new Ast\ConstExpr\QuoteAwareConstExprStringNode(
68+
$value,
69+
$type === Lexer::TOKEN_SINGLE_QUOTED_STRING
70+
? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED
71+
: Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED
72+
);
73+
}
74+
6075
return new Ast\ConstExpr\ConstExprStringNode($value);
6176

6277
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {

src/Parser/TypeParser.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ class TypeParser
1515
/** @var ConstExprParser|null */
1616
private $constExprParser;
1717

18-
public function __construct(?ConstExprParser $constExprParser = null)
18+
/** @var bool */
19+
private $quoteAwareConstExprString;
20+
21+
public function __construct(?ConstExprParser $constExprParser = null, bool $quoteAwareConstExprString = false)
1922
{
2023
$this->constExprParser = $constExprParser;
24+
$this->quoteAwareConstExprString = $quoteAwareConstExprString;
2125
}
2226

2327
/** @phpstan-impure */
@@ -562,7 +566,7 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape
562566

563567
/**
564568
* @phpstan-impure
565-
* @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
569+
* @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\QuoteAwareConstExprStringNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
566570
*/
567571
private function parseArrayShapeKey(TokenIterator $tokens)
568572
{
@@ -571,11 +575,20 @@ private function parseArrayShapeKey(TokenIterator $tokens)
571575
$tokens->next();
572576

573577
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
574-
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
578+
if ($this->quoteAwareConstExprString) {
579+
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), "'"), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
580+
} else {
581+
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
582+
}
575583
$tokens->next();
576584

577585
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
578-
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
586+
if ($this->quoteAwareConstExprString) {
587+
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), '"'), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
588+
} else {
589+
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
590+
}
591+
579592
$tokens->next();
580593

581594
} else {
@@ -626,16 +639,24 @@ private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectSha
626639

627640
/**
628641
* @phpstan-impure
629-
* @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
642+
* @return Ast\ConstExpr\QuoteAwareConstExprStringNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
630643
*/
631644
private function parseObjectShapeKey(TokenIterator $tokens)
632645
{
633646
if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
634-
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
647+
if ($this->quoteAwareConstExprString) {
648+
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), "'"), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
649+
} else {
650+
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
651+
}
635652
$tokens->next();
636653

637654
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
638-
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
655+
if ($this->quoteAwareConstExprString) {
656+
$key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(trim($tokens->currentTokenValue(), '"'), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
657+
} else {
658+
$key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
659+
}
639660
$tokens->next();
640661

641662
} else {

0 commit comments

Comments
 (0)