Skip to content

Commit bf692c1

Browse files
committed
Add PhpdocNoNamedArgumentsTagFixer
1 parent 6058a83 commit bf692c1

File tree

7 files changed

+317
-2
lines changed

7 files changed

+317
-2
lines changed

.dev-tools/phpcs.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<exclude name='Generic.Files.LineLength' />
1616
<exclude name='PSR12.Files.FileHeader.SpacingAfterBlock' />
1717
<exclude name='PSR12.Files.OpenTag.NotAlone' />
18+
<exclude name='Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine' />
1819
<exclude name='Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore' />
1920
</rule>
2021

.php-cs-fixer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
1919
use PhpCsFixerConfig\Rules\LibraryRules;
2020
use PhpCsFixerCustomFixers\Fixer\NoSuperfluousConcatenationFixer;
21+
use PhpCsFixerCustomFixers\Fixer\PhpdocNoNamedArgumentsTagFixer;
2122
use PhpCsFixerCustomFixers\Fixer\PhpdocOnlyAllowedAnnotationsFixer;
2223
use PhpCsFixerCustomFixers\Fixer\PromotedConstructorPropertyFixer;
2324
use PhpCsFixerCustomFixers\Fixer\TypedClassConstantFixer;
@@ -57,6 +58,7 @@
5758
unset($rules[PromotedConstructorPropertyFixer::name()]); // TODO: remove when dropping support to PHP <8.0
5859
unset($rules[TypedClassConstantFixer::name()]); // TODO: remove when dropping support to PHP <8.3
5960
$rules['trailing_comma_in_multiline'] = ['after_heredoc' => true, 'elements' => ['arguments', 'arrays']]; // TODO: remove when dropping support to PHP <8.0
61+
$rules[PhpdocNoNamedArgumentsTagFixer::name()] = ['directory' => __DIR__ . '/src/Analyzer/Analysis/']; // TODO: change to ['directory' => __DIR__ . '/src/']
6062

6163
$rules[PhpdocOnlyAllowedAnnotationsFixer::name()]['elements'][] = 'phpstan-type';
6264

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG for PHP CS Fixer: custom fixers
22

