Skip to content

Commit ceef8c3

Browse files
committed
Bug fixes for parsing associativity
This fixes the most common cases for #19 (there may be other edge cases I haven't considered yet) This also fixes incorrect precedence for `<=>`. It has the same precedence as `==` - https://secure.php.net/manual/en/language.operators.precedence.php has correct information for the `<=>` operator.
1 parent a266248 commit ceef8c3

19 files changed

+511
-1
lines changed

src/Parser.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1844,13 +1844,13 @@ private function parseBinaryExpressionOrHigher($precedence, $parentNode) {
18441844
TokenKind::LessThanGreaterThanToken => [17, Associativity::None],
18451845
TokenKind::EqualsEqualsEqualsToken => [17, Associativity::None],
18461846
TokenKind::ExclamationEqualsEqualsToken => [17, Associativity::None],
1847+
TokenKind::LessThanEqualsGreaterThanToken => [17, Associativity::None],
18471848

18481849
// relational-expression (X)
18491850
TokenKind::LessThanToken => [18, Associativity::None],
18501851
TokenKind::GreaterThanToken => [18, Associativity::None],
18511852
TokenKind::LessThanEqualsToken => [18, Associativity::None],
18521853
TokenKind::GreaterThanEqualsToken => [18, Associativity::None],
1853-
TokenKind::LessThanEqualsGreaterThanToken => [18, Associativity::None],
18541854

18551855
// shift-expression (L)
18561856
TokenKind::LessThanLessThanToken => [19, Associativity::Left],
@@ -1883,8 +1883,33 @@ private function getBinaryOperatorPrecedenceAndAssociativity($token) {
18831883
return self::UNKNOWN_PRECEDENCE_AND_ASSOCIATIVITY;
18841884
}
18851885

1886+
/**
1887+
* @internal Do not use outside this class, this may be changed or removed.
1888+
*/
1889+
const KNOWN_ASSIGNMENT_TOKEN_SET = [
1890+
TokenKind::AsteriskAsteriskEqualsToken => true,
1891+
TokenKind::AsteriskEqualsToken => true,
1892+
TokenKind::SlashEqualsToken => true,
1893+
TokenKind::PercentEqualsToken => true,
1894+
TokenKind::PlusEqualsToken => true,
1895+
TokenKind::MinusEqualsToken => true,
1896+
TokenKind::DotEqualsToken => true,
1897+
TokenKind::LessThanLessThanEqualsToken => true,
1898+
TokenKind::GreaterThanGreaterThanEqualsToken => true,
1899+
TokenKind::AmpersandEqualsToken => true,
1900+
TokenKind::CaretEqualsToken => true,
1901+
TokenKind::BarEqualsToken => true,
1902+
// InstanceOf has other remaining issues, but this heuristic is an improvement for many common cases such as `$x && $y = $z`
1903+
];
1904+
18861905
private function makeBinaryExpression($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) {
18871906
$assignmentExpression = $operatorToken->kind === TokenKind::EqualsToken;
1907+
if ($assignmentExpression || \array_key_exists($operatorToken->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) {
1908+
if ($leftOperand instanceof BinaryExpression && !\array_key_exists($leftOperand->operator->kind, self::KNOWN_ASSIGNMENT_TOKEN_SET)) {
1909+
// Handle cases without parenthesis, such as $x ** $y === $z, as $x ** ($y === $z)
1910+
return $this->shiftBinaryOperands($leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode);
1911+
}
1912+
}
18881913
$binaryExpression = $assignmentExpression ? new AssignmentExpression() : new BinaryExpression();
18891914
$binaryExpression->parent = $parentNode;
18901915
$leftOperand->parent = $binaryExpression;
@@ -1898,6 +1923,25 @@ private function makeBinaryExpression($leftOperand, $operatorToken, $byRefToken,
18981923
return $binaryExpression;
18991924
}
19001925

1926+
private function shiftBinaryOperands(BinaryExpression $leftOperand, $operatorToken, $byRefToken, $rightOperand, $parentNode) {
1927+
$inner = $this->makeBinaryExpression(
1928+
$leftOperand->rightOperand,
1929+
$operatorToken,
1930+
$byRefToken,
1931+
$rightOperand,
1932+
$parentNode
1933+
);
1934+
$outer = $this->makeBinaryExpression(
1935+
$leftOperand->leftOperand,
1936+
$leftOperand->operator,
1937+
$leftOperand->byRef ?? null,
1938+
$inner,
1939+
$parentNode
1940+
);
1941+
$inner->parent = $outer;
1942+
return $outer;
1943+
}
1944+
19011945
private function parseDoStatement($parentNode) {
19021946
$doStatement = new DoStatement();
19031947
$doStatement->parent = $parentNode;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
$x && $y = $z;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"SourceFileNode": {
3+
"statementList": [
4+
{
5+
"InlineHtml": {
6+
"scriptSectionEndTag": null,
7+
"text": null,
8+
"scriptSectionStartTag": {
9+
"kind": "ScriptSectionStartTag",
10+
"textLength": 6
11+
}
12+
}
13+
},
14+
{
15+
"ExpressionStatement": {
16+
"expression": {
17+
"BinaryExpression": {
18+
"leftOperand": {
19+
"Variable": {
20+
"dollar": null,
21+
"name": {
22+
"kind": "VariableName",
23+
"textLength": 2
24+
}
25+
}
26+
},
27+
"operator": {
28+
"kind": "AmpersandAmpersandToken",
29+
"textLength": 2
30+
},
31+
"rightOperand": {
32+
"AssignmentExpression": {
33+
"leftOperand": {
34+
"Variable": {
35+
"dollar": null,
36+
"name": {
37+
"kind": "VariableName",
38+
"textLength": 2
39+
}
40+
}
41+
},
42+
"operator": {
43+
"kind": "EqualsToken",
44+
"textLength": 1
45+
},
46+
"byRef": null,
47+
"rightOperand": {
48+
"Variable": {
49+
"dollar": null,
50+
"name": {
51+
"kind": "VariableName",
52+
"textLength": 2
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
},
60+
"semicolon": {
61+
"kind": "SemicolonToken",
62+
"textLength": 1
63+
}
64+
}
65+
}
66+
],
67+
"endOfFileToken": {
68+
"kind": "EndOfFileToken",
69+
"textLength": 0
70+
}
71+
}
72+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
$x = $y || $z;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"SourceFileNode": {
3+
"statementList": [
4+
{
5+
"InlineHtml": {
6+
"scriptSectionEndTag": null,
7+
"text": null,
8+
"scriptSectionStartTag": {
9+
"kind": "ScriptSectionStartTag",
10+
"textLength": 6
11+
}
12+
}
13+
},
14+
{
15+
"ExpressionStatement": {
16+
"expression": {
17+
"AssignmentExpression": {
18+
"leftOperand": {
19+
"Variable": {
20+
"dollar": null,
21+
"name": {
22+
"kind": "VariableName",
23+
"textLength": 2
24+
}
25+
}
26+
},
27+
"operator": {
28+
"kind": "EqualsToken",
29+
"textLength": 1
30+
},
31+
"byRef": null,
32+
"rightOperand": {
33+
"BinaryExpression": {
34+
"leftOperand": {
35+
"Variable": {
36+
"dollar": null,
37+
"name": {
38+
"kind": "VariableName",
39+
"textLength": 2
40+
}
41+
}
42+
},
43+
"operator": {
44+
"kind": "BarBarToken",
45+
"textLength": 2
46+
},
47+
"rightOperand": {
48+
"Variable": {
49+
"dollar": null,
50+
"name": {
51+
"kind": "VariableName",
52+
"textLength": 2
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
},
60+
"semicolon": {
61+
"kind": "SemicolonToken",
62+
"textLength": 1
63+
}
64+
}
65+
}
66+
],
67+
"endOfFileToken": {
68+
"kind": "EndOfFileToken",
69+
"textLength": 0
70+
}
71+
}
72+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
$x && $y += $z;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"SourceFileNode": {
3+
"statementList": [
4+
{
5+
"InlineHtml": {
6+
"scriptSectionEndTag": null,
7+
"text": null,
8+
"scriptSectionStartTag": {
9+
"kind": "ScriptSectionStartTag",
10+
"textLength": 6
11+
}
12+
}
13+
},
14+
{
15+
"ExpressionStatement": {
16+
"expression": {
17+
"BinaryExpression": {
18+
"leftOperand": {
19+
"Variable": {
20+
"dollar": null,
21+
"name": {
22+
"kind": "VariableName",
23+
"textLength": 2
24+
}
25+
}
26+
},
27+
"operator": {
28+
"kind": "AmpersandAmpersandToken",
29+
"textLength": 2
30+
},
31+
"rightOperand": {
32+
"BinaryExpression": {
33+
"leftOperand": {
34+
"Variable": {
35+
"dollar": null,
36+
"name": {
37+
"kind": "VariableName",
38+
"textLength": 2
39+
}
40+
}
41+
},
42+
"operator": {
43+
"kind": "PlusEqualsToken",
44+
"textLength": 2
45+
},
46+
"rightOperand": {
47+
"Variable": {
48+
"dollar": null,
49+
"name": {
50+
"kind": "VariableName",
51+
"textLength": 2
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
},
59+
"semicolon": {
60+
"kind": "SemicolonToken",
61+
"textLength": 1
62+
}
63+
}
64+
}
65+
],
66+
"endOfFileToken": {
67+
"kind": "EndOfFileToken",
68+
"textLength": 0
69+
}
70+
}
71+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
$x += $y && $z;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"SourceFileNode": {
3+
"statementList": [
4+
{
5+
"InlineHtml": {
6+
"scriptSectionEndTag": null,
7+
"text": null,
8+
"scriptSectionStartTag": {
9+
"kind": "ScriptSectionStartTag",
10+
"textLength": 6
11+
}
12+
}
13+
},
14+
{
15+
"ExpressionStatement": {
16+
"expression": {
17+
"BinaryExpression": {
18+
"leftOperand": {
19+
"Variable": {
20+
"dollar": null,
21+
"name": {
22+
"kind": "VariableName",
23+
"textLength": 2
24+
}
25+
}
26+
},
27+
"operator": {
28+
"kind": "PlusEqualsToken",
29+
"textLength": 2
30+
},
31+
"rightOperand": {
32+
"BinaryExpression": {
33+
"leftOperand": {
34+
"Variable": {
35+
"dollar": null,
36+
"name": {
37+
"kind": "VariableName",
38+
"textLength": 2
39+
}
40+
}
41+
},
42+
"operator": {
43+
"kind": "AmpersandAmpersandToken",
44+
"textLength": 2
45+
},
46+
"rightOperand": {
47+
"Variable": {
48+
"dollar": null,
49+
"name": {
50+
"kind": "VariableName",
51+
"textLength": 2
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
},
59+
"semicolon": {
60+
"kind": "SemicolonToken",
61+
"textLength": 1
62+
}
63+
}
64+
}
65+
],
66+
"endOfFileToken": {
67+
"kind": "EndOfFileToken",
68+
"textLength": 0
69+
}
70+
}
71+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
$x < $a <=> $b;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)