1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* See class comment |
4
|
|
|
* |
5
|
|
|
* PHP Version 5 |
6
|
|
|
* |
7
|
|
|
* @category Netresearch |
8
|
|
|
* @package Netresearch\Kite |
9
|
|
|
* @subpackage ExpressionLanguage |
10
|
|
|
* @author Christian Opitz <[email protected]> |
11
|
|
|
* @license http://www.netresearch.de Netresearch Copyright |
12
|
|
|
* @link http://www.netresearch.de |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace Netresearch\Kite\ExpressionLanguage; |
16
|
|
|
use Symfony\Component\ExpressionLanguage\SyntaxError; |
17
|
|
|
use Symfony\Component\ExpressionLanguage\Token; |
18
|
|
|
use Symfony\Component\ExpressionLanguage\TokenStream; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Extended expression lexer - handles strings with nested expressions: |
22
|
|
|
* For example: |
23
|
|
|
* "The job {job.name} is {job.getWorkflow().isRunning() ? 'currently' : 'not'} running" |
24
|
|
|
* will be transformed into that: |
25
|
|
|
* "'The job ' ~ (variables.getExpanded('job.name')) ~ ' is ' ~ (variables.getExpanded('job').getWorkflow().isRunning() ? 'currently' : 'not') ~ 'running'" |
26
|
|
|
* |
27
|
|
|
* When there is only one expression, the expression type is not casted to string |
28
|
|
|
* "{{foo: 'bar'}}" will return the specified hash |
29
|
|
|
* |
30
|
|
|
* Also this rewrites objects and properties access in order to use the |
31
|
|
|
* variableRepository getExpanded method - actually this would have been |
32
|
|
|
* a parser or node task but here we can do that in the quickest possible |
33
|
|
|
* way |
34
|
|
|
* |
35
|
|
|
* @category Netresearch |
36
|
|
|
* @package Netresearch\Kite |
37
|
|
|
* @subpackage ExpressionLanguage |
38
|
|
|
* @author Christian Opitz <[email protected]> |
39
|
|
|
* @license http://www.netresearch.de Netresearch Copyright |
40
|
|
|
* @link http://www.netresearch.de |
41
|
|
|
*/ |
42
|
|
|
class Lexer extends \Symfony\Component\ExpressionLanguage\Lexer |
43
|
|
|
{ |
44
|
|
|
/** |
45
|
|
|
* Tokenizes an expression. |
46
|
|
|
* |
47
|
|
|
* @param string $expression The expression to tokenize |
48
|
|
|
* |
49
|
|
|
* @return TokenStream A token stream instance |
50
|
|
|
* |
51
|
|
|
* @throws SyntaxError |
52
|
|
|
*/ |
53
|
|
|
public function tokenize($expression) |
54
|
|
|
{ |
55
|
|
|
$length = strlen($expression); |
56
|
|
|
$stringsAndExpressions = $this->splitIntoStringsAndActualExpressions($expression, $length); |
57
|
|
|
|
58
|
|
|
// Actually tokenize all remaining expressions |
59
|
|
|
$tokens = array(); |
60
|
|
|
$isMultipleExpressions = isset($stringsAndExpressions[1]); |
61
|
|
|
foreach ($stringsAndExpressions as $i => $item) { |
62
|
|
|
if ($isMultipleExpressions && $i > 0) { |
63
|
|
|
$tokens[] = new Token(Token::OPERATOR_TYPE, '~', $item[2]); |
64
|
|
|
} |
65
|
|
|
if ($item[1]) { |
66
|
|
|
if ($isMultipleExpressions) { |
67
|
|
|
$tokens[] = new Token(Token::PUNCTUATION_TYPE, '(', $item[2]); |
68
|
|
|
} |
69
|
|
|
foreach ($this->tokenizeExpression($item[0]) as $token) { |
70
|
|
|
$token->cursor += $item[2]; |
71
|
|
|
$tokens[] = $token; |
72
|
|
|
} |
73
|
|
|
if ($isMultipleExpressions) { |
74
|
|
|
$tokens[] = new Token(Token::PUNCTUATION_TYPE, ')', $item[2]); |
75
|
|
|
} |
76
|
|
|
} else { |
77
|
|
|
$tokens[] = new Token(Token::STRING_TYPE, $item[0], $item[2]); |
78
|
|
|
} |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
$tokens[] = new Token(Token::EOF_TYPE, null, $length); |
82
|
|
|
|
83
|
|
|
return new TokenStream($tokens); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Split a "string with {expression} and string parts" into |
88
|
|
|
* [ |
89
|
|
|
* [ "string with an ", false, 0 ], |
90
|
|
|
* [ "expression", true, 12 ], |
91
|
|
|
* [ " and string parts", false, 23 ] |
92
|
|
|
* ] |
93
|
|
|
* |
94
|
|
|
* @param string $expression The mixed expression string |
95
|
|
|
* @param int $length Length beginning from 0 to analyze |
96
|
|
|
* |
97
|
|
|
* @return array |
98
|
|
|
*/ |
99
|
|
|
protected function splitIntoStringsAndActualExpressions($expression, $length) |
100
|
|
|
{ |
101
|
|
|
$cursor = -1; |
102
|
|
|
$expressions = [[null, false, 0]]; |
103
|
|
|
$current = 0; |
104
|
|
|
$group = 0; |
105
|
|
|
// Split the expression into it's string and actual expression parts |
106
|
|
|
while (++$cursor < $length) { |
107
|
|
|
if ($expression[$cursor] === '{' || $expression[$cursor] === '}') { |
108
|
|
|
if ($cursor && $expression[$cursor - 1] === '\\') { |
|
|
|
|
109
|
|
|
// Escaped parenthesis |
110
|
|
|
// Let parser unescape them after parsing |
111
|
|
|
} else { |
112
|
|
|
$type = $expression[$cursor] === '{' ? 1 : -1; |
113
|
|
|
$group += $type; |
114
|
|
|
if ($group === 1 && $type === 1) { |
115
|
|
|
$expressions[++$current] = [null, true, $cursor]; |
116
|
|
|
continue; |
117
|
|
|
} elseif ($group === 0 && $type === -1) { |
118
|
|
|
$expressions[++$current] = [null, false, $cursor]; |
119
|
|
|
continue; |
120
|
|
|
} elseif ($group < 0) { |
121
|
|
|
throw new \Exception('Unopened and unescaped closing parenthesis'); |
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
$expressions[$current][0] .= $expression[$cursor]; |
126
|
|
|
} |
127
|
|
|
if ($group) { |
128
|
|
|
throw new \Exception('Unclosed and unescaped opening parenthesis'); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
// Filter out empty expressions |
132
|
|
|
foreach ($expressions as $i => $properties) { |
133
|
|
|
if ($properties[0] === null) { |
134
|
|
|
unset($expressions[$i]); |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
return array_values($expressions); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Actually tokenize an expression - at this point object and property access is |
142
|
|
|
* transformed, so that "this.property" will be "get('this.propery')" |
143
|
|
|
* |
144
|
|
|
* Also all function calls (but isset and empty) will be rewritten from |
145
|
|
|
* function(abc) to call("function", abc) to make dynamic functions possible |
146
|
|
|
* |
147
|
|
|
* @param string $expression The expression |
148
|
|
|
* |
149
|
|
|
* @return array |
150
|
|
|
*/ |
151
|
|
|
protected function tokenizeExpression($expression) |
152
|
|
|
{ |
153
|
|
|
$stream = parent::tokenize($expression); |
|
|
|
|
154
|
|
|
$tokens = array(); |
155
|
|
|
$previousWasDot = false; |
156
|
|
|
$ignorePrimaryExpressions = array_flip(['null', 'NULL', 'false', 'FALSE', 'true', 'TRUE']); |
157
|
|
|
while (!$stream->isEOF()) { |
158
|
|
|
/* @var \Symfony\Component\ExpressionLanguage\Token $token */ |
159
|
|
|
$token = $stream->current; |
160
|
|
|
$stream->next(); |
161
|
|
|
if ($token->type === Token::NAME_TYPE && !$previousWasDot) { |
162
|
|
|
if (array_key_exists($token->value, $ignorePrimaryExpressions)) { |
163
|
|
|
$tokens[] = $token; |
164
|
|
|
continue; |
165
|
|
|
} |
166
|
|
|
$isTest = false; |
167
|
|
|
if ($stream->current->test(Token::PUNCTUATION_TYPE, '(')) { |
168
|
|
|
$tokens[] = $token; |
169
|
|
|
$tokens[] = $stream->current; |
170
|
|
|
$stream->next(); |
171
|
|
|
if ($token->value === 'isset' || $token->value === 'empty') { |
172
|
|
|
$isTest = true; |
173
|
|
|
$token = $stream->current; |
174
|
|
|
if ($token->type !== Token::NAME_TYPE) { |
175
|
|
|
throw new SyntaxError('Expected name', $token->cursor); |
176
|
|
|
} |
177
|
|
|
$stream->next(); |
178
|
|
|
} else { |
179
|
|
|
$tokens[] = new Token(Token::STRING_TYPE, $token->value, $token->cursor); |
180
|
|
|
$token->value = 'call'; |
181
|
|
|
if (!$stream->current->test(Token::PUNCTUATION_TYPE, ')')) { |
182
|
|
|
$tokens[] = new Token(Token::PUNCTUATION_TYPE, ',', $token->cursor); |
183
|
|
|
} |
184
|
|
|
continue; |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
$names = array($token->value); |
188
|
|
|
$isFunctionCall = false; |
189
|
|
|
while (!$stream->isEOF() && $stream->current->type === Token::PUNCTUATION_TYPE && $stream->current->value === '.') { |
190
|
|
|
$stream->next(); |
191
|
|
|
$nameToken = $stream->current; |
192
|
|
|
$stream->next(); |
193
|
|
|
// Operators like "not" and "matches" are valid method or property names - others not |
194
|
|
|
if ($nameToken->type !== Token::NAME_TYPE |
195
|
|
|
&& ($nameToken->type !== Token::OPERATOR_TYPE || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $nameToken->value)) |
196
|
|
|
) { |
197
|
|
|
throw new SyntaxError('Expected name', $nameToken->cursor); |
198
|
|
|
} |
199
|
|
|
if ($stream->current->test(Token::PUNCTUATION_TYPE, '(')) { |
200
|
|
|
$isFunctionCall = true; |
201
|
|
|
} else { |
202
|
|
|
$names[] = $nameToken->value; |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
if ($isTest) { |
206
|
|
|
if ($isFunctionCall) { |
207
|
|
|
throw new SyntaxError('Can\'t use function return value in write context', $stream->current->cursor); |
208
|
|
|
} |
209
|
|
|
if (!$stream->current->test(Token::PUNCTUATION_TYPE, ')')) { |
210
|
|
|
throw new SyntaxError('Expected )', $stream->current->cursor); |
211
|
|
|
} |
212
|
|
|
$tokens[] = new Token(Token::STRING_TYPE, implode('.', $names), $token->cursor); |
213
|
|
|
} else { |
214
|
|
|
$tokens[] = new Token(Token::NAME_TYPE, 'get', $token->cursor); |
215
|
|
|
$tokens[] = new Token(Token::PUNCTUATION_TYPE, '(', $token->cursor); |
216
|
|
|
$tokens[] = new Token(Token::STRING_TYPE, implode('.', $names), $token->cursor); |
217
|
|
|
$tokens[] = new Token(Token::PUNCTUATION_TYPE, ')', $token->cursor); |
218
|
|
|
if ($isFunctionCall) { |
219
|
|
|
$tokens[] = new Token(Token::PUNCTUATION_TYPE, '.', $nameToken->cursor - strlen($nameToken->value)); |
|
|
|
|
220
|
|
|
$tokens[] = $nameToken; |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
} else { |
224
|
|
|
$tokens[] = $token; |
225
|
|
|
$previousWasDot = $token->test(Token::PUNCTUATION_TYPE, '.'); |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
return $tokens; |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
?> |
|
|
|
|
234
|
|
|
|
This check looks for the bodies of
if
statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.These
if
bodies can be removed. If you have an empty if but statements in theelse
branch, consider inverting the condition.could be turned into
This is much more concise to read.