Skip to content

Commit e99debf

Browse files
committed
WIP - work on parsing partially invalid ASTs. This will be complicated.
Also, add some missing binary operations not covered by php-ast test suite
1 parent c1b353f commit e99debf

File tree

3 files changed

+155
-79
lines changed

3 files changed

+155
-79
lines changed

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<phpunit bootstrap="src/ASTConverter/Bootstrap.php">
22
<testsuites>
3-
<testsuite name="ConverterTest">
3+
<testsuite name="ConverterTests">
44
<directory>tests</directory>
55
</testsuite>
66
</testsuites>

src/ASTConverter/ASTConverter.php

Lines changed: 153 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,36 @@
33

44
use PhpParser\ParserFactory;
55

6-
/**
7-
* @suppress PhanTypeMismatchProperty https://github.com/etsy/phan/issues/609
8-
* @suppress PhanUndeclaredProperty - docComment really exists.
9-
* NOTE: this may be removed in the future.
10-
*
11-
* Phan was used while developing this. The asserts can be cleaned up in the future.
12-
*/
13-
function astnode(int $kind, int $flags, ?array $children, int $lineno, ?string $docComment = null) : \ast\Node {
14-
$node = new \ast\Node();
15-
$node->kind = $kind;
16-
$node->flags = $flags;
17-
$node->lineno = $lineno;
18-
$node->children = $children;
19-
if (is_string($docComment)) {
20-
$node->docComment = $docComment;
21-
}
22-
return $node;
23-
}
24-
25-
function sl($node) : ?int {
26-
if ($node instanceof \PhpParser\Node) {
27-
return $node->getAttribute('startLine');
28-
}
29-
return null;
30-
}
31-
32-
function el($node) : ?int {
33-
if ($node instanceof \PhpParser\Node) {
34-
return $node->getAttribute('endLine');
35-
}
36-
return null;
37-
}
38-
396
class ASTConverter {
407
// The latest stable version of php-ast.
418
// For something > 40, update the library's release.
429
// For something < 40, there are no releases.
4310
const AST_VERSION = 40;
4411

45-
public static function ast_parse_code_fallback(string $source, int $version) {
12+
private static $should_add_placeholders = false;
13+
14+
public static function set_should_add_placeholders(bool $value) : void {
15+
self::$should_add_placeholders = $value;
16+
}
17+
18+
public static function ast_parse_code_fallback(string $source, int $version, bool $suppressErrors = false, array &$errors = null) {
4619
if ($version !== self::AST_VERSION) {
4720
throw new \InvalidArgumentException(sprintf("Unexpected version: want %d, got %d", self::AST_VERSION, $version));
4821
}
4922
// Aside: this can be implemented as a stub.
50-
$parserNode = self::phpparser_parse($source);
23+
$parserNode = self::phpparser_parse($source, $suppressErrors, $errors);
5124
return self::phpparser_to_phpast($parserNode, $version);
5225
}
5326

54-
public static function phpparser_parse(string $source) {
27+
public static function phpparser_parse(string $source, bool $suppressErrors = false, array &$errors = null) {
5528
$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
29+
$errorHandler = $suppressErrors ? new \PhpParser\ErrorHandler\Collecting() : null;
5630
// $nodeDumper = new PhpParser\NodeDumper();
57-
return $parser->parse($source);
31+
$result = $parser->parse($source, $errorHandler);
32+
if ($suppressErrors) {
33+
$errors = $errorHandler->getErrors();
34+
}
35+
return $result;
5836
}
5937

6038

@@ -84,7 +62,7 @@ private static function _phpparser_stmtlist_to_ast_node(array $parserNodes, ?int
8462
foreach ($childNode as $childNodePart) {
8563
$children[] = $childNodePart;
8664
}
87-
} else {
65+
} else if (!is_null($childNode)) {
8866
$children[] = $childNode;
8967
}
9068
}
@@ -126,8 +104,6 @@ private static function _phpparser_node_to_ast_node($n) {
126104
self::_phpparser_node_to_ast_node($n->expr),
127105
$startLine
128106
);
129-
case 'PhpParser\Node\Expr\AssignOp\ShiftLeft':
130-
return self::_ast_node_assignop(\ast\flags\BINARY_SHIFT_LEFT, $n, $startLine);
131107
case 'PhpParser\Node\Expr\AssignOp\BitwiseAnd':
132108
return self::_ast_node_assignop(\ast\flags\BINARY_BITWISE_AND, $n, $startLine);
133109
case 'PhpParser\Node\Expr\AssignOp\BitwiseOr':
@@ -152,41 +128,41 @@ private static function _phpparser_node_to_ast_node($n) {
152128
return self::_ast_node_assignop(\ast\flags\BINARY_SHIFT_LEFT, $n, $startLine);
153129
case 'PhpParser\Node\Expr\AssignOp\ShiftRight':
154130
return self::_ast_node_assignop(\ast\flags\BINARY_SHIFT_RIGHT, $n, $startLine);
131+
case 'PhpParser\Node\Expr\BinaryOp\BitwiseAnd':
132+
return self::_ast_node_binaryop(\ast\flags\BINARY_BITWISE_AND, $n, $startLine);
133+
case 'PhpParser\Node\Expr\BinaryOp\BitwiseOr':
134+
return self::_ast_node_binaryop(\ast\flags\BINARY_BITWISE_OR, $n, $startLine);
135+
case 'PhpParser\Node\Expr\BinaryOp\BitwiseXor':
136+
return self::_ast_node_binaryop(\ast\flags\BINARY_BITWISE_XOR, $n, $startLine);
137+
case 'PhpParser\Node\Expr\BinaryOp\Concat':
138+
return self::_ast_node_binaryop(\ast\flags\BINARY_CONCAT, $n, $startLine);
155139
case 'PhpParser\Node\Expr\BinaryOp\Coalesce':
156-
return astnode(
157-
\ast\AST_BINARY_OP,
158-
\ast\flags\BINARY_COALESCE,
159-
self::_phpparser_nodes_to_left_right_children($n->left, $n->right),
160-
$startLine
161-
);
140+
return self::_ast_node_binaryop(\ast\flags\BINARY_COALESCE, $n, $startLine);
141+
case 'PhpParser\Node\Expr\BinaryOp\Div':
142+
return self::_ast_node_binaryop(\ast\flags\BINARY_DIV, $n, $startLine);
162143
case 'PhpParser\Node\Expr\BinaryOp\Greater':
163-
return astnode(
164-
\ast\AST_BINARY_OP,
165-
\ast\flags\BINARY_IS_GREATER,
166-
self::_phpparser_nodes_to_left_right_children($n->left, $n->right),
167-
$startLine
168-
);
144+
return self::_ast_node_binaryop(\ast\flags\BINARY_IS_GREATER, $n, $startLine);
169145
case 'PhpParser\Node\Expr\BinaryOp\GreaterOrEqual':
170-
return astnode(
171-
\ast\AST_BINARY_OP,
172-
\ast\flags\BINARY_IS_GREATER_OR_EQUAL,
173-
self::_phpparser_nodes_to_left_right_children($n->left, $n->right),
174-
$startLine
175-
);
146+
return self::_ast_node_binaryop(\ast\flags\BINARY_IS_GREATER_OR_EQUAL, $n, $startLine);
176147
case 'PhpParser\Node\Expr\BinaryOp\LogicalAnd':
177-
return astnode(
178-
\ast\AST_BINARY_OP,
179-
\ast\flags\BINARY_BOOL_AND,
180-
self::_phpparser_nodes_to_left_right_children($n->left, $n->right),
181-
$startLine
182-
);
148+
return self::_ast_node_binaryop(\ast\flags\BINARY_BOOL_AND, $n, $startLine);
183149
case 'PhpParser\Node\Expr\BinaryOp\LogicalOr':
184-
return astnode(
185-
\ast\AST_BINARY_OP,
186-
\ast\flags\BINARY_BOOL_OR,
187-
self::_phpparser_nodes_to_left_right_children($n->left, $n->right),
188-
$startLine
189-
);
150+
return self::_ast_node_binaryop(\ast\flags\BINARY_BOOL_OR, $n, $startLine);
151+
// FIXME: rest of binary operations.
152+
case 'PhpParser\Node\Expr\BinaryOp\Mod':
153+
return self::_ast_node_binaryop(\ast\flags\BINARY_MOD, $n, $startLine);
154+
case 'PhpParser\Node\Expr\BinaryOp\Mul':
155+
return self::_ast_node_binaryop(\ast\flags\BINARY_MUL, $n, $startLine);
156+
case 'PhpParser\Node\Expr\BinaryOp\Minus':
157+
return self::_ast_node_binaryop(\ast\flags\BINARY_SUB, $n, $startLine);
158+
case 'PhpParser\Node\Expr\BinaryOp\Plus':
159+
return self::_ast_node_binaryop(\ast\flags\BINARY_ADD, $n, $startLine);
160+
case 'PhpParser\Node\Expr\BinaryOp\Pow':
161+
return self::_ast_node_binaryop(\ast\flags\BINARY_POW, $n, $startLine);
162+
case 'PhpParser\Node\Expr\BinaryOp\ShiftLeft':
163+
return self::_ast_node_binaryop(\ast\flags\BINARY_SHIFT_LEFT, $n, $startLine);
164+
case 'PhpParser\Node\Expr\BinaryOp\ShiftRight':
165+
return self::_ast_node_binaryop(\ast\flags\BINARY_SHIFT_RIGHT, $n, $startLine);
190166
case 'PhpParser\Node\Expr\Closure':
191167
// TODO: is there a corresponding flag for $n->static? $n->byRef?
192168
return self::_ast_decl_closure(
@@ -200,6 +176,9 @@ private static function _phpparser_node_to_ast_node($n) {
200176
$n->getAttribute('endLine'),
201177
self::_extract_phpdoc_comment($n->getAttribute('comments'))
202178
);
179+
// FIXME: add a test of ClassConstFetch to php-ast
180+
case 'PhpParser\Node\Expr\ClassConstFetch':
181+
return self::_phpparser_classconstfetch_to_ast_classconstfetch($n, $startLine);
203182
case 'PhpParser\Node\Expr\ConstFetch':
204183
return astnode(\ast\AST_CONST, 0, ['name' => self::_phpparser_node_to_ast_node($n->name)], $startLine);
205184
case 'PhpParser\Node\Expr\ErrorSuppress':
@@ -209,6 +188,9 @@ private static function _phpparser_node_to_ast_node($n) {
209188
self::_phpparser_node_to_ast_node($n->expr),
210189
$startLine
211190
);
191+
case 'PhpParser\Node\Expr\Error':
192+
// TODO: handle this.
193+
return null;
212194
case 'PhpParser\Node\Expr\FuncCall':
213195
return self::_ast_node_call(
214196
self::_phpparser_node_to_ast_node($n->name),
@@ -234,11 +216,7 @@ private static function _phpparser_node_to_ast_node($n) {
234216
'args' => self::_phpparser_arg_list_to_ast_arg_list($n->args, $startLine),
235217
], $startLine);
236218
case 'PhpParser\Node\Expr\PropertyFetch':
237-
$name = $n->name;
238-
return astnode(\ast\AST_PROP, 0, [
239-
'expr' => self::_phpparser_node_to_ast_node($n->var),
240-
'prop' => is_object($name) ? self::_phpparser_node_to_ast_node($name) : $name,
241-
], $startLine);
219+
return self::_phpparser_propertyfetch_to_ast_prop($n, $startLine);
242220
case 'PhpParser\Node\Expr\UnaryMinus':
243221
return self::_ast_node_unary_op(\ast\flags\UNARY_MINUS, self::_phpparser_node_to_ast_node($n->expr), $startLine);
244222
case 'PhpParser\Node\Expr\UnaryPlus':
@@ -476,7 +454,14 @@ private static function _ast_node_while($cond, $stmts, int $startLine) : \ast\No
476454
return $node;
477455
}
478456

479-
private static function _ast_node_assign($var, $expr, int $line) : \ast\Node {
457+
private static function _ast_node_assign($var, $expr, int $line) : ?\ast\Node {
458+
if ($expr === null) {
459+
if (self::$should_add_placeholders) {
460+
$expr = '__INCOMPLETE_EXPR__';
461+
} else {
462+
return null;
463+
}
464+
}
480465
$node = new \ast\Node();
481466
$node->kind = \ast\AST_ASSIGN;
482467
$node->flags = 0;
@@ -600,9 +585,17 @@ private static function _ast_node_name_fullyqualified(string $name, int $line) :
600585
return astnode(\ast\AST_NAME, \ast\flags\NAME_FQ, ['name' => $name], $line);
601586
}
602587

603-
private static function _ast_node_variable($expr, int $line) : \ast\Node {
588+
private static function _ast_node_variable($expr, int $line) : ?\ast\Node {
589+
// TODO: 2 different ways to handle an Error. 1. Add a placeholder. 2. remove all of the statements in that tree.
604590
if ($expr instanceof \PhpParser\Node) {
605591
$expr = self::_phpparser_node_to_ast_node($expr);
592+
if ($expr === null) {
593+
if (self::$should_add_placeholders) {
594+
$expr = '__INCOMPLETE_VARIABLE__';
595+
} else {
596+
return null;
597+
}
598+
}
606599
}
607600
$node = new \ast\Node;
608601
$node->kind = \ast\AST_VAR;
@@ -897,6 +890,18 @@ private static function _ast_node_assignop(int $flags, \PhpParser\Node $node, in
897890
);
898891
}
899892

893+
/**
894+
* @suppress PhanUndeclaredProperty
895+
*/
896+
private static function _ast_node_binaryop(int $flags, \PhpParser\Node $n, int $startLine) {
897+
return astnode(
898+
\ast\AST_BINARY_OP,
899+
$flags,
900+
self::_phpparser_nodes_to_left_right_children($n->left, $n->right),
901+
$startLine
902+
);
903+
}
904+
900905
private static function _phpparser_nodes_to_left_right_children($left, $right) : array {
901906
return [
902907
'left' => self::_phpparser_node_to_ast_node($left),
@@ -1046,4 +1051,75 @@ private static function _phpparser_array_to_ast_array(\PhpParser\Node $n, int $s
10461051
}
10471052
return astnode(\ast\AST_ARRAY, \ast\flags\ARRAY_SYNTAX_SHORT, $astItems, $startLine);
10481053
}
1054+
1055+
private static function _phpparser_propertyfetch_to_ast_prop(\PhpParser\Node $n, int $startLine) : ?\ast\Node {
1056+
assert($n instanceof \PhpParser\Node\Expr\PropertyFetch);
1057+
$name = $n->name;
1058+
if (is_object($name)) {
1059+
$name = self::_phpparser_node_to_ast_node($name);
1060+
}
1061+
if ($name === null) {
1062+
if (self::$should_add_placeholders) {
1063+
$name = '__INCOMPLETE_PROPERTY__';
1064+
} else {
1065+
return null;
1066+
}
1067+
}
1068+
return astnode(\ast\AST_PROP, 0, [
1069+
'expr' => self::_phpparser_node_to_ast_node($n->var),
1070+
'prop' => is_object($name) ? : $name,
1071+
], $startLine);
1072+
}
1073+
1074+
private static function _phpparser_classconstfetch_to_ast_classconstfetch(\PhpParser\Node $n, int $startLine) : ?\ast\Node {
1075+
assert($n instanceof \PhpParser\Node\Expr\ClassConstFetch);
1076+
$name = $n->name;
1077+
if (is_object($name)) {
1078+
$name = self::_phpparser_node_to_ast_node($name);
1079+
}
1080+
if ($name === null) {
1081+
if (self::$should_add_placeholders) {
1082+
$name = '__INCOMPLETE_CLASS_CONST__';
1083+
} else {
1084+
return null;
1085+
}
1086+
}
1087+
return astnode(\ast\AST_CLASS_CONST, 0, [
1088+
'class' => self::_phpparser_node_to_ast_node($n->class),
1089+
'const' => $name,
1090+
], $startLine);
1091+
}
1092+
}
1093+
1094+
/**
1095+
* @suppress PhanTypeMismatchProperty https://github.com/etsy/phan/issues/609
1096+
* @suppress PhanUndeclaredProperty - docComment really exists.
1097+
* NOTE: this may be removed in the future.
1098+
*
1099+
* Phan was used while developing this. The asserts can be cleaned up in the future.
1100+
*/
1101+
function astnode(int $kind, int $flags, ?array $children, int $lineno, ?string $docComment = null) : \ast\Node {
1102+
$node = new \ast\Node();
1103+
$node->kind = $kind;
1104+
$node->flags = $flags;
1105+
$node->lineno = $lineno;
1106+
$node->children = $children;
1107+
if (is_string($docComment)) {
1108+
$node->docComment = $docComment;
1109+
}
1110+
return $node;
1111+
}
1112+
1113+
function sl($node) : ?int {
1114+
if ($node instanceof \PhpParser\Node) {
1115+
return $node->getAttribute('startLine');
1116+
}
1117+
return null;
1118+
}
1119+
1120+
function el($node) : ?int {
1121+
if ($node instanceof \PhpParser\Node) {
1122+
return $node->getAttribute('endLine');
1123+
}
1124+
return null;
10491125
}

tests/ASTConverter/ConversionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
require_once __DIR__ . '/../../src/util.php';
66

7-
class TestConversion extends \PHPUnit\Framework\TestCase {
7+
class ConversionTest extends \PHPUnit\Framework\TestCase {
88
protected function _scanSourceDirForPHP(string $sourceDir) : array {
99
$files = scandir($sourceDir);
1010
if (!$files) {

0 commit comments

Comments
 (0)