Skip to content

Commit

Permalink
Introduce NAMED_ARGUMENT_SEPARATOR_TYPE and related rules. (#302)
Browse files Browse the repository at this point in the history
  • Loading branch information
VincentLanglet authored Aug 30, 2024
1 parent 9402a6a commit 1158bcd
Show file tree
Hide file tree
Showing 25 changed files with 334 additions and 3 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"webmozart/assert": "^1.10"
},
"require-dev": {
"composer/semver": "^3.4.2",
"composer/semver": "^3.2.0",
"dereuromark/composer-prefer-lowest": "^0.1.10",
"ergebnis/composer-normalize": "^2.29",
"friendsofphp/php-cs-fixer": "^3.13.0",
Expand Down
8 changes: 8 additions & 0 deletions docs/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ The `TwigCsFixer\Token\Tokenizer` transform the file into a list of tokens which

The name of a filter function. Like in `{{ foo|filter(bar) }}`.

- **TwigCsFixer\Token\Token::MACRO_NAME_TYPE**:

The name used in the definition of a macro function. Like `foo` in `{% macro foo() %}`.

- **TwigCsFixer\Token\Token::TEST_NAME_TYPE**:

The name of a test function. Like in `{% if foo is test(bar) %}`.
Expand Down Expand Up @@ -129,6 +133,10 @@ The `TwigCsFixer\Token\Tokenizer` transform the file into a list of tokens which

The `#}` delimiter.

- **TwigCsFixer\Token\Token::NAMED_ARGUMENT_SEPARATOR_TYPE**:

The `=` or `:` separator used when using named argument. Like `{{ foo(bar=true, baz: false) }}`.

### Rule

Then, the easiest way to write a custom rule is to implement the `TwigCsFixer\Rules\AbstractRule` class
Expand Down
9 changes: 9 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
- `spaceRatio`: how many spaces replace a tab (default 4).
- `useTab`: indentation must be done with tab (default false).

- **NamedArgumentSeparatorRule**:

Ensures named arguments use `:` syntax instead of `=` (For `twig/twig >= 3.12.0`).

- **NamedArgumentSpacingRule**:

Ensures named arguments use no space around `=` and no space before/one space after `:`.

- **OperatorNameSpacingRule**:

Ensures there is no consecutive spaces inside operator names.
Expand Down Expand Up @@ -146,6 +154,7 @@ new TwigCsFixer\Rules\Whitespace\IndentRule(3);

**Twig**:
- DelimiterSpacingRule
- NamedArgumentSpacingRule
- OperatorNameSpacingRule
- OperatorSpacingRule
- PunctuationSpacingRule
Expand Down
23 changes: 23 additions & 0 deletions src/Environment/StubbedEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ public function __construct(
}
}

/**
* Avoid dependency to composer/semver for twig version comparison.
*/
public static function satisfiesTwigVersion(int $major, int $minor = 0, int $patch = 0): bool
{
$version = explode('.', self::VERSION);

if ($major < $version[0]) {
return true;
}
if ($major > $version[0]) {
return false;
}
if ($minor < $version[1]) {
return true;
}
if ($minor > $version[1]) {
return false;
}

return $version[2] >= $patch;
}

/**
* @param string $name
*/
Expand Down
46 changes: 46 additions & 0 deletions src/Rules/Function/NamedArgumentSeparatorRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace TwigCsFixer\Rules\Function;

use TwigCsFixer\Environment\StubbedEnvironment;
use TwigCsFixer\Rules\AbstractFixableRule;
use TwigCsFixer\Token\Token;
use TwigCsFixer\Token\Tokens;

/**
* Ensures named arguments use `:` syntax instead of `=` (For `twig/twig >= 3.12.0`).
*/
final class NamedArgumentSeparatorRule extends AbstractFixableRule
{
public function __construct()
{
if (!StubbedEnvironment::satisfiesTwigVersion(3, 12)) {
throw new \InvalidArgumentException('Named argument with semi colons requires twig/twig >= 3.12.0');
}
}

protected function process(int $tokenIndex, Tokens $tokens): void
{
$token = $tokens->get($tokenIndex);
if (!$token->isMatching(Token::NAMED_ARGUMENT_SEPARATOR_TYPE)) {
return;
}

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

$fixer = $this->addFixableError(
\sprintf('Named arguments should be declared with the separator "%s".', ':'),
$token
);

if (null === $fixer) {
return;
}

$fixer->replaceToken($tokenIndex, ':');
}
}
38 changes: 38 additions & 0 deletions src/Rules/Function/NamedArgumentSpacingRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace TwigCsFixer\Rules\Function;

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

/**
* Ensures named arguments use no space around `=` and no space before/one space after `:`.
*/
final class NamedArgumentSpacingRule extends AbstractSpacingRule
{
protected function getSpaceBefore(int $tokenIndex, Tokens $tokens): ?int
{
$token = $tokens->get($tokenIndex);
if ($token->isMatching(Token::NAMED_ARGUMENT_SEPARATOR_TYPE)) {
return 0;
}

return null;
}

protected function getSpaceAfter(int $tokenIndex, Tokens $tokens): ?int
{
$token = $tokens->get($tokenIndex);
if ($token->isMatching(Token::NAMED_ARGUMENT_SEPARATOR_TYPE, '=')) {
return 0;
}
if ($token->isMatching(Token::NAMED_ARGUMENT_SEPARATOR_TYPE, ':')) {
return 1;
}

return null;
}
}
2 changes: 2 additions & 0 deletions src/Standard/Twig.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace TwigCsFixer\Standard;

use TwigCsFixer\Rules\Delimiter\DelimiterSpacingRule;
use TwigCsFixer\Rules\Function\NamedArgumentSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorNameSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorSpacingRule;
use TwigCsFixer\Rules\Punctuation\PunctuationSpacingRule;
Expand All @@ -21,6 +22,7 @@ public function getRules(): array
{
return [
new DelimiterSpacingRule(),
new NamedArgumentSpacingRule(),
new OperatorNameSpacingRule(),
new OperatorSpacingRule(),
new PunctuationSpacingRule(),
Expand Down
2 changes: 2 additions & 0 deletions src/Token/Token.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class Token
public const BLOCK_NAME_TYPE = 'BLOCK_NAME_TYPE';
public const FUNCTION_NAME_TYPE = 'FUNCTION_NAME_TYPE';
public const FILTER_NAME_TYPE = 'FILTER_NAME_TYPE';
public const MACRO_NAME_TYPE = 'MACRO_NAME_TYPE';
public const TEST_NAME_TYPE = 'TEST_NAME_TYPE';
public const WHITESPACE_TYPE = 'WHITESPACE_TYPE';
public const TAB_TYPE = 'TAB_TYPE';
Expand All @@ -41,6 +42,7 @@ final class Token
public const COMMENT_TAB_TYPE = 'COMMENT_TAB_TYPE';
public const COMMENT_EOL_TYPE = 'COMMENT_EOL_TYPE';
public const COMMENT_END_TYPE = 'COMMENT_END_TYPE';
public const NAMED_ARGUMENT_SEPARATOR_TYPE = 'NAMED_ARGUMENT_SEPARATOR_TYPE';

public const WHITESPACE_TOKENS = [
self::WHITESPACE_TYPE => self::WHITESPACE_TYPE,
Expand Down
25 changes: 24 additions & 1 deletion src/Token/Tokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,21 @@ private function lexOperator(string $operator): void
} elseif (':' === $operator && $this->isInTernary()) {
$bracket = array_pop($this->bracketsAndTernary);
$this->pushToken(Token::OPERATOR_TYPE, $operator, $bracket);
} elseif ('=' === $operator) {
if (
self::STATE_BLOCK !== $this->getState()
|| 'macro' !== $this->getStateParam('blockName')
) {
$bracket = end($this->bracketsAndTernary);
if (false !== $bracket && '(' === $bracket->getValue()) {
// This is a named argument separator instead
$this->pushToken(Token::NAMED_ARGUMENT_SEPARATOR_TYPE, $operator);

return;
}
}

$this->pushToken(Token::OPERATOR_TYPE, $operator);
} else {
$this->pushToken(Token::OPERATOR_TYPE, $operator);
}
Expand All @@ -601,7 +616,9 @@ private function lexName(string $name): void
$lastNonEmptyToken = $this->lastNonEmptyToken;
Assert::notNull($lastNonEmptyToken, 'A name cannot be the first non empty token.');

if ($lastNonEmptyToken->isMatching(Token::PUNCTUATION_TYPE, '|')) {
if ($lastNonEmptyToken->isMatching(Token::BLOCK_NAME_TYPE, 'macro')) {
$this->pushToken(Token::MACRO_NAME_TYPE, $name);
} elseif ($lastNonEmptyToken->isMatching(Token::PUNCTUATION_TYPE, '|')) {
$this->pushToken(Token::FILTER_NAME_TYPE, $name);
} elseif ($lastNonEmptyToken->isMatching(Token::OPERATOR_TYPE, ['is', 'is not'])) {
$this->pushToken(Token::TEST_NAME_TYPE, $name);
Expand Down Expand Up @@ -681,6 +698,12 @@ private function lexPunctuation(): void

return;
}
if ('(' === $bracket->getValue()) {
// This is a named argument separator instead
$this->pushToken(Token::NAMED_ARGUMENT_SEPARATOR_TYPE, $currentCode);

return;
}

$this->pushToken(Token::PUNCTUATION_TYPE, $currentCode);
} else {
Expand Down
2 changes: 1 addition & 1 deletion tests/Environment/Fixtures/CustomTokenParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public function parse(Token $token): Node
}
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);

return new Node([], [], $token->getLine(), $this->getTag());
return new Node([], [], $token->getLine());
}

