1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\PhpDocParser\Parser;
4:
5: use PHPStan\PhpDocParser\Ast;
6: use PHPStan\PhpDocParser\Lexer\Lexer;
7: use function str_replace;
8: use function strtolower;
9: use function substr;
10:
11: class ConstExprParser
12: {
13:
14: /** @var bool */
15: private $unescapeStrings;
16:
17: /** @var bool */
18: private $quoteAwareConstExprString;
19:
20: /** @var bool */
21: private $useLinesAttributes;
22:
23: /** @var bool */
24: private $useIndexAttributes;
25:
26: /** @var bool */
27: private $parseDoctrineStrings;
28:
29: /**
30: * @param array{lines?: bool, indexes?: bool} $usedAttributes
31: */
32: public function __construct(
33: bool $unescapeStrings = false,
34: bool $quoteAwareConstExprString = false,
35: array $usedAttributes = []
36: )
37: {
38: $this->unescapeStrings = $unescapeStrings;
39: $this->quoteAwareConstExprString = $quoteAwareConstExprString;
40: $this->useLinesAttributes = $usedAttributes['lines'] ?? false;
41: $this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
42: $this->parseDoctrineStrings = false;
43: }
44:
45: /**
46: * @internal
47: */
48: public function toDoctrine(): self
49: {
50: $self = new self(
51: $this->unescapeStrings,
52: $this->quoteAwareConstExprString,
53: [
54: 'lines' => $this->useLinesAttributes,
55: 'indexes' => $this->useIndexAttributes,
56: ]
57: );
58: $self->parseDoctrineStrings = true;
59: return $self;
60: }
61:
62: public function parse(TokenIterator $tokens, bool $trimStrings = false): Ast\ConstExpr\ConstExprNode
63: {
64: $startLine = $tokens->currentTokenLine();
65: $startIndex = $tokens->currentTokenIndex();
66: if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) {
67: $value = $tokens->currentTokenValue();
68: $tokens->next();
69:
70: return $this->enrichWithAttributes(
71: $tokens,
72: new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)),
73: $startLine,
74: $startIndex
75: );
76: }
77:
78: if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
79: $value = $tokens->currentTokenValue();
80: $tokens->next();
81:
82: return $this->enrichWithAttributes(
83: $tokens,
84: new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)),
85: $startLine,
86: $startIndex
87: );
88: }
89:
90: if ($this->parseDoctrineStrings && $tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
91: $value = $tokens->currentTokenValue();
92: $tokens->next();
93:
94: return $this->enrichWithAttributes(
95: $tokens,
96: new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($value)),
97: $startLine,
98: $startIndex
99: );
100: }
101:
102: if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
103: if ($this->parseDoctrineStrings) {
104: if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
105: throw new ParserException(
106: $tokens->currentTokenValue(),
107: $tokens->currentTokenType(),
108: $tokens->currentTokenOffset(),
109: Lexer::TOKEN_DOUBLE_QUOTED_STRING,
110: null,
111: $tokens->currentTokenLine()
112: );
113: }
114:
115: $value = $tokens->currentTokenValue();
116: $tokens->next();
117:
118: return $this->enrichWithAttributes(
119: $tokens,
120: $this->parseDoctrineString($value, $tokens),
121: $startLine,
122: $startIndex
123: );
124: }
125: $value = $tokens->currentTokenValue();
126: $type = $tokens->currentTokenType();
127: if ($trimStrings) {
128: if ($this->unescapeStrings) {
129: $value = StringUnescaper::unescapeString($value);
130: } else {
131: $value = substr($value, 1, -1);
132: }
133: }
134: $tokens->next();
135:
136: if ($this->quoteAwareConstExprString) {
137: return $this->enrichWithAttributes(
138: $tokens,
139: new Ast\ConstExpr\QuoteAwareConstExprStringNode(
140: $value,
141: $type === Lexer::TOKEN_SINGLE_QUOTED_STRING
142: ? Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED
143: : Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED
144: ),
145: $startLine,
146: $startIndex
147: );
148: }
149:
150: return $this->enrichWithAttributes(
151: $tokens,
152: new Ast\ConstExpr\ConstExprStringNode($value),
153: $startLine,
154: $startIndex
155: );
156:
157: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
158: $identifier = $tokens->currentTokenValue();
159: $tokens->next();
160:
161: switch (strtolower($identifier)) {
162: case 'true':
163: return $this->enrichWithAttributes(
164: $tokens,
165: new Ast\ConstExpr\ConstExprTrueNode(),
166: $startLine,
167: $startIndex
168: );
169: case 'false':
170: return $this->enrichWithAttributes(
171: $tokens,
172: new Ast\ConstExpr\ConstExprFalseNode(),
173: $startLine,
174: $startIndex
175: );
176: case 'null':
177: return $this->enrichWithAttributes(
178: $tokens,
179: new Ast\ConstExpr\ConstExprNullNode(),
180: $startLine,
181: $startIndex
182: );
183: case 'array':
184: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
185: return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex);
186: }
187:
188: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
189: $classConstantName = '';
190: $lastType = null;
191: while (true) {
192: if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) {
193: $classConstantName .= $tokens->currentTokenValue();
194: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
195: $lastType = Lexer::TOKEN_IDENTIFIER;
196:
197: continue;
198: }
199:
200: if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
201: $classConstantName .= '*';
202: $lastType = Lexer::TOKEN_WILDCARD;
203:
204: if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') {
205: break;
206: }
207:
208: continue;
209: }
210:
211: if ($lastType === null) {
212: // trigger parse error if nothing valid was consumed
213: $tokens->consumeTokenType(Lexer::TOKEN_WILDCARD);
214: }
215:
216: break;
217: }
218:
219: return $this->enrichWithAttributes(
220: $tokens,
221: new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName),
222: $startLine,
223: $startIndex
224: );
225:
226: }
227:
228: return $this->enrichWithAttributes(
229: $tokens,
230: new Ast\ConstExpr\ConstFetchNode('', $identifier),
231: $startLine,
232: $startIndex
233: );
234:
235: } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
236: return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex);
237: }
238:
239: throw new ParserException(
240: $tokens->currentTokenValue(),
241: $tokens->currentTokenType(),
242: $tokens->currentTokenOffset(),
243: Lexer::TOKEN_IDENTIFIER,
244: null,
245: $tokens->currentTokenLine()
246: );
247: }
248:
249:
250: private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
251: {
252: $items = [];
253:
254: $startLine = $tokens->currentTokenLine();
255:
256: if (!$tokens->tryConsumeTokenType($endToken)) {
257: do {
258: $items[] = $this->parseArrayItem($tokens);
259: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken));
260: $tokens->consumeTokenType($endToken);
261: }
262:
263: return $this->enrichWithAttributes(
264: $tokens,
265: new Ast\ConstExpr\ConstExprArrayNode($items),
266: $startLine,
267: $startIndex
268: );
269: }
270:
271:
272: /**
273: * This method is supposed to be called with TokenIterator after reading TOKEN_DOUBLE_QUOTED_STRING and shifting
274: * to the next token.
275: */
276: public function parseDoctrineString(string $text, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode
277: {
278: // Because of how Lexer works, a valid Doctrine string
279: // can consist of a sequence of TOKEN_DOUBLE_QUOTED_STRING and TOKEN_DOCTRINE_ANNOTATION_STRING
280: while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
281: $text .= $tokens->currentTokenValue();
282: $tokens->next();
283: }
284:
285: return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text));
286: }
287:
288:
289: private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
290: {
291: $startLine = $tokens->currentTokenLine();
292: $startIndex = $tokens->currentTokenIndex();
293:
294: $expr = $this->parse($tokens);
295:
296: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
297: $key = $expr;
298: $value = $this->parse($tokens);
299:
300: } else {
301: $key = null;
302: $value = $expr;
303: }
304:
305: return $this->enrichWithAttributes(
306: $tokens,
307: new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
308: $startLine,
309: $startIndex
310: );
311: }
312:
313: /**
314: * @template T of Ast\ConstExpr\ConstExprNode
315: * @param T $node
316: * @return T
317: */
318: private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
319: {
320: if ($this->useLinesAttributes) {
321: $node->setAttribute(Ast\Attribute::START_LINE, $startLine);
322: $node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
323: }
324:
325: if ($this->useIndexAttributes) {
326: $node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
327: $node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
328: }
329:
330: return $node;
331: }
332:
333: }
334: