diff --git a/grammar/php5.y b/grammar/php5.y index 9706c5a8db..4843ee408c 100644 --- a/grammar/php5.y +++ b/grammar/php5.y @@ -628,7 +628,8 @@ array_expr: scalar_dereference: array_expr '[' dim_offset ']' { $$ = Expr\ArrayDimFetch[$1, $3]; } | T_CONSTANT_ENCAPSED_STRING '[' dim_offset ']' - { $$ = Expr\ArrayDimFetch[Scalar\String_[Scalar\String_::parse($1, false)], $3]; } + { $attrs = attributes(); $attrs['kind'] = strKind($1); + $$ = Expr\ArrayDimFetch[new Scalar\String_(Scalar\String_::parse($1), $attrs), $3]; } | constant '[' dim_offset ']' { $$ = Expr\ArrayDimFetch[$1, $3]; } | scalar_dereference '[' dim_offset ']' { $$ = Expr\ArrayDimFetch[$1, $3]; } /* alternative array syntax missing intentionally */ @@ -741,7 +742,9 @@ ctor_arguments: common_scalar: T_LNUMBER { $$ = Scalar\LNumber::fromString($1, attributes()); } | T_DNUMBER { $$ = Scalar\DNumber[Scalar\DNumber::parse($1)]; } - | T_CONSTANT_ENCAPSED_STRING { $$ = Scalar\String_[Scalar\String_::parse($1, false)]; } + | T_CONSTANT_ENCAPSED_STRING + { $attrs = attributes(); $attrs['kind'] = strKind($1); + $$ = new Scalar\String_(Scalar\String_::parse($1, false), $attrs); } | T_LINE { $$ = Scalar\MagicConst\Line[]; } | T_FILE { $$ = Scalar\MagicConst\File[]; } | T_DIR { $$ = Scalar\MagicConst\Dir[]; } @@ -751,9 +754,11 @@ common_scalar: | T_FUNC_C { $$ = Scalar\MagicConst\Function_[]; } | T_NS_C { $$ = Scalar\MagicConst\Namespace_[]; } | T_START_HEREDOC T_ENCAPSED_AND_WHITESPACE T_END_HEREDOC - { $$ = Scalar\String_[Scalar\String_::parseDocString($1, $2, false)]; } + { $attrs = attributes(); setDocStringAttrs($attrs, $1); + $$ = new Scalar\String_(Scalar\String_::parseDocString($1, $2, false), $attrs); } | T_START_HEREDOC T_END_HEREDOC - { $$ = Scalar\String_['']; } + { $attrs = attributes(); setDocStringAttrs($attrs, $1); + $$ = new Scalar\String_('', $attrs); } ; static_scalar: @@ -811,9 +816,11 @@ scalar: common_scalar { $$ = $1; } | constant { $$ = $1; } | '"' encaps_list '"' - { parseEncapsed($2, '"', false); $$ = Scalar\Encapsed[$2]; } + { $attrs = attributes(); $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED; + parseEncapsed($2, '"', true); $$ = new Scalar\Encapsed($2, $attrs); } | T_START_HEREDOC encaps_list T_END_HEREDOC - { parseEncapsedDoc($2, false); $$ = Scalar\Encapsed[$2]; } + { $attrs = attributes(); setDocStringAttrs($attrs, $1); + parseEncapsedDoc($2, true); $$ = new Scalar\Encapsed($2, $attrs); } ; static_array_pair_list: diff --git a/grammar/php7.y b/grammar/php7.y index 1cb3cf8027..83b5469df9 100644 --- a/grammar/php7.y +++ b/grammar/php7.y @@ -679,7 +679,9 @@ dereferencable_scalar: | '[' array_pair_list ']' { $attrs = attributes(); $attrs['kind'] = Expr\Array_::KIND_SHORT; $$ = new Expr\Array_($2, $attrs); } - | T_CONSTANT_ENCAPSED_STRING { $$ = Scalar\String_[Scalar\String_::parse($1)]; } + | T_CONSTANT_ENCAPSED_STRING + { $attrs = attributes(); $attrs['kind'] = strKind($1); + $$ = new Scalar\String_(Scalar\String_::parse($1), $attrs); } ; scalar: @@ -696,13 +698,17 @@ scalar: | dereferencable_scalar { $$ = $1; } | constant { $$ = $1; } | T_START_HEREDOC T_ENCAPSED_AND_WHITESPACE T_END_HEREDOC - { $$ = Scalar\String_[Scalar\String_::parseDocString($1, $2)]; } + { $attrs = attributes(); setDocStringAttrs($attrs, $1); + $$ = new Scalar\String_(Scalar\String_::parseDocString($1, $2), $attrs); } | T_START_HEREDOC T_END_HEREDOC - { $$ = Scalar\String_['']; } + { $attrs = attributes(); setDocStringAttrs($attrs, $1); + $$ = new Scalar\String_('', $attrs); } | '"' encaps_list '"' - { parseEncapsed($2, '"', true); $$ = Scalar\Encapsed[$2]; } + { $attrs = attributes(); $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED; + parseEncapsed($2, '"', true); $$ = new Scalar\Encapsed($2, $attrs); } | T_START_HEREDOC encaps_list T_END_HEREDOC - { parseEncapsedDoc($2, true); $$ = Scalar\Encapsed[$2]; } + { $attrs = attributes(); setDocStringAttrs($attrs, $1); + parseEncapsedDoc($2, true); $$ = new Scalar\Encapsed($2, $attrs); } ; optional_comma: diff --git a/grammar/rebuildParsers.php b/grammar/rebuildParsers.php index 8f13446db4..d7ecbe4660 100644 --- a/grammar/rebuildParsers.php +++ b/grammar/rebuildParsers.php @@ -178,6 +178,23 @@ function($matches) { . ' else { ' . $args[0] . ' = null; }'; } + if ('strKind' == $name) { + assertArgs(1, $args, $name); + + return '(' . $args[0] . '[0] === "\'" || (' . $args[0] . '[1] === "\'" && ' + . '(' . $args[0] . '[0] === \'b\' || ' . $args[0] . '[0] === \'B\')) ' + . '? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED)'; + } + + if ('setDocStringAttrs' == $name) { + assertArgs(2, $args, $name); + + return $args[0] . '[\'kind\'] = strpos(' . $args[1] . ', "\'") === false ' + . '? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; ' + . 'preg_match(\'/\A[bB]?<<<[ \t]*[\\\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\\\'"]?(?:\r\n|\n|\r)\z/\', ' . $args[1] . ', $matches); ' + . $args[0] . '[\'docLabel\'] = $matches[1];'; + } + return $matches[0]; }, $code diff --git a/lib/PhpParser/Node/Scalar/String_.php b/lib/PhpParser/Node/Scalar/String_.php index 2666be3917..d7bbf83e84 100644 --- a/lib/PhpParser/Node/Scalar/String_.php +++ b/lib/PhpParser/Node/Scalar/String_.php @@ -7,6 +7,12 @@ class String_ extends Scalar { + /* For use in "kind" attribute */ + const KIND_SINGLE_QUOTED = 1; + const KIND_DOUBLE_QUOTED = 2; + const KIND_HEREDOC = 3; + const KIND_NOWDOC = 4; + /** @var string String value */ public $value; diff --git a/lib/PhpParser/Parser/Php5.php b/lib/PhpParser/Parser/Php5.php index a40fd80ca0..d1664f3464 100644 --- a/lib/PhpParser/Parser/Php5.php +++ b/lib/PhpParser/Parser/Php5.php @@ -2429,7 +2429,8 @@ protected function reduceRule378() { } protected function reduceRule379() { - $this->semValue = new Expr\ArrayDimFetch(new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(4-1)], false), $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes; $attrs['kind'] = ($this->semStack[$this->stackPos-(4-1)][0] === "'" || ($this->semStack[$this->stackPos-(4-1)][1] === "'" && ($this->semStack[$this->stackPos-(4-1)][0] === 'b' || $this->semStack[$this->stackPos-(4-1)][0] === 'B')) ? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED); + $this->semValue = new Expr\ArrayDimFetch(new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(4-1)]), $attrs), $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes); } protected function reduceRule380() { @@ -2611,7 +2612,8 @@ protected function reduceRule420() { } protected function reduceRule421() { - $this->semValue = new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(1-1)], false), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes; $attrs['kind'] = ($this->semStack[$this->stackPos-(1-1)][0] === "'" || ($this->semStack[$this->stackPos-(1-1)][1] === "'" && ($this->semStack[$this->stackPos-(1-1)][0] === 'b' || $this->semStack[$this->stackPos-(1-1)][0] === 'B')) ? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED); + $this->semValue = new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(1-1)], false), $attrs); } protected function reduceRule422() { @@ -2647,11 +2649,13 @@ protected function reduceRule429() { } protected function reduceRule430() { - $this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)], false), $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];; + $this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)], false), $attrs); } protected function reduceRule431() { - $this->semValue = new Scalar\String_('', $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(2-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(2-1)], $matches); $attrs['docLabel'] = $matches[1];; + $this->semValue = new Scalar\String_('', $attrs); } protected function reduceRule432() { @@ -2827,11 +2831,13 @@ protected function reduceRule474() { } protected function reduceRule475() { - foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', false); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED; + foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', true); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs); } protected function reduceRule476() { - foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, false); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$this->stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];; + foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, true); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$this->stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs); } protected function reduceRule477() { diff --git a/lib/PhpParser/Parser/Php7.php b/lib/PhpParser/Parser/Php7.php index 90318d2a71..b3ab0b9c82 100644 --- a/lib/PhpParser/Parser/Php7.php +++ b/lib/PhpParser/Parser/Php7.php @@ -2375,7 +2375,8 @@ protected function reduceRule394() { } protected function reduceRule395() { - $this->semValue = new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(1-1)]), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes; $attrs['kind'] = ($this->semStack[$this->stackPos-(1-1)][0] === "'" || ($this->semStack[$this->stackPos-(1-1)][1] === "'" && ($this->semStack[$this->stackPos-(1-1)][0] === 'b' || $this->semStack[$this->stackPos-(1-1)][0] === 'B')) ? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED); + $this->semValue = new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(1-1)]), $attrs); } protected function reduceRule396() { @@ -2427,19 +2428,23 @@ protected function reduceRule407() { } protected function reduceRule408() { - $this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)]), $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];; + $this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)]), $attrs); } protected function reduceRule409() { - $this->semValue = new Scalar\String_('', $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(2-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(2-1)], $matches); $attrs['docLabel'] = $matches[1];; + $this->semValue = new Scalar\String_('', $attrs); } protected function reduceRule410() { - foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', true); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED; + foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', true); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs); } protected function reduceRule411() { - foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, true); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$this->stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes); + $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];; + foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, true); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$this->stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs); } protected function reduceRule412() { diff --git a/lib/PhpParser/PrettyPrinter/Standard.php b/lib/PhpParser/PrettyPrinter/Standard.php index 1027eeb834..2e6832ac07 100644 --- a/lib/PhpParser/PrettyPrinter/Standard.php +++ b/lib/PhpParser/PrettyPrinter/Standard.php @@ -84,10 +84,55 @@ public function pScalar_MagicConst_Trait(MagicConst\Trait_ $node) { // Scalars public function pScalar_String(Scalar\String_ $node) { - return '\'' . $this->pNoIndent(addcslashes($node->value, '\'\\')) . '\''; + $kind = $node->getAttribute('kind', Scalar\String_::KIND_SINGLE_QUOTED); + switch ($kind) { + case Scalar\String_::KIND_NOWDOC: + $label = $node->getAttribute('docLabel'); + if ($label && !$this->containsEndLabel($node->value, $label)) { + if ($node->value === '') { + return $this->pNoIndent("<<<'$label'\n$label") . $this->docStringEndToken; + } + + return $this->pNoIndent("<<<'$label'\n$node->value\n$label") + . $this->docStringEndToken; + } + /* break missing intentionally */ + case Scalar\String_::KIND_SINGLE_QUOTED: + return '\'' . $this->pNoIndent(addcslashes($node->value, '\'\\')) . '\''; + case Scalar\String_::KIND_HEREDOC: + $label = $node->getAttribute('docLabel'); + if ($label && !$this->containsEndLabel($node->value, $label)) { + if ($node->value === '') { + return $this->pNoIndent("<<<$label\n$label") . $this->docStringEndToken; + } + + $escaped = $this->escapeString($node->value, null); + return $this->pNoIndent("<<<$label\n" . $escaped ."\n$label") + . $this->docStringEndToken; + } + /* break missing intentionally */ + case Scalar\String_::KIND_DOUBLE_QUOTED: + return '"' . $this->escapeString($node->value, '"') . '"'; + } + throw new \Exception('Invalid string kind'); } public function pScalar_Encapsed(Scalar\Encapsed $node) { + if ($node->getAttribute('kind') === Scalar\String_::KIND_HEREDOC) { + $label = $node->getAttribute('docLabel'); + if ($label && !$this->encapsedContainsEndLabel($node->parts, $label)) { + if (count($node->parts) === 1 + && $node->parts[0] instanceof Scalar\EncapsedStringPart + && $node->parts[0]->value === '' + ) { + return $this->pNoIndent("<<<$label\n$label") . $this->docStringEndToken; + } + + return $this->pNoIndent( + "<<<$label\n" . $this->pEncapsList($node->parts, null) . "\n$label" + ) . $this->docStringEndToken; + } + } return '"' . $this->pEncapsList($node->parts, '"') . '"'; } @@ -103,6 +148,7 @@ public function pScalar_LNumber(Scalar\LNumber $node) { case Scalar\LNumber::KIND_HEX: return '0x' . base_convert($str, 10, 16); } + throw new \Exception('Invalid number kind'); } public function pScalar_DNumber(Scalar\DNumber $node) { @@ -790,7 +836,7 @@ protected function pEncapsList(array $encapsList, $quote) { $return = ''; foreach ($encapsList as $element) { if ($element instanceof Scalar\EncapsedStringPart) { - $return .= addcslashes($element->value, "\n\r\t\f\v$" . $quote . "\\"); + $return .= $this->escapeString($element->value, $quote); } else { $return .= '{' . $this->p($element) . '}'; } @@ -799,6 +845,34 @@ protected function pEncapsList(array $encapsList, $quote) { return $return; } + protected function escapeString($string, $quote) { + if (null === $quote) { + // For doc strings, don't escape newlines + return addcslashes($string, "\t\f\v$\\"); + } + return addcslashes($string, "\n\r\t\f\v$" . $quote . "\\"); + } + + protected function containsEndLabel($string, $label, $atStart = true, $atEnd = true) { + $start = $atStart ? '(?:^|[\r\n])' : '[\r\n]'; + $end = $atEnd ? '(?:$|[;\r\n])' : '[;\r\n]'; + return false !== strpos($string, $label) + && preg_match('/' . $start . $label . $end . '/', $string); + } + + protected function encapsedContainsEndLabel(array $parts, $label) { + foreach ($parts as $i => $part) { + $atStart = $i === 0; + $atEnd = $i === count($parts) - 1; + if ($part instanceof Scalar\EncapsedStringPart + && $this->containsEndLabel($part->value, $label, $atStart, $atEnd) + ) { + return true; + } + } + return false; + } + protected function pDereferenceLhs(Node $node) { if ($node instanceof Expr\Variable || $node instanceof Name diff --git a/lib/PhpParser/PrettyPrinterAbstract.php b/lib/PhpParser/PrettyPrinterAbstract.php index d8f9ec8137..3cd5d7ded6 100644 --- a/lib/PhpParser/PrettyPrinterAbstract.php +++ b/lib/PhpParser/PrettyPrinterAbstract.php @@ -75,6 +75,7 @@ abstract class PrettyPrinterAbstract ); protected $noIndentToken; + protected $docStringEndToken; protected $canUseSemicolonNamespaces; protected $options; @@ -89,6 +90,7 @@ abstract class PrettyPrinterAbstract */ public function __construct(array $options = []) { $this->noIndentToken = '_NO_INDENT_' . mt_rand(); + $this->docStringEndToken = '_DOC_STRING_END_' . mt_rand(); $defaultOptions = ['shortArraySyntax' => false]; $this->options = $options + $defaultOptions; @@ -104,7 +106,7 @@ public function __construct(array $options = []) { public function prettyPrint(array $stmts) { $this->preprocessNodes($stmts); - return ltrim(str_replace("\n" . $this->noIndentToken, "\n", $this->pStmts($stmts, false))); + return ltrim($this->handleMagicTokens($this->pStmts($stmts, false))); } /** @@ -115,7 +117,7 @@ public function prettyPrint(array $stmts) { * @return string Pretty printed node */ public function prettyPrintExpr(Expr $node) { - return str_replace("\n" . $this->noIndentToken, "\n", $this->p($node)); + return $this->handleMagicTokens($this->p($node)); } /** @@ -157,6 +159,17 @@ protected function preprocessNodes(array $nodes) { } } + protected function handleMagicTokens($str) { + // Drop no-indent tokens + $str = str_replace($this->noIndentToken, '', $str); + + // Replace doc-string-end tokens with nothing or a newline + $str = str_replace($this->docStringEndToken . ";\n", ";\n", $str); + $str = str_replace($this->docStringEndToken, "\n", $str); + + return $str; + } + /** * Pretty prints an array of nodes (statements) and indents them optionally. * diff --git a/test/PhpParser/ParserTest.php b/test/PhpParser/ParserTest.php index 669f4134e1..85e726d8dc 100644 --- a/test/PhpParser/ParserTest.php +++ b/test/PhpParser/ParserTest.php @@ -3,6 +3,9 @@ namespace PhpParser; use PhpParser\Comment; +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar; +use PhpParser\Node\Scalar\String_; abstract class ParserTest extends \PHPUnit_Framework_TestCase { @@ -105,6 +108,52 @@ public function testInvalidToken() { $parser = $this->getParser($lexer); $parser->parse('dummy'); } + + /** + * @dataProvider provideTestKindAttributes + */ + public function testKindAttributes($code, $expectedAttributes) { + $parser = $this->getParser(new Lexer); + $stmts = $parser->parse("getAttributes(); + foreach ($expectedAttributes as $name => $value) { + $this->assertSame($value, $attributes[$name]); + } + } + + public function provideTestKindAttributes() { + return array( + array('0', ['kind' => Scalar\LNumber::KIND_DEC]), + array('9', ['kind' => Scalar\LNumber::KIND_DEC]), + array('07', ['kind' => Scalar\LNumber::KIND_OCT]), + array('0xf', ['kind' => Scalar\LNumber::KIND_HEX]), + array('0XF', ['kind' => Scalar\LNumber::KIND_HEX]), + array('0b1', ['kind' => Scalar\LNumber::KIND_BIN]), + array('0B1', ['kind' => Scalar\LNumber::KIND_BIN]), + array('[]', ['kind' => Expr\Array_::KIND_SHORT]), + array('array()', ['kind' => Expr\Array_::KIND_LONG]), + array("'foo'", ['kind' => String_::KIND_SINGLE_QUOTED]), + array("b'foo'", ['kind' => String_::KIND_SINGLE_QUOTED]), + array("B'foo'", ['kind' => String_::KIND_SINGLE_QUOTED]), + array('"foo"', ['kind' => String_::KIND_DOUBLE_QUOTED]), + array('b"foo"', ['kind' => String_::KIND_DOUBLE_QUOTED]), + array('B"foo"', ['kind' => String_::KIND_DOUBLE_QUOTED]), + array('"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]), + array('b"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]), + array('B"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]), + array("<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']), + array("<< String_::KIND_HEREDOC, 'docLabel' => 'STR']), + array("<<<\"STR\"\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']), + array("b<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']), + array("B<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']), + array("<<< \t 'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']), + array("<<<'\xff'\n\xff\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => "\xff"]), + array("<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']), + array("b<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']), + array("B<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']), + array("<<< \t \"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']), + ); + } } class InvalidTokenLexer extends Lexer { diff --git a/test/PhpParser/PrettyPrinterTest.php b/test/PhpParser/PrettyPrinterTest.php index ac072a2f41..9dbd3d30d9 100644 --- a/test/PhpParser/PrettyPrinterTest.php +++ b/test/PhpParser/PrettyPrinterTest.php @@ -4,6 +4,8 @@ use PhpParser\Comment; use PhpParser\Node\Expr; +use PhpParser\Node\Scalar\Encapsed; +use PhpParser\Node\Scalar\EncapsedStringPart; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PhpParser\PrettyPrinter\Standard; @@ -111,4 +113,52 @@ public function testArraySyntaxDefault() { $expected = "['key' => 'val']"; $this->assertSame($expected, $prettyPrinter->prettyPrintExpr($expr)); } + + /** + * @dataProvider provideTestKindAttributes + */ + public function testKindAttributes($node, $expected) { + $prttyPrinter = new PrettyPrinter\Standard; + $result = $prttyPrinter->prettyPrintExpr($node); + $this->assertSame($expected, $result); + } + + public function provideTestKindAttributes() { + $nowdoc = ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']; + $heredoc = ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR']; + return [ + // Defaults to single quoted + [new String_('foo'), "'foo'"], + // Explicit single/double quoted + [new String_('foo', ['kind' => String_::KIND_SINGLE_QUOTED]), "'foo'"], + [new String_('foo', ['kind' => String_::KIND_DOUBLE_QUOTED]), '"foo"'], + // Fallback from doc string if no label + [new String_('foo', ['kind' => String_::KIND_NOWDOC]), "'foo'"], + [new String_('foo', ['kind' => String_::KIND_HEREDOC]), '"foo"'], + // Fallback if string contains label + [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'A']), "'A\nB\nC'"], + [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'B']), "'A\nB\nC'"], + [new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'C']), "'A\nB\nC'"], + [new String_("STR;", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']), "'STR;'"], + // Doc string if label not contained (or not in ending position) + [new String_("foo", $nowdoc), "<<<'STR'\nfoo\nSTR\n"], + [new String_("foo", $heredoc), "<< 5 + + 1 + Foo diff --git a/test/code/prettyPrinter/expr/docStrings.test b/test/code/prettyPrinter/expr/docStrings.test new file mode 100644 index 0000000000..a4a60acead --- /dev/null +++ b/test/code/prettyPrinter/expr/docStrings.test @@ -0,0 +1,86 @@ +Literals +----- +d} +STR; + +call( + <<d} +STR; +call(<<