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:
209: private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode
210: {
211: $items = [];
212:
213: $startLine = $tokens->currentTokenLine();
214:
215: if (!$tokens->tryConsumeTokenType($endToken)) {
216: do {
217: $items[] = $this->parseArrayItem($tokens);
218: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken));
219: $tokens->consumeTokenType($endToken);
220: }
221:
222: return $this->enrichWithAttributes(
223: $tokens,
224: new Ast\ConstExpr\ConstExprArrayNode($items),
225: $startLine,
226: $startIndex,
227: );
228: }
229:
230:
231: /**
232: * This method is supposed to be called with TokenIterator after reading TOKEN_DOUBLE_QUOTED_STRING and shifting
233: * to the next token.
234: */
235: public function parseDoctrineString(string $text, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode
236: {
237: // Because of how Lexer works, a valid Doctrine string
238: // can consist of a sequence of TOKEN_DOUBLE_QUOTED_STRING and TOKEN_DOCTRINE_ANNOTATION_STRING
239: while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) {
240: $text .= $tokens->currentTokenValue();
241: $tokens->next();
242: }
243:
244: return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text));
245: }
246:
247:
248: private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode
249: {
250: $startLine = $tokens->currentTokenLine();
251: $startIndex = $tokens->currentTokenIndex();
252:
253: $expr = $this->parse($tokens);
254:
255: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) {
256: $key = $expr;
257: $value = $this->parse($tokens);
258:
259: } else {
260: $key = null;
261: $value = $expr;
262: }
263:
264: return $this->enrichWithAttributes(
265: $tokens,
266: new Ast\ConstExpr\ConstExprArrayItemNode($key, $value),
267: $startLine,
268: $startIndex,
269: );
270: }
271:
272: /**
273: * @template T of Ast\ConstExpr\ConstExprNode
274: * @param T $node
275: * @return T
276: */
277: private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode
278: {
279: if ($this->config->useLinesAttributes) {
280: $node->setAttribute(Ast\Attribute::START_LINE, $startLine);
281: $node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
282: }
283:
284: if ($this->config->useIndexAttributes) {
285: $node->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
286: $node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
287: }
288:
289: return $node;
290: }
291:
292: }
293: