Skip to content

Commit 511e002

Browse files
Add nullsafe operator support (#429)
1 parent 503eed7 commit 511e002

File tree

7 files changed

+41
-14
lines changed

7 files changed

+41
-14
lines changed

src/Cache/Manager/FileCacheManager.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function __destruct()
4242
/**
4343
* This class is not intended to be serialized, and cannot be deserialized.
4444
*/
45-
public function __sleep(): array
45+
public function __serialize(): array
4646
{
4747
throw new \BadMethodCallException(\sprintf('Cannot serialize %s.', self::class));
4848
}
@@ -51,8 +51,10 @@ public function __sleep(): array
5151
* Prevent attacker executing code by leveraging the __destruct method.
5252
*
5353
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
54+
*
55+
* @param array<mixed> $data
5456
*/
55-
public function __wakeup(): void
57+
public function __unserialize(array $data): void
5658
{
5759
throw new \BadMethodCallException(\sprintf('Cannot unserialize %s.', self::class));
5860
}

src/Rules/Operator/OperatorSpacingRule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ protected function getSpaceBefore(int $tokenIndex, Tokens $tokens): ?int
2525
return $this->isUnary($tokenIndex, $tokens) ? null : 1;
2626
}
2727

28-
if ($token->isMatching(Token::OPERATOR_TYPE, '..')) {
28+
if ($token->isMatching(Token::OPERATOR_TYPE, ['..', '?.'])) {
2929
return 0;
3030
}
3131

@@ -49,7 +49,7 @@ protected function getSpaceAfter(int $tokenIndex, Tokens $tokens): ?int
4949
return $this->isUnary($tokenIndex, $tokens) ? 0 : 1;
5050
}
5151

52-
if ($token->isMatching(Token::OPERATOR_TYPE, '..')) {
52+
if ($token->isMatching(Token::OPERATOR_TYPE, ['..', '?.'])) {
5353
return 0;
5454
}
5555

src/Token/Tokenizer.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ final class Tokenizer implements TokenizerInterface
4343
private const REGEX_DQ_STRING_PART = '/'.self::DQ_STRING_PATTERN.'/As';
4444
private const REGEX_DQ_STRING_DELIM = '/"/A';
4545

46+
private const PUNCTUATIONS = ['(', ')', '[', ']', '{', '}', ':', '.', ',', '|'];
47+
4648
/**
4749
* @var non-empty-string
4850
*/
@@ -331,16 +333,19 @@ private function lexExpression(): void
331333
} elseif (1 === preg_match("/^\r\n?|^\n/", $currentCode.$nextToken, $match)) {
332334
$this->lexEOL($match[0]);
333335
} elseif ('.' === $currentCode && '.' === $nextToken && '.' === $next2Token) {
336+
// NEXT_MAJOR: Should be an OPERATOR_TYPE like in Twig 3.21+
334337
$this->lexSpread();
335338
} elseif ('=' === $currentCode && '>' === $nextToken) {
339+
// NEXT_MAJOR: Should be an OPERATOR_TYPE like in Twig 3.21+
336340
$this->lexArrowFunction();
337341
} elseif (1 === preg_match($this->operatorRegex, $this->code, $match, 0, $this->cursor)) {
338342
$this->lexOperator($match[0]);
339343
} elseif (1 === preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
340344
$this->lexName($match[0]);
341345
} elseif (1 === preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) {
342346
$this->lexNumber($match[0]);
343-
} elseif (\in_array($currentCode, ['(', ')', '[', ']', '{', '}', ':', '.', ',', '|'], true)) {
347+
} elseif (\in_array($currentCode, self::PUNCTUATIONS, true)) {
348+
// NEXT_MAJOR: `.` and `|` should be operator instead like in Twig 3.21+
344349
$this->lexPunctuation();
345350
} elseif (1 === preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
346351
$this->lexString($match[0]);
@@ -783,15 +788,13 @@ private function getOperatorRegex(Environment $env): string
783788
$expressionParsers = [];
784789
// @phpstan-ignore-next-line method.internal
785790
foreach ($env->getExpressionParsers() as $expressionParser) {
786-
$operator = $expressionParser->getName();
787-
// Avoid conflict with ARROW_TYPE and PUNCTUATION_TYPE
788-
if (\in_array($operator, ['=>', '(', '[', '|', '.'], true)) {
789-
continue;
790-
}
791+
foreach ([$expressionParser->getName(), ...$expressionParser->getAliases()] as $name) {
792+
// Avoid conflict with PUNCTUATION_TYPE
793+
if (\in_array($name, self::PUNCTUATIONS, true)) {
794+
continue;
795+
}
791796

792-
$expressionParsers[] = $operator;
793-
foreach ($expressionParser->getAliases() as $alias) {
794-
$expressionParsers[] = $alias;
797+
$expressionParsers[] = $name;
795798
}
796799
}
797800
} else {
@@ -805,7 +808,7 @@ private function getOperatorRegex(Environment $env): string
805808
}
806809

807810
/** @var string[] $operators */
808-
$operators = ['=', '?', '?:', ...$expressionParsers];
811+
$operators = ['=', '?', '?:', '?.', ...$expressionParsers];
809812
$lengthByOperator = [];
810813
foreach ($operators as $operator) {
811814
$lengthByOperator[$operator] = \strlen($operator);

tests/Rules/Operator/OperatorSpacing/OperatorSpacingRuleTest.fixed.twig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,5 @@ Untouch +-/*%==:
5353
not array or b is foo }}
5454

5555
{% types {foo: 'int', bar?: 'string'} %}
56+
57+
{{ foo?.bar }}

tests/Rules/Operator/OperatorSpacing/OperatorSpacingRuleTest.twig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,5 @@ Untouch +-/*%==:
5353
not array or b is foo }}
5454

5555
{% types {foo: 'int', bar?: 'string'} %}
56+
57+
{{ foo?.bar }}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ user?.address?.city }}

tests/Token/Tokenizer/TokenizerTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,23 @@ public static function tokenizeDataProvider(): iterable
870870
8 => Token::EOF_TYPE,
871871
],
872872
];
873+
874+
yield [
875+
__DIR__.'/Fixtures/test19.twig',
876+
[
877+
0 => Token::VAR_START_TYPE,
878+
1 => Token::WHITESPACE_TYPE,
879+
2 => Token::NAME_TYPE,
880+
3 => Token::OPERATOR_TYPE,
881+
4 => Token::NAME_TYPE,
882+
5 => Token::OPERATOR_TYPE,
883+
6 => Token::NAME_TYPE,
884+
7 => Token::WHITESPACE_TYPE,
885+
8 => Token::VAR_END_TYPE,
886+
9 => Token::EOL_TYPE,
887+
10 => Token::EOF_TYPE,
888+
],
889+
];
873890
}
874891

875892
/**

0 commit comments

Comments
 (0)