Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
UPGRADE FROM 2.x to 3.0
=======================
UPGRADE
=======

FROM 3.x to 4.0
---------------

### OperatorSpacingRule

The `OperatorSpacingRule` has speen split in three:
- `TwigCsFixer\Rules\Operator\OperatorSpacingRule`
- `TwigCsFixer\Rules\Operator\TernaryOperatorSpacingRule`
- `TwigCsFixer\Rules\Operator\UnaryOperatorSpacingRule`

### Token

The `Token::OPERATOR_TYPE` has been split in three:
- `Token::OPERATOR_TYPE`
- `Token::TERNARY_OPERATOR_TYPE`
- `Token::UNARY_OPERATOR_TYPE`

FROM 2.x to 3.0
---------------

- The `checkstyle` and `junit` reporter now try to use absolute path rather than relative path.
- In debug mode, the report now contains both the identifier and the message of the error.
Expand Down
13 changes: 11 additions & 2 deletions docs/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,20 @@ The `TwigCsFixer\Token\Tokenizer` transform the file into a list of tokens which

- **TwigCsFixer\Token\Token::OPERATOR_TYPE**:

Any twig operator like `+`, `-`, `and`, `or`, etc. Also include `?` and `:` when used in ternary.
Any twig operator like `+`, `-`, `and`, `or`, etc. Do not include unary and ternary operators.

- **TwigCsFixer\Token\Token::UNARY_OPERATOR_TYPE**:

Any twig unary operator like `not`, `-`, `+`.

- **TwigCsFixer\Token\Token::TERNARY_OPERATOR_TYPE**:

`?` and `:` when used in ternary.

- **TwigCsFixer\Token\Token::PUNCTUATION_TYPE**:

One of the `(`, `)`, `[`, `]`, `{`, `}`, `:`, `.`, `,`, `|` characters. For `:`, only when it's not a ternary.
One of the `(`, `)`, `[`, `]`, `{`, `}`, `:`, `.`, `,`, `|` characters.
For `:`, only when it's not a ternary nor a slice operator.

- **TwigCsFixer\Token\Token::INTERPOLATION_START_TYPE**:

Expand Down
12 changes: 11 additions & 1 deletion docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
- **OperatorSpacingRule**: (Configurable):

Ensures there is no space before and after ':', '..' and '?.', and
there is one space before and after other operators. Options are:
there is one space before and after other non-unary and non-ternary operators. Options are:
- `beforeOverride`: used to override the space before check for specific operators.
- `afterOverride`: used to override the space after check for specific operators.

Expand All @@ -77,6 +77,10 @@
Ensures that strings use single quotes when possible. Options are:
- `skipStringContainingSingleQuote`: ignore double-quoted strings that contains single-quotes (default true).

- **TernaryOperatorSpacingRule**:

Ensures there is one space before and after ternary operators.

- **TrailingCommaMultiLineRule** (Configurable):

Ensures that multi-line arrays, objects and argument lists have a trailing comma. Options are:
Expand All @@ -90,6 +94,10 @@

Ensures that files have no trailing spaces.

- **UnaryOperatorSpacingRule**:

Ensures there is one space after `not` and no space after others unary operators.

### Non-fixable

To use these rules, you have to [allow non-fixable rules](configuration.md#non-fixable-rules) on your ruleset.
Expand Down Expand Up @@ -180,6 +188,8 @@ new TwigCsFixer\Rules\Whitespace\IndentRule(3);
- OperatorNameSpacingRule
- OperatorSpacingRule
- PunctuationSpacingRule
- TernaryOperatorSpacingRule
- UnaryOperatorSpacingRule
- VariableNameRule

### TwigCsFixer
Expand Down
46 changes: 3 additions & 43 deletions src/Rules/Operator/OperatorSpacingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
use TwigCsFixer\Rules\ConfigurableRuleInterface;
use TwigCsFixer\Token\Token;
use TwigCsFixer\Token\Tokens;
use Webmozart\Assert\Assert;

/**
* Ensures there is no space before and after ':', '..' and '?.', and
* there is one space before and after other operators.
* there is one space before and after other non-unary and non-ternary operators.
*/
final class OperatorSpacingRule extends AbstractSpacingRule implements ConfigurableRuleInterface
{
Expand Down Expand Up @@ -46,20 +45,10 @@ protected function getSpaceBefore(int $tokenIndex, Tokens $tokens): ?int
return $this->beforeOverride[$value];
}

if ($token->isMatching(Token::OPERATOR_TYPE, ['not', '-', '+'])) {
return $this->isUnary($tokenIndex, $tokens) ? null : 1;
}

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

if ($token->isMatching(Token::OPERATOR_TYPE, ':')) {
$relatedToken = $token->getRelatedToken();

return null !== $relatedToken && '?' === $relatedToken->getValue() ? 1 : 0;
}

return 1;
}

Expand All @@ -75,39 +64,10 @@ protected function getSpaceAfter(int $tokenIndex, Tokens $tokens): ?int
return $this->afterOverride[$value];
}

