Repository: microsoft/tolerant-php-parser (php)

Issue:
Support `__halt_compiler` as a member of the top level statements
This would allow validation test suites to pass again

- On the top level scope, emit a new HaltCompilerStatement with haltCompilerToken, openParen, closeParen, semicolonToken, and data of type T_INLINE_HTML
- It's a compile error on an inner statement, so not really important to parse it, but that would be with a MissingToken for `data` if it was parsed in an inner statement.

```
// Zend/zend_language_parser.y
top_statement:
//  ....
	|	T_HALT_COMPILER '(' ')' ';'
			{ $$ = zend_ast_create(ZEND_AST_HALT_COMPILER,
			      zend_ast_create_zval_from_long(zend_get_scanned_file_offset()));
			  zend_stop_lexing(); }
```

```
php > var_export(token_get_all('<?php echo 2; __halt_compiler(); ?><?php fake data'));
array (
  0 => 
  array (
    0 => 382,
    1 => '<?php ',
    2 => 1,
  ),
  1 => 
  array (
    0 => 324,
    1 => 'echo',
    2 => 1,
  ),
  2 => 
  array (
    0 => 385,
    1 => ' ',
    2 => 1,
  ),
  3 => 
  array (
    0 => 309,
    1 => '2',
    2 => 1,
  ),
  4 => ';',
  5 => 
  array (
    0 => 385,
    1 => ' ',
    2 => 1,
  ),
  6 => 
  array (
    0 => 363,
    1 => '__halt_compiler',
    2 => 1,
  ),
  7 => '(',
  8 => ')',
  9 => ';',
  10 => 
  array (
    0 => 313,
    1 => ' ?><?php fake data',
    2 => 1,
  ),
)
php > echo token_name(313);
T_INLINE_HTML
```

Pull request description:
Support `__halt_compiler` statement
https://www.php.net/manual/en/function.halt-compiler.php can be used to
embed data in php scripts from the outermost scope.
In inner scope, it is parsed in php-src only for the sake of error
messages about it needing to be in the outermost scope, so treat it as
an unexpected token in other contexts.

