Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support external rulesets #6083

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d43868b
Introduce registering external RuleSets
niklam Oct 29, 2021
a395832
Add method registerCustomRules() to Config, ConfigInterface
niklam Oct 29, 2021
386094b
Add quick documentation for external rule sets
niklam Oct 29, 2021
ea64127
Add missing method to .php-cs-fixer.custom.php
niklam Oct 29, 2021
188760b
Use Preg:: instead of direct usage of preg_match
niklam Oct 29, 2021
e5cd9a6
Fix code style for SampleRulesOk, SampleRulesBad
niklam Oct 29, 2021
3661507
Fix error message for missing extending of AbstractRuleSetDescription
niklam Oct 29, 2021
5189005
Make test style consistent for new tests
niklam Oct 29, 2021
eba01ae
Change registerCustomRuleSets to accept array
niklam Oct 29, 2021
95708f7
Fix tests, sort RuleSets after registering custom RuleSet
niklam Oct 29, 2021
6d7e364
Requires RuleSetDescriptionInterface instead of AbstractRuleSetDescri…
niklam Oct 29, 2021
1aa7fef
Introduce private RuleSets::sortSetDefinitions()
niklam Jun 4, 2023
d4f9804
Fixes to code-style
niklam Jun 4, 2023
a5d8be8
Fix code-style for RuleSets.php
niklam Jun 4, 2023
91dde6e
Changes per review comments
niklam Jun 7, 2023
b910c3f
Use class_implements() instead of instanceof in checking custom rule …
niklam Jun 8, 2023
3347e6a
Fix rule sets tests providers to contain named test cases
niklam Jun 8, 2023
e17ef58
Change description of registerCustomRuleSets
niklam Jun 8, 2023
b7d432c
Fix description of registerCustomRuleSets
niklam Jun 8, 2023
5367773
Fix exception message
niklam Jun 8, 2023
1ad47d8
Add more info to docs
niklam Jun 8, 2023
c9c0b40
Change registerRuleSet() to have return type void
niklam Jun 8, 2023
5378a0a
Apply suggestions from code review
niklam Jun 18, 2023
87e701d
Changes per review comments
niklam Jun 18, 2023
bc011a7
Update src/RuleSet/RuleSets.php
niklam Jun 19, 2023
795624a
Introduce registering external RuleSets
niklam Oct 29, 2021
6c53776
Fix RuleSet name requirements
niklam Jun 19, 2023
4352e1b
Quick fixes
niklam Jun 19, 2023
3243de8
Introduce RuleSetNameValidator, related changes
niklam Jun 19, 2023
174dfc9
Satisfy current QA suite
Wirone Jan 19, 2024
4c90028
Simplify RuleSets test doubles
Wirone Jan 19, 2024
2232bdf
Use `RuleSetNameValidator` for internal validation of built-in fixers
Wirone Jan 19, 2024
0f72734
Allow `:risky` convention
Wirone Jan 19, 2024
5d883df
More test cases for `RuleSetNameValidator`
Wirone Jan 19, 2024
81c7a5a
Use rule sets' real name when registering
Wirone Jan 19, 2024
a8d0d45
Handle custom rule sets via dedicated contract
Wirone May 20, 2024
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: 0 additions & 24 deletions dev-tools/phpstan/baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -871,30 +871,6 @@
'count' => 1,
'path' => __DIR__ . '/../../src/RuleSet/RuleSet.php',
];
$ignoreErrors[] = [
// identifier: method.notFound
'message' => '#^Call to an undefined method object\\:\\:getName\\(\\)\\.$#',
'count' => 1,
'path' => __DIR__ . '/../../src/RuleSet/RuleSets.php',
];
$ignoreErrors[] = [
// identifier: return.type
'message' => '#^Method PhpCsFixer\\\\RuleSet\\\\RuleSets\\:\\:getSetDefinitions\\(\\) should return array\\<string, PhpCsFixer\\\\RuleSet\\\\RuleSetDescriptionInterface\\> but returns array\\<int\\|string, object\\>\\.$#',
'count' => 1,
'path' => __DIR__ . '/../../src/RuleSet/RuleSets.php',
];
$ignoreErrors[] = [
// identifier: argument.type
'message' => '#^Parameter \\#2 \\$callback of function uksort expects callable\\(int\\|string, int\\|string\\)\\: int, Closure\\(string, string\\)\\: int\\<\\-1, 1\\> given\\.$#',
'count' => 1,
'path' => __DIR__ . '/../../src/RuleSet/RuleSets.php',
];
$ignoreErrors[] = [
// identifier: assign.propertyType
'message' => '#^Static property PhpCsFixer\\\\RuleSet\\\\RuleSets\\:\\:\\$setDefinitions \\(array\\<string, PhpCsFixer\\\\RuleSet\\\\RuleSetDescriptionInterface\\>\\) does not accept array\\<int\\|string, object\\>\\.$#',
'count' => 1,
'path' => __DIR__ . '/../../src/RuleSet/RuleSets.php',
];
$ignoreErrors[] = [
// identifier: return.type
'message' => '#^Method PhpCsFixer\\\\Tokenizer\\\\Analyzer\\\\AttributeAnalyzer\\:\\:collectAttributes\\(\\) should return list\\<array\\{start\\: int, end\\: int, name\\: string\\}\\> but returns non\\-empty\\-array\\<int\\<0, max\\>, array\\{start\\: int, end\\: int, name\\: string\\}\\>\\.$#',
Expand Down
17 changes: 17 additions & 0 deletions doc/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,20 @@ configure them in your config file.
->setIndent("\t")
->setLineEnding("\r\n")
;

It's possible to register custom rule sets, which makes it easier to reuse custom configuration between multiple projects. If you have prepared rule set, you can register it, and then enable it in the rules. Custom rule sets (in this example ``\MyNameSpace\MyRuleSetClass``) must implement ``\PhpCsFixer\RuleSet\RuleSetDescriptionInterface``.

.. code-block:: php

<?php

return (new PhpCsFixer\Config())
->registerCustomRuleSets([
MyNameSpace\MyRuleSetClass::class, // It identifies itself as '@MyRuleSet'
])
->setRules([
'@MyRuleSet' => true,
])
;
Wirone marked this conversation as resolved.
Show resolved Hide resolved

ℹ️ If you use ``\PhpCsFixer\ConfigInterface`` implementation other than built-in one, make sure it implements ``\PhpCsFixer\CustomRulesetsAwareConfigInterface``.
33 changes: 32 additions & 1 deletion src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace PhpCsFixer;

use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
use PhpCsFixer\Runner\Parallel\ParallelConfig;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;

Expand All @@ -23,7 +24,7 @@
* @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
class Config implements ConfigInterface, ParallelAwareConfigInterface
class Config implements ConfigInterface, ParallelAwareConfigInterface, CustomRulesetsAwareConfigInterface
{
private string $cacheFile = '.php-cs-fixer.cache';

Expand All @@ -32,6 +33,11 @@
*/
private array $customFixers = [];

/**
* @var list<class-string<RuleSetDescriptionInterface>>
*/
private array $customRuleSets = [];

/**
* @var null|iterable<\SplFileInfo>
*/
Expand Down Expand Up @@ -94,6 +100,11 @@
return $this->customFixers;
}

public function getCustomRuleSets(): array
{
return $this->customRuleSets;
}

/**
* @return Finder
*/
Expand Down Expand Up @@ -163,6 +174,26 @@
return $this;
}

