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