3+
## v3.27.0
4+
- Add PhpdocNoNamedArgumentsTagFixer
5+
36
## v3.26.0
47
- Add TypedClassConstantFixer
58

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Latest stable version](https://img.shields.io/packagist/v/kubawerlos/php-cs-fixer-custom-fixers.svg?label=current%20version)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
66
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
77
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
8-
![Tests](https://img.shields.io/badge/tests-3750-brightgreen.svg)
8+
![Tests](https://img.shields.io/badge/tests-3776-brightgreen.svg)
99
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
1010

1111
[![CI status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml/badge.svg)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml)
@@ -530,6 +530,23 @@ The `@var` annotations must be used correctly in code.
530530
$bar = new Foo();
531531
```
532532

533+
#### PhpdocNoNamedArgumentsTagFixer
534+
There must be `@no-named-arguments` tag in PHPDoc of a class/enum/interface/trait.
535+
Configuration options:
536+
- `description` (`string`): description of the tag; defaults to `''`
537+
- `directory` (`string`): directory in which apply the changes, empty value will result with current working directory (result of `getcwd` call); defaults to `''`
538+
```diff
539+
<?php
540+
+
541+
+/**
542+
+ * @no-named-arguments
543+
+ */
544+
class Foo
545+
{
546+
public function bar(string $s) {}
547+
}
548+
```
549+
533550
#### PhpdocNoSuperfluousParamFixer
534551
There must be no superfluous parameters in PHPDoc.
535552
```diff

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"php": "^7.4 || ^8.0",
1414
"ext-filter": "*",
1515
"ext-tokenizer": "*",
16-
"friendsofphp/php-cs-fixer": "^3.61.1"
16+
"friendsofphp/php-cs-fixer": "^3.61.1",
17+
"symfony/options-resolver": "^5.4 || ^6.4 || ^7.0"
1718
},
1819
"require-dev": {
1920
"phpunit/phpunit": "^9.6.22 || 10.5.45 || ^11.5.7"
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace PhpCsFixerCustomFixers\Fixer;
13+
14+
use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
15+
use PhpCsFixer\Fixer\AbstractPhpUnitFixer;
16+
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
17+
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
18+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
19+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
20+
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
21+
use PhpCsFixer\FixerDefinition\CodeSample;
22+
use PhpCsFixer\FixerDefinition\FixerDefinition;
23+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
24+
use PhpCsFixer\Preg;
25+
use PhpCsFixer\Tokenizer\Token;
26+
use PhpCsFixer\Tokenizer\Tokens;
27+
use PhpCsFixer\WhitespacesFixerConfig;
28+
use Symfony\Component\OptionsResolver\Options;
29+
30+
/**
31+
* @implements ConfigurableFixerInterface<_InputConfig, _Config>
32+
*
33+
* @phpstan-type _InputConfig array{directory?: string, description?: string}
34+
* @phpstan-type _Config array{directory: string, description: string}
35+
*/
36+
final class PhpdocNoNamedArgumentsTagFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
37+
{
38+
private string $description = '';
39+
private string $directory = '';
40+
private WhitespacesFixerConfig $whitespacesConfig;
41+
42+
public function setWhitespacesConfig(WhitespacesFixerConfig $config): void
43+
{
44+
$this->whitespacesConfig = $config;
45+
}
46+
47+
public function getDefinition(): FixerDefinitionInterface
48+
{
49+
return new FixerDefinition(
50+
'There must be `@no-named-arguments` tag in PHPDoc of a class/enum/interface/trait.',
51+
[new CodeSample(<<<'PHP'
52+
<?php
53+
class Foo
54+
{
55+
public function bar(string $s) {}
56+
}
57+
58+
PHP)],
59+
'',
60+
);
61+
}
62+
63+
public function getConfigurationDefinition(): FixerConfigurationResolverInterface
64+
{
65+
$fixerName = $this->getName();
66+
67+
return new FixerConfigurationResolver([
68+
(new FixerOptionBuilder('description', 'description of the tag'))
69+
->setAllowedTypes(['string'])
70+
->setDefault($this->description)
71+
->getOption(),
72+
(new FixerOptionBuilder('directory', 'directory in which apply the changes, empty value will result with current working directory (result of `getcwd` call)'))
73+
->setAllowedTypes(['string'])
74+
->setDefault($this->directory)
75+
->setNormalizer(static function (Options $options, string $value) use ($fixerName): string {
76+
if ($value === '') {
77+
$value = \getcwd();
78+
\assert(\is_string($value));
79+
}
80+
81+
if (!\is_dir($value)) {
82+
throw new InvalidFixerConfigurationException($fixerName, \sprintf('The directory "%s" does not exists.', $value));
83+
}
84+
85+
$value = realpath($value) . \DIRECTORY_SEPARATOR;
86+
87+
return $value;
88+
})
89+
->getOption(),
90+
]);
91+
}
92+
93+
/**
94+
* @param _InputConfig $configuration
95+
*/
96+
public function configure(array $configuration): void
97+
{
98+
/** @var array{directory: string, description: string} $configuration */
99+
$configuration = $this->getConfigurationDefinition()->resolve($configuration);
100+
101+
$this->directory = $configuration['directory'];
102+
$this->description = $configuration['description'];
103+
}
104+
105+
public function getPriority(): int
106+
{
107+
return 0;
108+
}
109+
110+
public function isCandidate(Tokens $tokens): bool
111+
{
112+
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
113+
}
114+
115+
public function isRisky(): bool
116+
{
117+
return false;
118+
}
119+
120+
public function fix(\SplFileInfo $file, Tokens $tokens): void
121+
{
122+
if (!\str_starts_with($file->getRealPath(), $this->directory)) {
123+
return;
124+
}
125+
126+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
127+
if (!$tokens[$index]->isClassy()) {
128+
continue;
129+
}
130+
131+
$prevIndex = $tokens->getPrevMeaningfulToken($index);
132+
if ($tokens[$prevIndex]->isGivenKind(\T_NEW)) {
133+
continue;
134+
}
135+
136+
$this->ensureIsDocBlockWithNoNameArgumentsTag($tokens, $index);
137+
138+
$docBlockIndex = $tokens->getPrevTokenOfKind($index + 2, [[\T_DOC_COMMENT]]);
139+
\assert(\is_int($docBlockIndex));
140+
141+
$content = $tokens[$docBlockIndex]->getContent();
142+
143+
$newContent = Preg::replace('/@no-named-arguments.*(\\R)/', \rtrim('@no-named-arguments ' . $this->description) . '$1', $content);
144+
145+
if ($newContent !== $content) {
146+
$tokens[$docBlockIndex] = new Token([\T_DOC_COMMENT, $newContent]);
147+
}
148+
}
149+
}
150+
151+
private function ensureIsDocBlockWithNoNameArgumentsTag(Tokens $tokens, int $index): void
152+
{
153+
/** @var \Closure(Tokens, int, WhitespacesFixerConfig): void $closure */
154+
static $closure;
155+
156+
if ($closure === null) {
157+
$function = function (Tokens $tokens, int $index, WhitespacesFixerConfig $whitespacesConfig): void {
158+
$object = new class () extends AbstractPhpUnitFixer implements WhitespacesAwareFixerInterface {
159+
public function ensureIsDocBlockWithNoNameArgumentsTag(Tokens $tokens, int $index, WhitespacesFixerConfig $whitespacesConfig): void
160+
{
161+
$this->setWhitespacesConfig($whitespacesConfig);
162+
$this->ensureIsDocBlockWithAnnotation($tokens, $index, 'no-named-arguments', ['no-named-arguments'], []);
163+
}
164+
165+
protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void {}
166+
167+
public function getDefinition(): FixerDefinitionInterface
168+
{
169+
throw new \BadMethodCallException('Not implemented');
170+
}
171+
};
172+
$object->ensureIsDocBlockWithNoNameArgumentsTag($tokens, $index, $whitespacesConfig);
173+
};
174+
$closure = \Closure::bind($function, null, AbstractPhpUnitFixer::class);
175+
}
176+
177+
$closure($tokens, $index, $this->whitespacesConfig);
178+
}
179+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace Tests\Fixer;
13+
14+
use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
15+
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
16+
17+
/**
18+
* @internal
19+
*
20+
* @covers \PhpCsFixerCustomFixers\Fixer\PhpdocNoNamedArgumentsTagFixer
21+
*/
22+
final class PhpdocNoNamedArgumentsTagFixerTest extends AbstractFixerTestCase
23+
{
24+
public function testConfiguration(): void
25+
{
26+
$options = self::getConfigurationOptions();
27+
self::assertArrayHasKey(0, $options);
28+
self::assertSame('description', $options[0]->getName());
29+
self::assertSame('', $options[0]->getDefault());
30+
self::assertArrayHasKey(1, $options);
31+
self::assertSame('directory', $options[1]->getName());
32+
self::assertSame('', $options[1]->getDefault());
33+
34+
$fixer = self::getFixer();
35+
\assert($fixer instanceof ConfigurableFixerInterface);
36+
37+
$invalidDirectory = __DIR__ . '/invalid';
38+
39+
$this->expectException(InvalidFixerConfigurationException::class);
40+
$this->expectExceptionMessage(\sprintf('[%s] The directory "%s" does not exists.', $fixer->getName(), $invalidDirectory));
41+
42+
$fixer->configure(['directory' => $invalidDirectory]);
43+
}
44+
45+
public function testIsRisky(): void
46+
{
47+
self::assertRiskiness(false);
48+
}
49+
50+
/**
51+
* @param array<string, int> $configuration
52+
*
53+
* @dataProvider provideFixCases
54+
*/
55+
public function testFix(string $expected, ?string $input = null, array $configuration = []): void
56+
{
57+
$this->doTest($expected, $input, $configuration);
58+
}
59+
60+
/**
61+
* @return iterable<string, array{0: string, 1?: null|string, 2?: array{path_prefix?: string, description?: string}, 3?: string}>
62+
*/
63+
public static function provideFixCases(): iterable
64+
{
65+
yield 'do not touch anonymous class' => [
66+
<<<'PHP'
67+
<?php
68+
new class () {};
69+
PHP,
70+
];
71+
72+
yield 'do not touch for different prefix' => [
73+
<<<'PHP'
74+
<?php
75+
class Foo {}
76+
PHP,
77+
null,
78+
['directory' => __DIR__ . '/../../tests'],
79+
];
80+
81+
yield 'create PHPDoc comment' => [
82+
<<<'PHP'
83+
<?php
84+
85+
/**
86+
* @no-named-arguments
87+
*/
88+
class Foo {}
89+
PHP,
90+
<<<'PHP'
91+
<?php
92+
class Foo {}
93+
PHP,
94+
];
95+
96+
yield 'change the description' => [
97+
<<<'PHP'
98+
<?php
99+
100+
/**
101+
* @no-named-arguments Some description
102+
*/
103+
class Foo {}
104+
PHP,
105+
<<<'PHP'
106+
<?php
107+
class Foo {}
108+
PHP,
109+
['description' => 'Some description'],
110+
];
111+
}
112+
}

0 commit comments

Comments
 (0)