Skip to content

Fix handling of HaltCompilerStatement #389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/Node/Statement/HaltCompilerStatement.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,26 @@ class HaltCompilerStatement extends Expression {
/** @var Token */
public $closeParen;

/** @var Token */
public $semicolon;
/** @var Token (there is an implicit ')' before php close tags (`?>`)) */
public $semicolonOrCloseTag;

/** @var Token */
/** @var Token|null TokenKind::InlineHtml data unless there are no bytes (This is optional if there is nothing after the semicolon) */
public $data;

const CHILD_NAMES = [
'haltCompilerKeyword',
'openParen',
'closeParen',
'semicolon',
'semicolonOrCloseTag',
'data',
];

/**
* @return int
*/
public function getHaltCompilerOffset() {
// This accounts for the fact that PHP close tags may include a single newline,
// and that $this->data may be null.
return $this->semicolonOrCloseTag->getEndPosition();
}
}
30 changes: 21 additions & 9 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2858,15 +2858,27 @@ private function parseUnsetStatement($parentNode) {
}

private function parseHaltCompilerStatement($parentNode) {
$unsetStatement = new HaltCompilerStatement();
$unsetStatement->parent = $parentNode;

$unsetStatement->haltCompilerKeyword = $this->eat1(TokenKind::HaltCompilerKeyword);
$unsetStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$unsetStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
$unsetStatement->semicolon = $this->eatSemicolonOrAbortStatement();
$unsetStatement->data = $this->eatOptional1(TokenKind::InlineHtml);
return $unsetStatement;
$haltCompilerStatement = new HaltCompilerStatement();
$haltCompilerStatement->parent = $parentNode;

$haltCompilerStatement->haltCompilerKeyword = $this->eat1(TokenKind::HaltCompilerKeyword);
$haltCompilerStatement->openParen = $this->eat1(TokenKind::OpenParenToken);
$haltCompilerStatement->closeParen = $this->eat1(TokenKind::CloseParenToken);
// There is an implicit ';' before the closing php tag.
$haltCompilerStatement->semicolonOrCloseTag = $this->eat(TokenKind::SemicolonToken, TokenKind::ScriptSectionEndTag);
// token_get_all() will return up to 3 tokens after __halt_compiler regardless of whether they're the right ones.
// For invalid php snippets, combine the remaining tokens into InlineHtml
$remainingTokens = [];
while ($this->token->kind !== TokenKind::EndOfFileToken) {
$remainingTokens[] = $this->token;
$this->advanceToken();
}
if ($remainingTokens) {
$firstToken = $remainingTokens[0];
$lastToken = end($remainingTokens);
$haltCompilerStatement->data = new Token(TokenKind::InlineHtml, $firstToken->fullStart, $firstToken->fullStart, $lastToken->fullStart + $lastToken->length - $firstToken->fullStart);
}
return $haltCompilerStatement;
}