/**
* @param list<class-string<RuleSetDescriptionInterface>> $ruleSets
*/
public function registerCustomRuleSets(array $ruleSets): ConfigInterface
{
foreach ($ruleSets as $class) {
if (!class_exists($class)) {
throw new \UnexpectedValueException(sprintf('Rule set "%s" does not exist.', $class));
}

if (!\in_array(RuleSetDescriptionInterface::class, class_implements($class), true)) {
throw new \UnexpectedValueException(sprintf('Rule set "%s" does not implement "%s".', $class, RuleSetDescriptionInterface::class));
}
}

$this->customRuleSets = array_values(array_unique(array_merge($this->customRuleSets, $ruleSets)));

Check warning on line 192 in src/Config.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 mutation tests

Escaped Mutant for Mutator "UnwrapArrayMerge": --- Original +++ New @@ @@ throw new \UnexpectedValueException(sprintf('Rule set "%s" does not implement "%s".', $class, RuleSetDescriptionInterface::class)); } } - $this->customRuleSets = array_values(array_unique(array_merge($this->customRuleSets, $ruleSets))); + $this->customRuleSets = array_values(array_unique($ruleSets)); return $this; } public function setCacheFile(string $cacheFile) : ConfigInterface

Check warning on line 192 in src/Config.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 mutation tests

Escaped Mutant for Mutator "UnwrapArrayUnique": --- Original +++ New @@ @@ throw new \UnexpectedValueException(sprintf('Rule set "%s" does not implement "%s".', $class, RuleSetDescriptionInterface::class)); } } - $this->customRuleSets = array_values(array_unique(array_merge($this->customRuleSets, $ruleSets))); + $this->customRuleSets = array_values(array_merge($this->customRuleSets, $ruleSets)); return $this; } public function setCacheFile(string $cacheFile) : ConfigInterface

Check warning on line 192 in src/Config.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 mutation tests