if ($token->isMatching(Token::OPERATOR_TYPE, ['-', '+'])) {
return $this->isUnary($tokenIndex, $tokens) ? 0 : 1;
}

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

if ($token->isMatching(Token::OPERATOR_TYPE, ':')) {
$relatedToken = $token->getRelatedToken();

return null !== $relatedToken && '?' === $relatedToken->getValue() ? 1 : 0;
}

return 1;
}

private function isUnary(int $tokenIndex, Tokens $tokens): bool
{
$previous = $tokens->findPrevious(Token::EMPTY_TOKENS, $tokenIndex - 1, exclude: true);
Assert::notFalse($previous, 'An OPERATOR_TYPE cannot be the first non-empty token');

$previousToken = $tokens->get($previous);

return $previousToken->isMatching([
// {{ 1 * -2 }}
Token::OPERATOR_TYPE,
// {{ -2 }}
Token::VAR_START_TYPE,
// {% if -2 ... %}
Token::BLOCK_NAME_TYPE,
])
// {{ 1 + (-2) }}
|| $previousToken->isMatching(Token::PUNCTUATION_TYPE, ['(', '[', ':', ',']);
}
}
35 changes: 35 additions & 0 deletions src/Rules/Operator/TernaryOperatorSpacingRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace TwigCsFixer\Rules\Operator;

use TwigCsFixer\Rules\AbstractSpacingRule;
use TwigCsFixer\Token\Token;
use TwigCsFixer\Token\Tokens;

/**
* Ensures there is one space before and after a ternary operator.
*/
final class TernaryOperatorSpacingRule extends AbstractSpacingRule
{
protected function getSpaceBefore(int $tokenIndex, Tokens $tokens): ?int
{
$token = $tokens->get($tokenIndex);
if (!$token->isMatching(Token::TERNARY_OPERATOR_TYPE)) {
return null;
}

return 1;
}

protected function getSpaceAfter(int $tokenIndex, Tokens $tokens): ?int
{
$token = $tokens->get($tokenIndex);
if (!$token->isMatching(Token::TERNARY_OPERATOR_TYPE)) {
return null;
}

return 1;
}
}
34 changes: 34 additions & 0 deletions src/Rules/Operator/UnaryOperatorSpacingRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace TwigCsFixer\Rules\Operator;

use TwigCsFixer\Rules\AbstractSpacingRule;
use TwigCsFixer\Token\Token;
use TwigCsFixer\Token\Tokens;

/**
* Ensures there is one space after `not` and no space after others unary operators.
*/
final class UnaryOperatorSpacingRule extends AbstractSpacingRule
{
protected function getSpaceBefore(int $tokenIndex, Tokens $tokens): ?int
{
return null;
}

protected function getSpaceAfter(int $tokenIndex, Tokens $tokens): ?int
{
$token = $tokens->get($tokenIndex);
if (!$token->isMatching(Token::UNARY_OPERATOR_TYPE)) {
return null;
}

if ('not' === $token->getValue()) {
return 1;
}

return 0;
}
}
4 changes: 4 additions & 0 deletions src/Standard/Twig.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use TwigCsFixer\Rules\Function\NamedArgumentSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorNameSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorSpacingRule;
use TwigCsFixer\Rules\Operator\TernaryOperatorSpacingRule;
use TwigCsFixer\Rules\Operator\UnaryOperatorSpacingRule;
use TwigCsFixer\Rules\Punctuation\PunctuationSpacingRule;
use TwigCsFixer\Rules\Variable\VariableNameRule;

Expand All @@ -32,6 +34,8 @@ public function getRules(): array
new OperatorNameSpacingRule(),
new OperatorSpacingRule(),
new PunctuationSpacingRule(),
new TernaryOperatorSpacingRule(),
new UnaryOperatorSpacingRule(),
new VariableNameRule(),
];
}
Expand Down
2 changes: 2 additions & 0 deletions src/Token/Token.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ final class Token
public const INLINE_COMMENT_WHITESPACE_TYPE = 'INLINE_COMMENT_WHITESPACE_TYPE';
public const INLINE_COMMENT_TAB_TYPE = 'INLINE_COMMENT_TAB_TYPE';
public const NAMED_ARGUMENT_SEPARATOR_TYPE = 'NAMED_ARGUMENT_SEPARATOR_TYPE';
public const UNARY_OPERATOR_TYPE = 'UNARY_OPERATOR_TYPE';
public const TERNARY_OPERATOR_TYPE = 'TERNARY_OPERATOR_TYPE';

