Skip to content

Add Support for PHPCS Standards / Rulesets and Related Bug Fixes #216

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

63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- Install on **any PHP 7.2-PHP 8.3** project with any dependencies
- Blazing fast with parallel run out of the box
- Use [PHP_CodeSniffer or PHP-CS-Fixer](https://tomasvotruba.com/blog/2017/05/03/combine-power-of-php-code-sniffer-and-php-cs-fixer-in-3-lines/) - anything you like
- Use **prepared sets** and [PHP CS Fixer sets](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/ruleSets/index.rst) to save time
- Use **prepared sets**, [PHP CS Fixer sets](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/ruleSets/index.rst), or [PHP Code Sniffer standards](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Usage#specifying-a-coding-standard) to save time

<br>

Expand Down Expand Up @@ -50,6 +50,7 @@ That's it!
Most of the time, you'll be happy with the default configuration. The most relevant part is configuring paths, checkers and sets:

```php
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff;
use PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;

Expand All @@ -58,6 +59,9 @@ return ECSConfig::configure()
->withRules([
ArraySyntaxFixer::class,
])
// Warnings for included PHPCS rules are disabled by default,
// but they can be enabled manually.
->withWarnings()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for many contributions 🥳

Let's take it one by one to ship them faster :)
I like this feature - please separate it to own PR 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing!

Just so we're on the same page, do you just want me to pull out the with[out]Warnings methods/parameters with warnings disabled by default, then enabled when using withSnifferStandards?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 👍

Lets start with warnings as a standalone feature. Without any dependency on withSnifferStandards.

Copy link
Contributor Author

@Kenneth-Sills Kenneth-Sills Jun 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay, been focused on "work" work! I'll be able to get back to this soon, and I'll create four five separate PRs for (in this order):

  1. The additional test script. Submitted via Add Convenience Composer test Script #227.
  2. The cache invalidation fix. Submitted via Improve Reliability of Configuration Caching #226.
  3. Duplicate error reports for PHPCS rules. Submitted via Stop Reporting Duplicate Errors for Sniffs in the Presence of Fixable Changes #228.
  4. Missing logic to skip self-disabled PHPCS rules during parsing. Submitted via Allow PHPCS Sniffs to Disable Themselves #229.
  5. Add support for PHPCS warnings.

I'll make sure each is appropriately tested, of course. I'll then rebase this branch and force merge to my fork. Hopefully that'll cut down on the effort required to review these submissions.😄

Let me know if you're opposed to the plan above or have any suggestions. Thanks for your time!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4/5 of the above have been prepared and are ready for your review! The last changes to allow PHPCS warnings has dependencies on the others though, so I'll need to wait for those PRs to be merged before coming back to finish it.

I appreciate your time, Tomas! Sorry again for taking so long. 😭

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, Tomas! Don't mean to be a bother, but is there any chance we could take a look at some of those above PRs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's been a while, I'll need to revisit the PHPCS warnings support feature to see what needs to be done to support it. 😅

->withPreparedSets(psr12: true);
```

Expand Down Expand Up @@ -89,6 +93,52 @@ return ECSConfig::configure()

<br>

Do you want to use [an existing PHPCS config or standard](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Usage#specifying-a-coding-standard)?
You have plenty of options:

```php
use Symplify\EasyCodingStandard\Config\ECSConfig;

return ECSConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])

// Load your existing [.]phpcs.xml[.dist]
->withSnifferStandards()

// Use any of their 8 built-ins.
->withSnifferStandards(psr12: true)

// Maybe third parties interest you?
->withSnifferStandards(vendor: 'WordPress')

// Or maybe you use LOTS of configs?
->withSnifferStandards(
config: [ 'phpcs.xml.dist', 'phpcs.xml' ],
vendor: [ 'WordPress', 'PHPCompatibility' ],
psr12: true
)

// The standards warning severities are respected by default,
// this returns to the default behavior of skipping most warnings.
->withSnifferStandards(withWarnings: false);
```

> [!IMPORTANT]
> This only supports a **subset** of PHPCS configuration:
>
> - Included rules and their default configurations.
> - Rules excluded globally or based on patterns.
> - Severity levels.
> - Ruleset inheritance.
>
> Rules included only for specific paths will be included globally instead.
> To migrate, enable these rules globally and specify _excluded_ paths instead.
>
> All other arguments, parameters, or settings are ignored.
> This includes specified file patterns and extensions.

<br>

### How to Skip Files/Rules?

Love the sets of rules, but want to skip single rule or some files?
Expand Down Expand Up @@ -203,6 +253,17 @@ Do you look for json format?
vendor/bin/ecs list-checkers --output-format json
```

### ECS isn't respecting my `phpcs:ignore` directives?

Currently, we do not support comment-based directives to enable, disable, or
ignore specific rules or specific tools. We do support our own global toggle:

```php
// @codingStandardsIgnoreStart
$atom = "[-a-z0-9!#$%&'*+/=?^_`{|}~]"; // RFC 5322 unquoted characters in local-part
// @codingStandardsIgnoreEnd
```

<br>

## How to Migrate from another coding standard tool?
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@fix-rector",
"@fix-cs"
],
"test": "phpunit",
"phpstan": "vendor/bin/phpstan analyse --ansi --memory-limit=1G --error-format symplify",
"rector": "vendor/bin/rector process --dry-run --memory-limit=1G --ansi",
"fix-rector": "vendor/bin/rector process --memory-limit=1G --ansi",
Expand Down
80 changes: 71 additions & 9 deletions src/Caching/FileHashComputer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
namespace Symplify\EasyCodingStandard\Caching;

use Nette\Utils\Json;
use PHP_CodeSniffer\Config as SnifferConfig;
use PHP_CodeSniffer\Sniffs\Sniff;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use Symplify\EasyCodingStandard\Application\Version\StaticVersionResolver;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\DependencyInjection\LazyContainerFactory;
use Symplify\EasyCodingStandard\DependencyInjection\SimpleParameterProvider;
use Symplify\EasyCodingStandard\Exception\Configuration\FileNotFoundException;
use Webmozart\Assert\Assert;

/**
* @see \Symplify\EasyCodingStandard\Tests\ChangedFilesDetector\FileHashComputer\FileHashComputerTest
Expand All @@ -18,16 +22,41 @@ final class FileHashComputer
{
public function computeConfig(string $filePath): string
{
$callable = require $filePath;
Assert::isCallable($callable);
$ecsConfig = (new LazyContainerFactory())->create([$filePath]);

$ecsConfig = new ECSConfig();
$callable($ecsConfig);
$services = array_reduce(
array_keys($ecsConfig->getBindings()),
function ($services, $className) use ($ecsConfig): array {
$service = $ecsConfig->get($className);

// hash the container setup
$fileHash = sha1(Json::encode($ecsConfig->getBindings()));
$isSniff = $service instanceof Sniff;
$isFixer = $service instanceof FixerInterface;

return sha1($fileHash . SimpleParameterProvider::hash() . StaticVersionResolver::PACKAGE_VERSION);
return [
$className => match (true) {
$isSniff => $this->getSniffConfiguration($service),
$isFixer => $this->getFixerConfiguration($service),

// It's too hard to define a good general rule for serialization.
// An arbitrary class property could include a timestamp somewhere,
// causing permanent cache invalidation, for instance.
default => [],
},
...$services,
];
},
[]
);

$servicesHash = sha1(Json::encode($services));
$snifferConfigHash = sha1(Json::encode($ecsConfig->get(SnifferConfig::class)->getSettings()));

return sha1(implode('', [
$servicesHash,
$snifferConfigHash,
SimpleParameterProvider::hash(),
StaticVersionResolver::resolvePackageVersion(),
]));
}

public function compute(string $filePath): string
Expand All @@ -39,4 +68,37 @@ public function compute(string $filePath): string

return $fileHash;
}

/**
* All configurable options must be public properties.
*
* @return array<string, mixed>
*
* @see https://github.com/squizlabs/PHP_CodeSniffer/blob/c6c65ca0dc8608ba87631523b97b2f8d5351a854/src/Ruleset.php#L1309
*/
private function getSniffConfiguration(Sniff $sniff): array
{
return get_object_vars($sniff);
}

/**
* All configurable options will be in a protected property, but third-party
* plugins may not respect the same convention, so we leave a fallback.
*
* @return array<string, mixed>
*
* @see https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/a56dc23a3a3bd3c919a439fc9c9677256663749c/src/AbstractFixer.php#L38
*/
private function getFixerConfiguration(FixerInterface $fixer): array
{
$extendsAbstract = $fixer instanceof AbstractFixer;
$isConfigurable = $fixer instanceof ConfigurableFixerInterface;

if (! $isConfigurable || ! $extendsAbstract) {
return get_object_vars($fixer);
}

$properties = (array) $fixer;
return $properties["\0*\0configuration"];
}
}
49 changes: 46 additions & 3 deletions src/Config/ECSConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
namespace Symplify\EasyCodingStandard\Config;

use Illuminate\Container\Container;
use PHP_CodeSniffer\Config as SnifferConfig;
use PHP_CodeSniffer\Ruleset as SnifferRuleSet;
use PHP_CodeSniffer\Sniffs\Sniff;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\RuleSet\RuleSet;
use PhpCsFixer\RuleSet\RuleSet as FixerRuleSet;
use PhpCsFixer\WhitespacesFixerConfig;
use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder;
use Symplify\EasyCodingStandard\Contract\Console\Output\OutputFormatterInterface;
Expand Down Expand Up @@ -197,7 +199,7 @@ public function dynamicSets(array $setNames): void
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();

$ruleSet = new RuleSet(array_fill_keys($setNames, true));
$ruleSet = new FixerRuleSet(array_fill_keys($setNames, true));
$fixerFactory->useRuleSet($ruleSet);

/** @var FixerInterface $fixer */
Expand All @@ -212,6 +214,47 @@ public function dynamicSets(array $setNames): void
}
}

/**
* @param string[] $standards
*/
public function snifferStandards(array $standards): void
{
$config = $this->get(SnifferConfig::class);
$config->standards = $standards;

$ruleset = new SnifferRuleSet($config);
$this->singleton(SnifferRuleSet::class, static fn (): SnifferRuleSet => $ruleset);

$globalIgnorePaths = $ruleset->getIgnorePatterns();

/** @var class-string<Sniff> $className */
foreach ($ruleset->sniffs as $className => $instance) {
/* ECS doesn't currently support rules being conditionally enabled
for subpaths, only disabled. So all rules will be globally
enabled. */
$this->singleton($className, static fn () => $instance);

$ignorePaths = $ruleset->getIgnorePatterns($className);

if (! (empty($ignorePaths) && empty($globalIgnorePaths))) {
$this->skip([
$className => array_merge($ignorePaths, $globalIgnorePaths),
]);
}
}
}

public function setWarnings(bool $areEnabled): void
{
$config = $this->get(SnifferConfig::class);

if ($areEnabled) {
$config->warningSeverity = $config->warningSeverity ?: 1;
} else {
$config->warningSeverity = 0;
}
}

public function import(string $setFilePath): void
{
$self = $this;
Expand Down Expand Up @@ -251,7 +294,7 @@ public function singleton($abstract, mixed $concrete = null): void
}

/**
* @param class-string $checkerClass
* @phpstan-assert class-string<Sniff|FixerInterface> $checkerClass
*/
private function assertCheckerClass(string $checkerClass): void
{
Expand Down
Loading
Loading