private function parseArrayCreationExpression($parentNode) {
Expand Down
2 changes: 1 addition & 1 deletion src/Token.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Token implements \JsonSerializable {
public $fullStart;
/** @var int */
public $start;
/** @var int */
/** @var int the length is equal to $this->getEndPosition() - $this->fullStart. */
public $length;

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/cases/parser/haltCompiler1.php.tree
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"kind": "CloseParenToken",
"textLength": 1
},
"semicolon": {
"semicolonOrCloseTag": {
"kind": "SemicolonToken",
"textLength": 1
},
Expand Down
24 changes: 0 additions & 24 deletions tests/cases/parser/haltCompiler4.php.diag
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,5 @@
"message": "';' expected.",
"start": 32,
"length": 0
},
{
"kind": 0,
"message": "Unexpected '::'",
"start": 32,
"length": 2
},
{
"kind": 0,
"message": "')' expected.",
"start": 38,
"length": 0
},
{
"kind": 0,
"message": "';' expected.",
"start": 38,
"length": 0
},
{
"kind": 0,
"message": "Unexpected 'InlineHtml'",
"start": 38,
"length": 3
}
]
50 changes: 4 additions & 46 deletions tests/cases/parser/haltCompiler4.php.tree
Original file line number Diff line number Diff line change
Expand Up @@ -27,58 +27,16 @@
"kind": "CloseParenToken",
"textLength": 0
},
"semicolon": {
"semicolonOrCloseTag": {
"error": "MissingToken",
"kind": "SemicolonToken",
"textLength": 0
},
"data": null
}
},
{
"error": "SkippedToken",
"kind": "ColonColonToken",
Copy link
Contributor Author

@TysonAndre TysonAndre Sep 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning in updating the tests I'd added in the previous PR is that HaltCompiler should halt the compiler and not parse any more statements, so I'd treat the snippet like __halt_compiler[();]::foo(); with inline html of ::foo();\n
(this token can't be used in other statements/expressions and is a syntax error)

<?php
// invalid
__halt_compiler::foo();

The MissingToken in the original tree were due to token_get_all() returning at most 3 tokens after T_HALT_COMPILER then returning T_INLINE_HTML for the rest

"textLength": 2
},
{
"ExpressionStatement": {
"expression": {
"CallExpression": {
"callableExpression": {
"QualifiedName": {
"globalSpecifier": null,
"relativeSpecifier": null,
"nameParts": [
{
"kind": "Name",
"textLength": 3
}
]
}
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"argumentExpressionList": null,
"closeParen": {
"error": "MissingToken",
"kind": "CloseParenToken",
"textLength": 0
}
}
},
"semicolon": {
"error": "MissingToken",
"kind": "SemicolonToken",
"textLength": 0
"data": {
"kind": "InlineHtml",
"textLength": 9
}
}
},
{
"error": "SkippedToken",
"kind": "InlineHtml",
"textLength": 3
}
],
"endOfFileToken": {
Expand Down
1 change: 1 addition & 0 deletions tests/cases/parser/haltCompiler5.php.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
48 changes: 48 additions & 0 deletions tests/cases/parser/haltCompiler5.php.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
{
"HaltCompilerStatement": {
"haltCompilerKeyword": {
"kind": "HaltCompilerKeyword",
"textLength": 15
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
},
"semicolon": null,
"data": null
}
},
{
"InlineHtml": {
"scriptSectionEndTag": {
"kind": "ScriptSectionEndTag",
"textLength": 3
},
"text": null,
"scriptSectionStartTag": null
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}
1 change: 1 addition & 0 deletions tests/cases/parser/haltCompiler6.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php /* has implicit ';' and no trailing newline byte */ __halt_compiler() ?>
1 change: 1 addition & 0 deletions tests/cases/parser/haltCompiler6.php.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
41 changes: 41 additions & 0 deletions tests/cases/parser/haltCompiler6.php.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
{
"HaltCompilerStatement": {
"haltCompilerKeyword": {
"kind": "HaltCompilerKeyword",
"textLength": 15
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
},
"semicolonOrCloseTag": {
"kind": "ScriptSectionEndTag",
"textLength": 2
},
"data": null
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}
8 changes: 8 additions & 0 deletions tests/cases/parser/haltCompiler7.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php
// A MissingToken should be generated for the missing `;` or close php tag.
// NOTE: token_get_all() will yield up to 3 tokens after T_HALT_COMPILER,
// no matter what those tokens happen to be, so tolerant-php-parser combines unexpected tokens into T_INLINE_HTML
// so that no subsequent statements get emitted.
// (T_HALT_COMPILER is forbidden in other node types)
// In this invalid AST, treat " + 1;\n" as the inline data after the missing semicolon.
__halt_compiler() + 1;
8 changes: 8 additions & 0 deletions tests/cases/parser/haltCompiler7.php.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"kind": 0,
"message": "';' expected.",
"start": 482,
"length": 0
}
]
45 changes: 45 additions & 0 deletions tests/cases/parser/haltCompiler7.php.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"SourceFileNode": {
"statementList": [
{
"InlineHtml": {
"scriptSectionEndTag": null,
"text": null,
"scriptSectionStartTag": {
"kind": "ScriptSectionStartTag",
"textLength": 6
}
}
},
{
"HaltCompilerStatement": {
"haltCompilerKeyword": {
"kind": "HaltCompilerKeyword",
"textLength": 15
},
"openParen": {
"kind": "OpenParenToken",
"textLength": 1
},
"closeParen": {
"kind": "CloseParenToken",
"textLength": 1
},
"semicolonOrCloseTag": {
"error": "MissingToken",
"kind": "SemicolonToken",
"textLength": 0
},
"data": {
"kind": "InlineHtml",
"textLength": 10
}
}
}
],
"endOfFileToken": {
"kind": "EndOfFileToken",
"textLength": 0
}
}
}
2 changes: 2 additions & 0 deletions tests/cases/parser/haltCompiler8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?php
__halt_compiler
20 changes: 20 additions & 0 deletions tests/cases/parser/haltCompiler8.php.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"kind": 0,
"message": "'(' expected.",
"start": 21,
"length": 0
},
{
"kind": 0,
"message": "')' expected.",
"start": 21,
"length": 0
},
{
"kind": 0,
"message": "';' expected.",
"start": 21,
"length": 0
}
]
Loading