public const WHITESPACE_TOKENS = [
self::WHITESPACE_TYPE => self::WHITESPACE_TYPE,
Expand Down
44 changes: 37 additions & 7 deletions src/Token/Tokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -618,11 +618,7 @@ private function lexArrowFunction(): void
private function lexOperator(string $operator): void
{
if ('?' === $operator) {
$token = $this->pushToken(Token::OPERATOR_TYPE, $operator);
$this->bracketsAndTernary[] = $token;
} elseif (':' === $operator && $this->isInTernary()) {
$bracket = array_pop($this->bracketsAndTernary);
$this->pushToken(Token::OPERATOR_TYPE, $operator, $bracket);
$this->lexTernary($operator);
} elseif ('=' === $operator) {
if (
self::STATE_BLOCK !== $this->getState()
Expand Down Expand Up @@ -650,7 +646,41 @@ private function lexOperator(string $operator): void

$this->pushToken(Token::OPERATOR_TYPE, $operator);
} else {
$this->pushToken(Token::OPERATOR_TYPE, $operator);
$previousToken = $this->lastNonEmptyToken;
Assert::notNull($previousToken, 'An operator cannot be the first non empty token.');

$isUnary = $previousToken->isMatching([
// {{ 1 * -2 }}
Token::OPERATOR_TYPE,
// {{ -2 }}
Token::VAR_START_TYPE,
// {% if -2 ... %}
Token::BLOCK_NAME_TYPE,
// {{ foo ? -1 : -2 }}
Token::TERNARY_OPERATOR_TYPE,
])
// {{ 1 + (-2) }}
|| $previousToken->isMatching(Token::PUNCTUATION_TYPE, ['(', '[', ':', ',']);

$this->pushToken(
$isUnary ? Token::UNARY_OPERATOR_TYPE : Token::OPERATOR_TYPE,
$operator,
);
}
}

private function lexTernary(string $operator): void
{
if ('?' === $operator) {
$token = $this->pushToken(Token::TERNARY_OPERATOR_TYPE, $operator);
$this->bracketsAndTernary[] = $token;
} elseif (':' === $operator) {
$ternary = array_pop($this->bracketsAndTernary);
$this->pushToken(Token::TERNARY_OPERATOR_TYPE, $operator, $ternary);
} else {
// @codeCoverageIgnoreStart
throw new \LogicException(\sprintf('Unexpected ternary operator "%s".', $operator));
// @codeCoverageIgnoreEnd
}
}

Expand Down Expand Up @@ -717,7 +747,7 @@ private function lexPunctuation(): void
if ($this->isInTernary()) {
if (':' === $currentCode) {
// This is a ternary instead
$this->lexOperator($currentCode);
$this->lexTernary($currentCode);

return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
{{ 1 // 2 }}
{{ 1 ** 2 }}
{{ foo ~ bar }}
{{ true ? true : false }}
{{ true ? true : false }}
{{ 1 == 2 }}
{{ not true }}
{{ not true }}
{{ false and true }}
{{ false or true }}
{{ a in array }}
Expand All @@ -20,7 +20,7 @@ Untouch +-/*%==:
{{ 1 ?? 2 }}

{{ 1 + -2 }}
{{ 1 + (-2 + 3) }}
{{ (1) + (-2 + 3) }}
{{ a[-2] }}
{{ { 'foo': -2 } }}
{{ foo.name }}
Expand Down Expand Up @@ -55,3 +55,4 @@ Untouch +-/*%==:
{% types {foo: 'int', bar?: 'string'} %}

{{ foo?.bar }}
{{ foo ? -1 : -2 }}
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,8 @@ public function testRule(): void
'OperatorSpacing.Before:7:5' => 'Expecting 1 whitespace before "**"; found 0.',
'OperatorSpacing.After:8:7' => 'Expecting 1 whitespace after "~"; found 0.',
'OperatorSpacing.Before:8:7' => 'Expecting 1 whitespace before "~"; found 0.',
'OperatorSpacing.After:9:10' => 'Expecting 1 whitespace after "?"; found 2.',
'OperatorSpacing.Before:9:10' => 'Expecting 1 whitespace before "?"; found 2.',
'OperatorSpacing.After:9:19' => 'Expecting 1 whitespace after ":"; found 2.',
'OperatorSpacing.Before:9:19' => 'Expecting 1 whitespace before ":"; found 2.',
'OperatorSpacing.After:10:5' => 'Expecting 1 whitespace after "=="; found 0.',
'OperatorSpacing.Before:10:5' => 'Expecting 1 whitespace before "=="; found 0.',
'OperatorSpacing.After:11:4' => 'Expecting 1 whitespace after "not"; found 3.',
'OperatorSpacing.After:12:11' => 'Expecting 1 whitespace after "and"; found 3.',
'OperatorSpacing.Before:12:11' => 'Expecting 1 whitespace before "and"; found 2.',
'OperatorSpacing.After:13:11' => 'Expecting 1 whitespace after "or"; found 3.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Untouch +-/*%==:
{{ 1??2 }}

{{ 1 +-2 }}
{{ 1 + (-2 + 3) }}
{{ (1) + (-2 + 3) }}
{{ a[-2] }}
{{ { 'foo': -2 } }}
{{ foo.name }}
Expand Down Expand Up @@ -55,3 +55,4 @@ Untouch +-/*%==:
{% types {foo: 'int', bar?: 'string'} %}

{{ foo?.bar }}
{{ foo ? -1 : -2 }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{ true ? true : false }}
{{ true ? true }}

{{ 'test'[1:2] }}
Loading