Merge pull request #245 from TysonAndre/fix-short-echo

Fixes #220 : Properly parse expression lists passed to `<?=`
This commit is contained in:
Rob Lourens 2018-06-11 16:33:48 -07:00 коммит произвёл GitHub
Родитель 01479d7e0a 80071b4d1e
Коммит bce97ae2fc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 521 добавлений и 59 удалений

Просмотреть файл

@ -10,9 +10,18 @@ use Microsoft\PhpParser\Node\Expression;
use Microsoft\PhpParser\Node\DelimitedList\ExpressionList;
use Microsoft\PhpParser\Token;
/**
* This represents either a literal echo expression (`echo expr`)
* or a short echo tag (`<?= expr...`)
*
* TODO: An echo statement cannot be used as an expression.
* Consider refactoring this to become EchoStatement in a future backwards incompatible release.
*/
class EchoExpression extends Expression {
/** @var Token */
/**
* @var Token|null this is null if generated from `<?=`
*/
public $echoKeyword;
/** @var ExpressionList */

Просмотреть файл

@ -19,9 +19,23 @@ class InlineHtml extends StatementNode {
/** @var Token|null */
public $scriptSectionStartTag;
/**
* @var ExpressionStatement|null used to represent the expression echoed by `<?=` while parsing.
*
* This should always be null in the returned AST,
* and is deliberately excluded from CHILD_NAMES.
*
* This will be null under any of these conditions:
*
* - The scriptSectionStartTag isn't TokenKind::ScriptSectionStartWithEchoTag,
* - The echoStatement was normalized and moved into a statement list.
* If a caller doesn't do this, that's a bug.
*/
public $echoStatement;
const CHILD_NAMES = [
'scriptSectionEndTag',
'text',
'scriptSectionStartTag'
'scriptSectionStartTag',
];
}

Просмотреть файл

@ -152,7 +152,13 @@ class Parser {
$sourceFile->uri = $uri;
$sourceFile->statementList = array();
if ($this->getCurrentToken()->kind !== TokenKind::EndOfFileToken) {
$sourceFile->statementList[] = $this->parseInlineHtml($sourceFile);
$inlineHTML = $this->parseInlineHtml($sourceFile);
$sourceFile->statementList[] = $inlineHTML;
if ($inlineHTML->echoStatement) {
$sourceFile->statementList[] = $inlineHTML->echoStatement;
$inlineHTML->echoStatement->parent = $sourceFile;
$inlineHTML->echoStatement = null;
}
}
$sourceFile->statementList =
\array_merge($sourceFile->statementList, $this->parseList($sourceFile, ParseContext::SourceElements));
@ -192,6 +198,11 @@ class Parser {
$element = $parseListElementFn($parentNode);
if ($element instanceof Node) {
$element->parent = $parentNode;
if ($element instanceof InlineHtml && $element->echoStatement && $listParseContext === ParseContext::SourceElements) {
$nodeArray[] = $element->echoStatement;
$element->echoStatement->parent = $parentNode;
$element->echoStatement = null;
}
}
$nodeArray[] = $element;
continue;
@ -534,6 +545,9 @@ class Parser {
case TokenKind::SemicolonToken:
return $this->parseEmptyStatement($parentNode);
case TokenKind::EchoKeyword:
return $this->parseEchoStatement($parentNode);
// trait-declaration
case TokenKind::TraitKeyword:
return $this->parseTraitDeclaration($parentNode);
@ -931,8 +945,6 @@ class Parser {
return $this->parseArrayCreationExpression($parentNode);
// intrinsic-construct
case TokenKind::EchoKeyword:
return $this->parseEchoExpression($parentNode);
case TokenKind::ListKeyword:
return $this->parseListIntrinsicExpression($parentNode);
case TokenKind::UnsetKeyword:
@ -2172,14 +2184,21 @@ class Parser {
return $scriptInclusionExpression;
}
private function parseEchoExpression($parentNode) {
private function parseEchoStatement($parentNode) {
$expressionStatement = new ExpressionStatement();
// TODO: Could flatten into EchoStatement instead?
$echoExpression = new EchoExpression();
$echoExpression->parent = $parentNode;
$echoExpression->parent = $expressionStatement;
$echoExpression->echoKeyword = $this->eat1(TokenKind::EchoKeyword);
$echoExpression->expressions =
$this->parseExpressionList($echoExpression);
return $echoExpression;
$expressionStatement->parent = $parentNode;
$expressionStatement->expression = $echoExpression;
$expressionStatement->semicolon = $this->eatSemicolonOrAbortStatement();
return $expressionStatement;
}
private function parseListIntrinsicExpression($parentNode) {
@ -3202,7 +3221,24 @@ class Parser {
$inlineHtml->parent = $parentNode;
$inlineHtml->scriptSectionEndTag = $this->eatOptional1(TokenKind::ScriptSectionEndTag);
$inlineHtml->text = $this->eatOptional1(TokenKind::InlineHtml);
$inlineHtml->scriptSectionStartTag = $this->eatOptional1(TokenKind::ScriptSectionStartTag);
$inlineHtml->scriptSectionStartTag = $this->eatOptional(TokenKind::ScriptSectionStartTag, TokenKind::ScriptSectionStartWithEchoTag);
// This is the easiest way to represent `<?= "expr", "other" `
if (($inlineHtml->scriptSectionStartTag->kind ?? null) === TokenKind::ScriptSectionStartWithEchoTag) {
$echoStatement = new ExpressionStatement();
$echoExpression = new EchoExpression();
$expressionList = $this->parseExpressionList($echoExpression) ?? (new MissingToken(TokenKind::Expression, $this->token->fullStart));
$echoExpression->expressions = $expressionList;
$echoExpression->parent = $echoStatement;
$echoStatement->expression = $echoExpression;
$echoStatement->semicolon = $this->eatSemicolonOrAbortStatement();
$echoStatement->parent = $inlineHtml;
// Deliberately leave echoKeyword as null instead of MissingToken
$inlineHtml->echoStatement = $echoStatement;
}
return $inlineHtml;
}

Просмотреть файл

@ -265,7 +265,7 @@ class PhpTokenizer implements TokenStreamProviderInterface {
T_DNUMBER => TokenKind::FloatingLiteralToken,
T_OPEN_TAG => TokenKind::ScriptSectionStartTag,
T_OPEN_TAG_WITH_ECHO => TokenKind::ScriptSectionStartTag,
T_OPEN_TAG_WITH_ECHO => TokenKind::ScriptSectionStartWithEchoTag,
T_CLOSE_TAG => TokenKind::ScriptSectionEndTag,
T_INLINE_HTML => TokenKind::InlineHtml,

Просмотреть файл

@ -167,7 +167,7 @@ class TokenKind {
const ScriptSectionStartTag = 323;
const ScriptSectionEndTag = 324;
const ScriptSectionStartWithEchoTag = 419;
// TODO how to handle incremental parsing w/ this?
const ScriptSectionPrependedText = 325;

Просмотреть файл

@ -164,7 +164,7 @@ class TokenStringMaps {
"<>" => TokenKind::LessThanGreaterThanToken,
"..." => TokenKind::DotDotDotToken,
"\\" => TokenKind::BackslashToken,
"<?=" => TokenKind::ScriptSectionStartTag, // TODO, technically not an operator
"<?=" => TokenKind::ScriptSectionStartWithEchoTag, // TODO, technically not an operator
"<?php " => TokenKind::ScriptSectionStartTag, // TODO, technically not an operator
"<?php\t" => TokenKind::ScriptSectionStartTag, // TODO add tests
"<?php\n" => TokenKind::ScriptSectionStartTag,

Просмотреть файл

@ -6,7 +6,7 @@
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"kind": "ScriptSectionStartWithEchoTag",
"textLength": 3
}
}
@ -14,13 +14,24 @@
{
"ExpressionStatement": {
"expression": {
"StringLiteral": {
"startQuote": null,
"children": {
"kind": "StringLiteralToken",
"textLength": 7
},
"endQuote": null
"EchoExpression": {
"echoKeyword": null,
"expressions": {
"ExpressionList": {
"children": [
{
"StringLiteral": {
"startQuote": null,
"children": {
"kind": "StringLiteralToken",
"textLength": 7
},
"endQuote": null
}
}
]
}
}
}
},
"semicolon": null

Просмотреть файл

@ -0,0 +1 @@
<?="hello","world"?>

Просмотреть файл

@ -0,0 +1 @@
[]

Просмотреть файл

@ -0,0 +1,70 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartWithEchoTag",
"textLength": 3
}
}
},
{
"ExpressionStatement": {
"expression": {
"EchoExpression": {
"echoKeyword": null,
"expressions": {
"ExpressionList": {
"children": [
{
"StringLiteral": {
"startQuote": null,
"children": {
"kind": "StringLiteralToken",
"textLength": 7
},
"endQuote": null
}
},
{
"kind": "CommaToken",
"textLength": 1
},
{
"StringLiteral": {
"startQuote": null,
"children": {
"kind": "StringLiteralToken",
"textLength": 7
},
"endQuote": null
}
}
]
}
}
}
},
"semicolon": null
}
},
{
"InlineHtml": {
"scriptSectionEndTag": {
"kind": "ScriptSectionEndTag",
"textLength": 2
},
"text": null,
"scriptSectionStartTag": null
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}

Просмотреть файл

@ -0,0 +1 @@
<?= echo "hello"; ?>

Просмотреть файл

@ -0,0 +1,14 @@
[
{
"kind": 0,
"message": "'Expression' expected.",
"start": 3,
"length": 0
},
{
"kind": 0,
"message": "';' expected.",
"start": 3,
"length": 0
}
]

Просмотреть файл

@ -0,0 +1,87 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartWithEchoTag",
"textLength": 3
}
}
},
{
"ExpressionStatement": {
"expression": {
"EchoExpression": {
"echoKeyword": null,
"expressions": {
"ExpressionList": {
"children": [
{
"error": "MissingToken",
"kind": "Expression",
"textLength": 0
}
]
}
}
}
},
"semicolon": {
"error": "MissingToken",
"kind": "SemicolonToken",
"textLength": 0
}
}
},
{
"ExpressionStatement": {
"expression": {
"EchoExpression": {
"echoKeyword": {
"kind": "EchoKeyword",
"textLength": 4
},
"expressions": {
"ExpressionList": {
"children": [
{
"StringLiteral": {
"startQuote": null,
"children": {
"kind": "StringLiteralToken",
"textLength": 7
},
"endQuote": null
}
}
]
}
}
}
},
"semicolon": {
"kind": "SemicolonToken",
"textLength": 1
}
}
},
{
"InlineHtml": {
"scriptSectionEndTag": {
"kind": "ScriptSectionEndTag",
"textLength": 2
},
"text": null,
"scriptSectionStartTag": null
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}

Просмотреть файл

@ -0,0 +1,5 @@
<?php
// FIXME the tolerant-php-parser has a bug, this code always echoes if executed.
// (i.e. same as `if (false); echo "hello world"` with an implicit semicolon)
// NOTE: If the inline HTML is surrounded by brackets, then this would never echo.
if (false)?>hello world<?php

Просмотреть файл

@ -0,0 +1 @@
[]

Просмотреть файл

@ -0,0 +1,65 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
{
"IfStatementNode": {
"ifKeyword": {
"kind": "IfKeyword",
"textLength": 2
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"expression": {
"ReservedWord": {
"children": {
"kind": "FalseReservedWord",
"textLength": 5
}
}
},
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
},
"colon": null,
"statements": {
"InlineHtml": {
"scriptSectionEndTag": {
"kind": "ScriptSectionEndTag",
"textLength": 2
},
"text": {
"kind": "InlineHtml",
"textLength": 11
},
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
"elseIfClauses": [],
"elseClause": null,
"endifKeyword": null,
"semicolon": null
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}

Просмотреть файл

@ -0,0 +1 @@
<?= "expression" // properly warns about missing ';'

Просмотреть файл

@ -0,0 +1,8 @@
[
{
"kind": 0,
"message": "';' expected.",
"start": 16,
"length": 0
}
]

Просмотреть файл

@ -0,0 +1,50 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartWithEchoTag",
"textLength": 3
}
}
},
{
"ExpressionStatement": {
"expression": {
"EchoExpression": {
"echoKeyword": null,
"expressions": {
"ExpressionList": {
"children": [
{
"StringLiteral": {
"startQuote": null,
"children": {
"kind": "StringLiteralToken",
"textLength": 12
},
"endQuote": null
}
}
]
}
}
}
},
"semicolon": {
"error": "MissingToken",
"kind": "SemicolonToken",
"textLength": 0
}
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}

Просмотреть файл

@ -0,0 +1 @@
<?php echo $a ?>

Просмотреть файл

@ -0,0 +1 @@
[]

Просмотреть файл

@ -0,0 +1,58 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
{
"ExpressionStatement": {
"expression": {
"EchoExpression": {
"echoKeyword": {
"kind": "EchoKeyword",
"textLength": 4
},
"expressions": {
"ExpressionList": {
"children": [
{
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 2
}
}
}
]
}
}
}
},
"semicolon": null
}
},
{
"InlineHtml": {
"scriptSectionEndTag": {
"kind": "ScriptSectionEndTag",
"textLength": 3
},
"text": null,
"scriptSectionStartTag": null
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}

Просмотреть файл

@ -1 +1,8 @@
[]
[
{
"kind": 0,
"message": "'Expression' expected.",
"start": 3,
"length": 0
}
]

Просмотреть файл

@ -6,13 +6,23 @@
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"kind": "ScriptSectionStartWithEchoTag",
"textLength": 3
}
}
},
{
"EmptyStatement": {
"ExpressionStatement": {
"expression": {
"EchoExpression": {
"echoKeyword": null,
"expressions": {
"error": "MissingToken",
"kind": "Expression",
"textLength": 0
}
}
},
"semicolon": {
"kind": "SemicolonToken",
"textLength": 1

Просмотреть файл

@ -9,7 +9,7 @@
"textLength": 4
},
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"kind": "ScriptSectionStartWithEchoTag",
"textLength": 3
}
}
@ -17,44 +17,55 @@
{
"ExpressionStatement": {
"expression": {
"AssignmentExpression": {
"leftOperand": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 8
}
}
},
"operator": {
"kind": "EqualsToken",
"textLength": 1
},
"byRef": null,
"rightOperand": {
"CallExpression": {
"callableExpression": {
"QualifiedName": {
"globalSpecifier": null,
"relativeSpecifier": null,
"nameParts": [
{
"kind": "Name",
"EchoExpression": {
"echoKeyword": null,
"expressions": {
"ExpressionList": {
"children": [
{
"AssignmentExpression": {
"leftOperand": {
"Variable": {
"dollar": null,
"name": {
"kind": "VariableName",
"textLength": 8
}
}
},
"operator": {
"kind": "EqualsToken",
"textLength": 1
},
"byRef": null,
"rightOperand": {
"CallExpression": {
"callableExpression": {
"QualifiedName": {
"globalSpecifier": null,
"relativeSpecifier": null,
"nameParts": [
{
"kind": "Name",
"textLength": 1
}
]
}
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"argumentExpressionList": null,
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
}
}
}
]
}
}
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"argumentExpressionList": null,
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
}
]
}
}
}