(In an inner `{...}` scope, the call to `token_get_all()` will still
stop after `__halt_compiler();`, returning T_INLINE_HTML,
so the remaining statements can't be parsed, anyway)

Additionally, `__halt_compiler` can't be used as a name, a member name
(e.g. method name), etc, so calling it `TokenKind::Name` seems
incorrect. (`__COMPILER_HALT_OFFSET__` is already properly a Name)

Closes #381

Patch:
diff --git a/.travis.yml b/.travis.yml
index 9088987..fb43613 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,8 +19,6 @@ matrix:
     - php: 7.2
       env: STATIC_ANALYSIS=true
   fast_finish: true
-  allow_failures:
-    - env: VALIDATION=true
 
 cache:
   directories:
diff --git a/src/Node/Statement/HaltCompilerStatement.php b/src/Node/Statement/HaltCompilerStatement.php
new file mode 100644
index 0000000..009009f
--- /dev/null
+++ b/src/Node/Statement/HaltCompilerStatement.php
@@ -0,0 +1,36 @@
+<?php
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+namespace Microsoft\PhpParser\Node\Statement;
+
+use Microsoft\PhpParser\Node\Expression;
+use Microsoft\PhpParser\Token;
+
+class HaltCompilerStatement extends Expression {
+
+    /** @var Token */
+    public $haltCompilerKeyword;
+
+    /** @var Token */
+    public $openParen;
+
+    /** @var Token */
+    public $closeParen;
+
+    /** @var Token */
+    public $semicolon;
+
+    /** @var Token */
+    public $data;
+
+    const CHILD_NAMES = [
+        'haltCompilerKeyword',
+        'openParen',
+        'closeParen',
+        'semicolon',
+        'data',
+    ];
+}
diff --git a/src/Parser.php b/src/Parser.php
index b2bcd6a..58dc058 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -97,6 +97,7 @@ use Microsoft\PhpParser\Node\Statement\{
     ForStatement,
     FunctionDeclaration,
     GotoStatement,
+    HaltCompilerStatement,
     IfStatementNode,
     InlineHtml,
     InterfaceDeclaration,
@@ -615,6 +616,15 @@ class Parser {
 
                 case TokenKind::UnsetKeyword:
                     return $this->parseUnsetStatement($parentNode);
+
+                case TokenKind::HaltCompilerKeyword:
+                    if ($parentNode instanceof SourceFileNode) {
+                        return $this->parseHaltCompilerStatement($parentNode);
+                    }
+                    // __halt_compiler is a fatal compile error anywhere other than the top level.
+                    // It won't be seen elsewhere in other programs - warn about the token being unexpected.
+                    $this->advanceToken();
+                    return new SkippedToken($token);
             }
 
             $expressionStatement = new ExpressionStatement();
@@ -1141,6 +1151,9 @@ class Parser {
 
             // attributes
             case TokenKind::AttributeToken:
+
+            // __halt_compiler
+            case TokenKind::HaltCompilerKeyword:
                 return true;
 
             default:
@@ -2844,6 +2857,18 @@ class Parser {
         return $unsetStatement;
     }
 
+    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;
+    }
+
     private function parseArrayCreationExpression($parentNode) {
         $arrayExpression = new ArrayCreationExpression();
         $arrayExpression->parent = $parentNode;
diff --git a/src/PhpTokenizer.php b/src/PhpTokenizer.php
index be8cd71..08c5788 100644
--- a/src/PhpTokenizer.php
+++ b/src/PhpTokenizer.php
@@ -228,7 +228,6 @@ class PhpTokenizer implements TokenStreamProviderInterface {
         T_DIR => TokenKind::Name,
         T_FILE => TokenKind::Name,
         T_FUNC_C => TokenKind::Name,
-        T_HALT_COMPILER => TokenKind::Name,
         T_METHOD_C => TokenKind::Name,
         T_NS_C => TokenKind::Name,
         T_TRAIT_C => TokenKind::Name,
@@ -274,6 +273,7 @@ class PhpTokenizer implements TokenStreamProviderInterface {
         T_FUNCTION => TokenKind::FunctionKeyword,
         T_GLOBAL => TokenKind::GlobalKeyword,
         T_GOTO => TokenKind::GotoKeyword,
+        T_HALT_COMPILER => TokenKind::HaltCompilerKeyword,
         T_IF => TokenKind::IfKeyword,
         T_IMPLEMENTS => TokenKind::ImplementsKeyword,
         T_INCLUDE => TokenKind::IncludeKeyword,
diff --git a/src/TokenKind.php b/src/TokenKind.php
index 49caca9..b4d59c2 100644
--- a/src/TokenKind.php
+++ b/src/TokenKind.php
@@ -91,6 +91,7 @@ class TokenKind {
     const IterableKeyword = self::IterableReservedWord;
     const EnumKeyword = 171;
     const ReadonlyKeyword = 172;
+    const HaltCompilerKeyword = 173;
 
     const OpenBracketToken = 201;
     const CloseBracketToken = 202;


Test patch:
diff --git a/tests/cases/parser/haltCompiler1.php b/tests/cases/parser/haltCompiler1.php
new file mode 100644
index 0000000..fb03434
--- /dev/null
+++ b/tests/cases/parser/haltCompiler1.php
@@ -0,0 +1,2 @@
+<?php
+__halt_compiler();
diff --git a/tests/cases/parser/haltCompiler1.php.diag b/tests/cases/parser/haltCompiler1.php.diag
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/tests/cases/parser/haltCompiler1.php.diag
@@ -0,0 +1,1 @@
+[]
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler1.php.tree b/tests/cases/parser/haltCompiler1.php.tree
new file mode 100644
index 0000000..767be1b
--- /dev/null
+++ b/tests/cases/parser/haltCompiler1.php.tree
@@ -0,0 +1,44 @@
+{
+    "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": {
+                        "kind": "SemicolonToken",
+                        "textLength": 1
+                    },
+                    "data": {
+                        "kind": "InlineHtml",
+                        "textLength": 1
+                    }
+                }
+            }
+        ],
+        "endOfFileToken": {
+            "kind": "EndOfFileToken",
+            "textLength": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler2.php b/tests/cases/parser/haltCompiler2.php
new file mode 100644
index 0000000..85c1c43
--- /dev/null
+++ b/tests/cases/parser/haltCompiler2.php
@@ -0,0 +1,4 @@
+<?php
+if (true) {
+    __halt_compiler();
+}
diff --git a/tests/cases/parser/haltCompiler2.php.diag b/tests/cases/parser/haltCompiler2.php.diag
new file mode 100644
index 0000000..a36acb1
--- /dev/null
+++ b/tests/cases/parser/haltCompiler2.php.diag
@@ -0,0 +1,26 @@
+[
+    {
+        "kind": 0,
+        "message": "Unexpected 'HaltCompilerKeyword'",
+        "start": 22,
+        "length": 15
+    },
+    {
+        "kind": 0,
+        "message": "'Expression' expected.",
+        "start": 38,
+        "length": 0
+    },
+    {
+        "kind": 0,
+        "message": "Unexpected 'InlineHtml'",
+        "start": 40,
+        "length": 3
+    },
+    {
+        "kind": 0,
+        "message": "'}' expected.",
+        "start": 43,
+        "length": 0
+    }
+]
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler2.php.tree b/tests/cases/parser/haltCompiler2.php.tree
new file mode 100644
index 0000000..9228dac
--- /dev/null
+++ b/tests/cases/parser/haltCompiler2.php.tree
@@ -0,0 +1,99 @@
+{
+    "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": "TrueReservedWord",
+                                "textLength": 4
+                            }
+                        }
+                    },
+                    "closeParen": {
+                        "kind": "CloseParenToken",
+                        "textLength": 1
+                    },
+                    "colon": null,
+                    "statements": {
+                        "CompoundStatementNode": {
+                            "openBrace": {
+                                "kind": "OpenBraceToken",
+                                "textLength": 1
+                            },
+                            "statements": [
+                                {
+                                    "error": "SkippedToken",
+                                    "kind": "HaltCompilerKeyword",
+                                    "textLength": 15
+                                },
+                                {
+                                    "ExpressionStatement": {
+                                        "expression": {
+                                            "ParenthesizedExpression": {
+                                                "openParen": {
+                                                    "kind": "OpenParenToken",
+                                                    "textLength": 1
+                                                },
+                                                "expression": {
+                                                    "error": "MissingToken",
+                                                    "kind": "Expression",
+                                                    "textLength": 0
+                                                },
+                                                "closeParen": {
+                                                    "kind": "CloseParenToken",
+                                                    "textLength": 1
+                                                }
+                                            }
+                                        },
+                                        "semicolon": {
+                                            "kind": "SemicolonToken",
+                                            "textLength": 1
+                                        }
+                                    }
+                                },
+                                {
+                                    "error": "SkippedToken",
+                                    "kind": "InlineHtml",
+                                    "textLength": 3
+                                }
+                            ],
+                            "closeBrace": {
+                                "error": "MissingToken",
+                                "kind": "CloseBraceToken",
+                                "textLength": 0
+                            }
+                        }
+                    },
+                    "elseIfClauses": [],
+                    "elseClause": null,
+                    "endifKeyword": null,
+                    "semicolon": null
+                }
+            }
+        ],
+        "endOfFileToken": {
+            "kind": "EndOfFileToken",
+            "textLength": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler3.php b/tests/cases/parser/haltCompiler3.php
new file mode 100644
index 0000000..5b967ce
--- /dev/null
+++ b/tests/cases/parser/haltCompiler3.php
@@ -0,0 +1,3 @@
+<?php
+// Only allowed in compound statements.
+if (true) __halt_compiler();
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler3.php.diag b/tests/cases/parser/haltCompiler3.php.diag
new file mode 100644
index 0000000..05b037b
--- /dev/null
+++ b/tests/cases/parser/haltCompiler3.php.diag
@@ -0,0 +1,14 @@
+[
+    {
+        "kind": 0,
+        "message": "Unexpected 'HaltCompilerKeyword'",
+        "start": 56,
+        "length": 15
+    },
+    {
+        "kind": 0,
+        "message": "'Expression' expected.",
+        "start": 72,
+        "length": 0
+    }
+]
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler3.php.tree b/tests/cases/parser/haltCompiler3.php.tree
new file mode 100644
index 0000000..ab8d28e
--- /dev/null
+++ b/tests/cases/parser/haltCompiler3.php.tree
@@ -0,0 +1,79 @@
+{
+    "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": "TrueReservedWord",
+                                "textLength": 4
+                            }
+                        }
+                    },
+                    "closeParen": {
+                        "kind": "CloseParenToken",
+                        "textLength": 1
+                    },
+                    "colon": null,
+                    "statements": {
+                        "error": "SkippedToken",
+                        "kind": "HaltCompilerKeyword",
+                        "textLength": 15
+                    },
+                    "elseIfClauses": [],
+                    "elseClause": null,
+                    "endifKeyword": null,
+                    "semicolon": null
+                }
+            },
+            {
+                "ExpressionStatement": {
+                    "expression": {
+                        "ParenthesizedExpression": {
+                            "openParen": {
+                                "kind": "OpenParenToken",
+                                "textLength": 1
+                            },
+                            "expression": {
+                                "error": "MissingToken",
+                                "kind": "Expression",
+                                "textLength": 0
+                            },
+                            "closeParen": {
+                                "kind": "CloseParenToken",
+                                "textLength": 1
+                            }
+                        }
+                    },
+                    "semicolon": {
+                        "kind": "SemicolonToken",
+                        "textLength": 1
+                    }
+                }
+            }
+        ],
+        "endOfFileToken": {
+            "kind": "EndOfFileToken",
+            "textLength": 0
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler4.php b/tests/cases/parser/haltCompiler4.php
new file mode 100644
index 0000000..ebdb7a5
--- /dev/null
+++ b/tests/cases/parser/haltCompiler4.php
@@ -0,0 +1,3 @@
+<?php
+// invalid
+__halt_compiler::foo();
diff --git a/tests/cases/parser/haltCompiler4.php.diag b/tests/cases/parser/haltCompiler4.php.diag
new file mode 100644
index 0000000..35a59b7
--- /dev/null
+++ b/tests/cases/parser/haltCompiler4.php.diag
@@ -0,0 +1,44 @@
+[
+    {
+        "kind": 0,
+        "message": "'(' expected.",
+        "start": 32,
+        "length": 0
+    },
+    {
+        "kind": 0,
+        "message": "')' expected.",
+        "start": 32,
+        "length": 0
+    },
+    {
+        "kind": 0,
+        "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
+    }
+]
\ No newline at end of file
diff --git a/tests/cases/parser/haltCompiler4.php.tree b/tests/cases/parser/haltCompiler4.php.tree
new file mode 100644
index 0000000..fe93086
--- /dev/null
+++ b/tests/cases/parser/haltCompiler4.php.tree
@@ -0,0 +1,89 @@
+{
+    "SourceFileNode": {
+        "statementList": [
+            {
+                "InlineHtml": {
+                    "scriptSectionEndTag": null,
+                    "text": null,
+                    "scriptSectionStartTag": {
+                        "kind": "ScriptSectionStartTag",
+                        "textLength": 6
+                    }
+                }
+            },
+            {
+                "HaltCompilerStatement": {
+                    "haltCompilerKeyword": {
+                        "kind": "HaltCompilerKeyword",
+                        "textLength": 15
+                    },
+                    "openParen": {
+                        "error": "MissingToken",
+                        "kind": "OpenParenToken",
+                        "textLength": 0
+                    },
+                    "closeParen": {
+                        "error": "MissingToken",
+                        "kind": "CloseParenToken",
+                        "textLength": 0
+                    },
+                    "semicolon": {
+                        "error": "MissingToken",
+                        "kind": "SemicolonToken",
+                        "textLength": 0
+                    },
+                    "data": null
+                }
+            },
+            {
+                "error": "SkippedToken",
+                "kind": "ColonColonToken",
+                "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
+                    }
+                }
+            },
+            {
+                "error": "SkippedToken",
+                "kind": "InlineHtml",
+                "textLength": 3
+            }
+        ],
+        "endOfFileToken": {
+            "kind": "EndOfFileToken",
+            "textLength": 0
+        }
+    }
+}
\ No newline at end of file