public function getTag(): string
Expand Down
22 changes: 22 additions & 0 deletions tests/Environment/StubbedEnvironmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@

final class StubbedEnvironmentTest extends TestCase
{
public function testSatisfiesTwigVersion(): void
{
$version = InstalledVersions::getVersion('twig/twig');
static::assertNotNull($version);
$explodedVersion = explode('.', $version);
$major = (int) $explodedVersion[0];
$minor = (int) $explodedVersion[1];
$patch = (int) $explodedVersion[2];

static::assertTrue(StubbedEnvironment::satisfiesTwigVersion($major, $minor, $patch));
static::assertTrue(StubbedEnvironment::satisfiesTwigVersion($major - 1, $minor, $patch));
static::assertTrue(StubbedEnvironment::satisfiesTwigVersion($major - 1, $minor + 1, $patch + 1));
static::assertTrue(StubbedEnvironment::satisfiesTwigVersion($major, $minor - 1, $patch));
static::assertTrue(StubbedEnvironment::satisfiesTwigVersion($major, $minor - 1, $patch + 1));
static::assertTrue(StubbedEnvironment::satisfiesTwigVersion($major, $minor, $patch - 1));
static::assertFalse(StubbedEnvironment::satisfiesTwigVersion($major + 1, $minor, $patch));
static::assertFalse(StubbedEnvironment::satisfiesTwigVersion($major + 1, $minor - 1, $patch - 1));
static::assertFalse(StubbedEnvironment::satisfiesTwigVersion($major, $minor + 1, $patch));
static::assertFalse(StubbedEnvironment::satisfiesTwigVersion($major, $minor + 1, $patch - 1));
static::assertFalse(StubbedEnvironment::satisfiesTwigVersion($major, $minor, $patch + 1));
}

public function testFilterIsStubbed(): void
{
$env = new StubbedEnvironment();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{ foo(bar:true, baz:42) }}
{% macro foo(bar=true) %}
{% set foo = true %}
24 changes: 24 additions & 0 deletions tests/Rules/Function/NamedArgumentSeparatorRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace TwigCsFixer\Tests\Rules\Function;

use Composer\InstalledVersions;
use Composer\Semver\VersionParser;
use TwigCsFixer\Rules\Function\NamedArgumentSeparatorRule;
use TwigCsFixer\Tests\Rules\AbstractRuleTestCase;

final class NamedArgumentSeparatorRuleTest extends AbstractRuleTestCase
{
public function testRule(): void
{
if (!InstalledVersions::satisfies(new VersionParser(), 'twig/twig', '>=3.12.0')) {
$this->expectException(\InvalidArgumentException::class);
}

$this->checkRule(new NamedArgumentSeparatorRule(), [
'NamedArgumentSeparator.Error:1:11' => 'Named arguments should be declared with the separator ":".',
]);
}
}
3 changes: 3 additions & 0 deletions tests/Rules/Function/NamedArgumentSeparatorRuleTest.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{ foo(bar=true, baz:42) }}
{% macro foo(bar=true) %}
{% set foo = true %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{ foo(bar=true, baz: 42) }}
{% macro foo(bar = true) %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace TwigCsFixer\Tests\Rules\Function\NamedArgumentSpacing;

use TwigCsFixer\Rules\Function\NamedArgumentSpacingRule;
use TwigCsFixer\Tests\Rules\AbstractRuleTestCase;

final class NamedArgumentSpacingRuleTest extends AbstractRuleTestCase
{
public function testRule(): void
{
$this->checkRule(new NamedArgumentSpacingRule(), [
'NamedArgumentSpacing.After:1:12' => 'Expecting 0 whitespace after "="; found 1.',
'NamedArgumentSpacing.Before:1:12' => 'Expecting 0 whitespace before "="; found 1.',
'NamedArgumentSpacing.After:1:24' => 'Expecting 1 whitespace after ":"; found 0.',
'NamedArgumentSpacing.Before:1:24' => 'Expecting 0 whitespace before ":"; found 1.',
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{ foo(bar = true, baz :42) }}
{% macro foo(bar = true) %}
2 changes: 2 additions & 0 deletions tests/Rules/String/HashQuote/HashQuoteRuleTest.fixed.twig
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
{% set float = {'12.3': a} %}

{% set needQuote = {'data-foo': a} %}

{% set namedArgument = foo(bar: true, baz: false) %}
2 changes: 2 additions & 0 deletions tests/Rules/String/HashQuote/HashQuoteRuleTest.twig
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
{% set float = {'12.3': a} %}

{% set needQuote = {'data-foo': a} %}

{% set namedArgument = foo(bar: true, baz: false) %}
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
{% set float = {'12.3': a} %}

{% set needQuote = {'data-foo': a} %}

{% set namedArgument = foo(bar: true, baz: false) %}
2 changes: 2 additions & 0 deletions tests/Standard/SymfonyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use TwigCsFixer\Rules\File\DirectoryNameRule;
use TwigCsFixer\Rules\File\FileExtensionRule;
use TwigCsFixer\Rules\File\FileNameRule;
use TwigCsFixer\Rules\Function\NamedArgumentSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorNameSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorSpacingRule;
use TwigCsFixer\Rules\Punctuation\PunctuationSpacingRule;
Expand All @@ -23,6 +24,7 @@ public function testGetRules(): void

static::assertEquals([
new DelimiterSpacingRule(),
new NamedArgumentSpacingRule(),
new OperatorNameSpacingRule(),
new OperatorSpacingRule(),
new PunctuationSpacingRule(),
Expand Down
2 changes: 2 additions & 0 deletions tests/Standard/TwigCsFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use TwigCsFixer\Rules\Delimiter\BlockNameSpacingRule;
use TwigCsFixer\Rules\Delimiter\DelimiterSpacingRule;
use TwigCsFixer\Rules\Function\IncludeFunctionRule;
use TwigCsFixer\Rules\Function\NamedArgumentSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorNameSpacingRule;
use TwigCsFixer\Rules\Operator\OperatorSpacingRule;
use TwigCsFixer\Rules\Punctuation\PunctuationSpacingRule;
Expand All @@ -29,6 +30,7 @@ public function testGetRules(): void

static::assertEquals([
new DelimiterSpacingRule(),
new NamedArgumentSpacingRule(),
new OperatorNameSpacingRule(),
new OperatorSpacingRule(),
new PunctuationSpacingRule(),
Expand Down
Loading

0 comments on commit 1158bcd

Please sign in to comment.