Escaped Mutant for Mutator "UnwrapArrayValues": --- Original +++ New @@ @@ throw new \UnexpectedValueException(sprintf('Rule set "%s" does not implement "%s".', $class, RuleSetDescriptionInterface::class)); } } - $this->customRuleSets = array_values(array_unique(array_merge($this->customRuleSets, $ruleSets))); + $this->customRuleSets = array_unique(array_merge($this->customRuleSets, $ruleSets)); return $this; } public function setCacheFile(string $cacheFile) : ConfigInterface

return $this;
}

public function setCacheFile(string $cacheFile): ConfigInterface
{
$this->cacheFile = $cacheFile;
Expand Down
2 changes: 1 addition & 1 deletion src/Console/Command/DocumentationCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$fixerFactory->registerBuiltInFixers();
$fixers = $fixerFactory->getFixers();

$setDefinitions = RuleSets::getSetDefinitions();
$setDefinitions = RuleSets::getBuiltInSetDefinitions();

$fixerDocumentGenerator = new FixerDocumentGenerator($locator);
$ruleSetDocumentationGenerator = new RuleSetDocumentationGenerator($locator);
Expand Down
8 changes: 8 additions & 0 deletions src/Console/ConfigurationResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
use PhpCsFixer\Console\Report\FixReport\ReporterInterface;
use PhpCsFixer\CustomRulesetsAwareConfigInterface;
use PhpCsFixer\Differ\DifferInterface;
use PhpCsFixer\Differ\NullDiffer;
use PhpCsFixer\Differ\UnifiedDiffer;
Expand All @@ -38,6 +39,7 @@
use PhpCsFixer\ParallelAwareConfigInterface;
use PhpCsFixer\RuleSet\RuleSet;
use PhpCsFixer\RuleSet\RuleSetInterface;
use PhpCsFixer\RuleSet\RuleSets;
use PhpCsFixer\Runner\Parallel\ParallelConfig;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
use PhpCsFixer\StdinFileInfo;
Expand Down Expand Up @@ -273,6 +275,12 @@
if (null === $this->config) {
$this->config = $this->defaultConfig;
}

if ($this->config instanceof CustomRulesetsAwareConfigInterface) {

Check warning on line 279 in src/Console/ConfigurationResolver.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 mutation tests

Escaped Mutant for Mutator "InstanceOf_": --- Original +++ New @@ @@ if (null === $this->config) { $this->config = $this->defaultConfig; } - if ($this->config instanceof CustomRulesetsAwareConfigInterface) { + if (true) { foreach ($this->config->getCustomRuleSets() as $ruleSet) { RuleSets::registerCustomRuleSet($ruleSet); }
foreach ($this->config->getCustomRuleSets() as $ruleSet) {
RuleSets::registerCustomRuleSet($ruleSet);
}
}
}

return $this->config;
Expand Down
39 changes: 39 additions & 0 deletions src/CustomRulesetsAwareConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer;

use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;

/**
* @author Greg Korba <greg@codito.dev>
*
* @TODO 4.0 Include support for custom rulesets in main ConfigInterface
*/
interface CustomRulesetsAwareConfigInterface extends ConfigInterface
{
/**
* Registers custom rule sets to be used the same way as built-in rule sets.
*
* @param list<class-string<RuleSetDescriptionInterface>> $ruleSets
*
* @todo v4 Introduce it in main ConfigInterface
*/
public function registerCustomRuleSets(array $ruleSets): ConfigInterface;

/**
* @return list<class-string<RuleSetDescriptionInterface>>
*/
public function getCustomRuleSets(): array;
}
73 changes: 67 additions & 6 deletions src/RuleSet/RuleSets.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace PhpCsFixer\RuleSet;

use PhpCsFixer\RuleSetNameValidator;
use Symfony\Component\Finder\Finder;

/**
Expand All @@ -26,27 +27,52 @@
/**
* @var array<string, RuleSetDescriptionInterface>
*/
private static $setDefinitions;
private static ?array $builtInSetDefinitions = null;

/**
* @var array<string, RuleSetDescriptionInterface>
*/
private static array $customRuleSetDefinitions = [];

/**
* @return array<string, RuleSetDescriptionInterface>
*/
public static function getSetDefinitions(): array
{
if (null === self::$setDefinitions) {
self::$setDefinitions = [];
$allRuleSets = array_merge(
self::getBuiltInSetDefinitions(),
self::$customRuleSetDefinitions
);

uksort($allRuleSets, static fn (string $x, string $y): int => strnatcmp($x, $y));

Check warning on line 47 in src/RuleSet/RuleSets.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 mutation tests

Escaped Mutant for Mutator "FunctionCallRemoval": --- Original +++ New @@ @@ public static function getSetDefinitions() : array { $allRuleSets = array_merge(self::getBuiltInSetDefinitions(), self::$customRuleSetDefinitions); - uksort($allRuleSets, static fn(string $x, string $y): int => strnatcmp($x, $y)); + return $allRuleSets; } /**

return $allRuleSets;
}

/**
* @return array<string, RuleSetDescriptionInterface>
*/
public static function getBuiltInSetDefinitions(): array
{
if (null === self::$builtInSetDefinitions) {
self::$builtInSetDefinitions = [];

foreach (Finder::create()->files()->in(__DIR__.'/Sets') as $file) {
/** @var class-string<RuleSetDescriptionInterface> $class */
$class = 'PhpCsFixer\RuleSet\Sets\\'.$file->getBasename('.php');
$set = new $class();

self::$setDefinitions[$set->getName()] = $set;
if (!RuleSetNameValidator::isValid($set->getName(), false)) {
throw new \InvalidArgumentException(sprintf('Rule set name invalid: %s', $set->getName()));
}

self::$builtInSetDefinitions[$set->getName()] = $set;
}

uksort(self::$setDefinitions, static fn (string $x, string $y): int => strnatcmp($x, $y));
uksort(self::$builtInSetDefinitions, static fn (string $x, string $y): int => strnatcmp($x, $y));
}

return self::$setDefinitions;
return self::$builtInSetDefinitions;
}

/**
Expand All @@ -67,4 +93,39 @@

return $definitions[$name];
}

/**
* @param class-string<RuleSetDescriptionInterface> $class
*/
public static function registerCustomRuleSet(string $class): void
{
if (!class_exists($class)
|| !\in_array(RuleSetDescriptionInterface::class, class_implements($class), true)
) {
throw new \InvalidArgumentException(
sprintf(
'Class "%s" must be an instance of "%s".',
$class,
RuleSetDescriptionInterface::class
)
);
}

$ruleset = new $class();
$name = $ruleset->getName();

if (!RuleSetNameValidator::isValid($name, true)) {

Check warning on line 117 in src/RuleSet/RuleSets.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 mutation tests

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ } $ruleset = new $class(); $name = $ruleset->getName(); - if (!RuleSetNameValidator::isValid($name, true)) { + if (!RuleSetNameValidator::isValid($name, false)) { throw new \InvalidArgumentException('RuleSet name must begin with "@" and a letter (a-z, A-Z), and can contain only letters (a-z, A-Z), numbers, underscores, slashes, colons, dots and hyphens.'); } if (!class_exists($class, true)) {
throw new \InvalidArgumentException('RuleSet name must begin with "@" and a letter (a-z, A-Z), and can contain only letters (a-z, A-Z), numbers, underscores, slashes, colons, dots and hyphens.');
}

if (!class_exists($class, true)) {
throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
}

if (\array_key_exists($name, self::getSetDefinitions())) {
throw new \InvalidArgumentException(sprintf('Set "%s" is already defined.', $name));
}

self::$customRuleSetDefinitions[$name] = $ruleset;
}
}
27 changes: 27 additions & 0 deletions src/RuleSetNameValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer;

final class RuleSetNameValidator
{
public static function isValid(string $name, bool $isCustom): bool
{
if (!$isCustom) {
return Preg::match('/^@[a-zA-Z][a-zA-Z0-9:_\/\.-]*$/', $name);
}

return Preg::match('/^@[a-zA-Z][a-zA-Z0-9:_\/\.-]*$/', $name);
}
}
1 change: 1 addition & 0 deletions tests/AutoReview/DescribeCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function testDescribeCommand(FixerFactory $factory, string $fixerName, ?a
$commandTester->execute([
'command' => $command->getName(),
'name' => $fixerName,
'--config' => realpath(__DIR__.'/../../.php-cs-fixer.dist.php'),
]);

self::assertSame(0, $commandTester->getStatusCode());
Expand Down
4 changes: 2 additions & 2 deletions tests/AutoReview/DocumentationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public function testRuleSetsDocumentationIsUpToDate(): void
$fixers = self::getFixers();
$paths = [];

foreach (RuleSets::getSetDefinitions() as $name => $definition) {
foreach (RuleSets::getBuiltInSetDefinitions() as $name => $definition) {
$path = $locator->getRuleSetsDocumentationFilePath($name);
$paths[$path] = $definition;

Expand All @@ -157,7 +157,7 @@ public function testRuleSetsDocumentationDirectoryHasNoExtraFiles(): void
$generator = new DocumentationLocator();

self::assertCount(
\count(RuleSets::getSetDefinitions()) + 1,
\count(RuleSets::getBuiltInSetDefinitions()) + 1,
(new Finder())->files()->in($generator->getRuleSetsDocumentationDirectoryPath())
);
}
Expand Down
Loading