Skip to content
Merged
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
2 changes: 1 addition & 1 deletion bin/ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public function loadIfNotLoadedYet(string $file): void
}

/** @var EasyCodingStandardConsoleApplication $application */
$application = $container->get(EasyCodingStandardConsoleApplication::class);
$application = $container->make(EasyCodingStandardConsoleApplication::class);

$statusCode = $application->run();
exit($statusCode);
14 changes: 3 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
"php": ">=8.4",
"composer/pcre": "^3.3.2",
"composer/xdebug-handler": "^3.0.5",
"entropy/entropy": "^0.3",
"entropy/entropy": "^0.4",
"friendsofphp/php-cs-fixer": "^3.95.5",
"illuminate/container": "12.39.*",
"nette/utils": "^4.1",
"sebastian/diff": "^9.0",
"squizlabs/php_codesniffer": "^4.0.1",
Expand All @@ -32,13 +31,13 @@
"phpstan/phpstan": "^2.2",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-webmozart-assert": "^2.0",
"phpunit/phpunit": "^13.0",
"phpunit/phpunit": "^13.2",
"rector/jack": "^1.0",
"rector/rector": "^2.4",
"rector/type-perfect": "^2.1",
"symplify/phpstan-rules": "^14.11",
"symplify/vendor-patches": "^11.5",
"tomasvotruba/class-leak": "^2.1.6",
"tomasvotruba/class-leak": "^2.1.7",
"tomasvotruba/type-coverage": "^2.1",
"tomasvotruba/unused-public": "^2.2",
"tracy/tracy": "^2.12"
Expand Down Expand Up @@ -85,12 +84,5 @@
"symfony/event-dispatcher": "7.*",
"symfony/process": "7.*",
"symfony/stopwatch": "7.*"
},
"extra": {
"patches": {
"illuminate/container": [
"patches/illuminate-container-container-php.patch"
]
}
}
}
11 changes: 0 additions & 11 deletions patches/illuminate-container-container-php.patch

This file was deleted.

6 changes: 4 additions & 2 deletions src/Application/FileProcessorCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ final class FileProcessorCollector
/**
* orders matters, so Fixer can cleanup after Sniffer
*/
public function __construct(SniffFileProcessor $sniffFileProcessor, FixerFileProcessor $fixerFileProcessor)
{
public function __construct(
SniffFileProcessor $sniffFileProcessor,
FixerFileProcessor $fixerFileProcessor
) {
$this->fileProcessors[] = $sniffFileProcessor;
$this->fileProcessors[] = $fixerFileProcessor;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Caching/FileHashComputer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public function computeConfig(string $filePath): string
$ecsConfig = new ECSConfig();
$callable($ecsConfig);

// hash the container setup
$fileHash = sha1(Json::encode($ecsConfig->getBindings()));
// hash the registered checkers and their configuration
$fileHash = sha1(Json::encode($ecsConfig->getCheckerConfiguration()));

return sha1($fileHash . SimpleParameterProvider::hash() . StaticVersionResolver::PACKAGE_VERSION);
}
Expand Down
239 changes: 174 additions & 65 deletions src/Config/ECSConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Symplify\EasyCodingStandard\Config;

use Illuminate\Container\Container;
use Entropy\Container\Container;
use Override;
use PHP_CodeSniffer\Sniffs\Sniff;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
Expand All @@ -14,7 +14,6 @@
use PhpCsFixer\RuleSet\RuleSet;
use PhpCsFixer\WhitespacesFixerConfig;
use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder;
use Symplify\EasyCodingStandard\Contract\Console\Output\OutputFormatterInterface;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\ConflictingCheckersCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\RemoveExcludedCheckersCompilerPass;
use Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\RemoveMutualCheckersCompilerPass;
Expand All @@ -29,9 +28,34 @@
final class ECSConfig extends Container
{
/**
* @var string[]
* Registered checkers, mapped to their configuration (empty array = no configuration).
*
* @var array<class-string<Sniff|FixerInterface>, mixed[]>
*/
private const array AUTOTAG_INTERFACES = [Sniff::class, FixerInterface::class, OutputFormatterInterface::class];
private array $checkerConfiguration = [];

/**
* Checkers removed by compiler passes (skip, mutual exclusion, …).
*
* @var array<class-string<Sniff|FixerInterface>, true>
*/
private array $removedCheckers = [];

/**
* Configured checker instances, built exactly once and shared, even when a
* class is declared in both a set and explicitly.
*
* @var array<class-string<Sniff|FixerInterface>, Sniff|FixerInterface>
*/
private array $builtCheckers = [];

/**
* Registration order, with duplicates preserved: a checker declared both in a
* set and explicitly is listed twice (both share the last-wins configuration).
*
* @var list<class-string<Sniff|FixerInterface>>
*/
private array $checkerRegistrationOrder = [];

public static function configure(): ECSConfigBuilder
{
Expand Down Expand Up @@ -76,18 +100,7 @@ public function rule(string $checkerClass): void
{
$this->assertCheckerClass($checkerClass);

$this->singleton($checkerClass);
$this->autowireWhitespaceAwareFixer($checkerClass);

if (is_a($checkerClass, ConfigurableFixerInterface::class, true)) {
$this->extend(
$checkerClass,
static function (ConfigurableFixerInterface $configurableFixer): ConfigurableFixerInterface {
$configurableFixer->configure([]);
return $configurableFixer;
}
);
}
$this->registerChecker($checkerClass, []);
}

/**
Expand All @@ -110,30 +123,11 @@ public function ruleWithConfiguration(string $checkerClass, array $configuration
{
$this->assertCheckerClass($checkerClass);

$this->singleton($checkerClass);

$this->autowireWhitespaceAwareFixer($checkerClass);

if (is_a($checkerClass, FixerInterface::class, true)) {
Assert::isAOf($checkerClass, ConfigurableFixerInterface::class);
$this->extend($checkerClass, static function (ConfigurableFixerInterface $configurableFixer) use (
$configuration
): ConfigurableFixerInterface {
$configurableFixer->configure($configuration);
return $configurableFixer;
});
}

if (is_a($checkerClass, Sniff::class, true)) {
$this->extend($checkerClass, static function (Sniff $sniff) use ($configuration): Sniff {
foreach ($configuration as $propertyName => $value) {
Assert::propertyExists($sniff, $propertyName);
$sniff->{$propertyName} = $value;
}

return $sniff;
});
}
$this->registerChecker($checkerClass, $configuration);
}

/**
Expand Down Expand Up @@ -253,20 +247,158 @@ public function boot(): void
}

/**
* @param string $abstract
* Checkers are returned fully configured; every other class is built by the parent container.
*
* @template TType as object
*
* @param class-string<TType> $class
* @return TType
*/
#[Override]
public function make(string $class): object
{
if (isset($this->checkerConfiguration[$class])) {
// a configured-checker key is always a Sniff|FixerInterface class-string
/** @var class-string<Sniff|FixerInterface> $checkerClass */
$checkerClass = $class;

/** @var TType $checker */
$checker = $this->buildConfiguredChecker($checkerClass);
return $checker;
}

return parent::make($class);
}

/**
* Checkers are returned in registration order with duplicates preserved (a class
* declared both in a set and explicitly appears twice, sharing one instance);
* every other service is resolved by the parent container.
*
* @template TType as object
*
* @param class-string<TType> $contractClass
* @return array<TType>
*/
#[Override]
public function singleton($abstract, mixed $concrete = null): void
public function findByContract(string $contractClass): array
{
parent::singleton($abstract, $concrete);
// genuine (non-checker) services from the parent container
$instances = [];
foreach (parent::findByContract($contractClass) as $class => $instance) {
if (isset($this->checkerConfiguration[$class])) {
// checkers are handled below, in registration order with duplicates
continue;
}

$instances[] = $instance;
}

foreach (self::AUTOTAG_INTERFACES as $autotagInterface) {
if (! is_a($abstract, $autotagInterface, true)) {
// checkers, in registration order, keeping duplicates and honouring removals
$checkerInstances = [];
foreach ($this->checkerRegistrationOrder as $checkerClass) {
if (isset($this->removedCheckers[$checkerClass])) {
continue;
}

$this->tag($abstract, $autotagInterface);
// avoid building checkers that cannot match the requested contract
if (! is_a($checkerClass, $contractClass, true)) {
continue;
}

$checkerInstances[] = $this->buildConfiguredChecker($checkerClass);
}

$matchingCheckers = array_filter(
$checkerInstances,
static fn (object $instance): bool => $instance instanceof $contractClass
);

return [...$instances, ...array_values($matchingCheckers)];
}

/**
* Registered checker classes, minus those removed by compiler passes.
*
* @return array<class-string<Sniff|FixerInterface>>
*/
public function getCheckerClasses(): array
{
return array_values(array_filter(
array_keys($this->checkerConfiguration),
fn (string $checkerClass): bool => ! isset($this->removedCheckers[$checkerClass])
));
}

/**
* Registered checkers and their configuration, used for cache invalidation.
*
* @return array<class-string<Sniff|FixerInterface>, mixed[]>
*/
public function getCheckerConfiguration(): array
{
$checkerConfiguration = [];
foreach ($this->checkerConfiguration as $checkerClass => $configuration) {
if (isset($this->removedCheckers[$checkerClass])) {
continue;
}

$checkerConfiguration[$checkerClass] = $configuration;
}

return $checkerConfiguration;
}

/**
* @param class-string<Sniff|FixerInterface> $checkerClass
*/
public function removeChecker(string $checkerClass): void
{
$this->removedCheckers[$checkerClass] = true;
}

/**
* @param class-string<Sniff|FixerInterface> $checkerClass
* @param mixed[] $configuration
*/
private function registerChecker(string $checkerClass, array $configuration): void
{
// last registration wins for configuration, mirroring the previous container override behaviour
$this->checkerConfiguration[$checkerClass] = $configuration;
$this->checkerRegistrationOrder[] = $checkerClass;
unset($this->removedCheckers[$checkerClass]);
}

/**
* @param class-string<Sniff|FixerInterface> $checkerClass
*/
private function buildConfiguredChecker(string $checkerClass): object
{
if (isset($this->builtCheckers[$checkerClass])) {
return $this->builtCheckers[$checkerClass];
}

// parent::make() autowires the raw checker by reflection; this class' make() override would recurse here
$checker = parent::make($checkerClass);

if ($checker instanceof WhitespacesAwareFixerInterface) {
$checker->setWhitespacesConfig($this->make(WhitespacesFixerConfig::class));
}

$configuration = $this->checkerConfiguration[$checkerClass];

if ($checker instanceof ConfigurableFixerInterface) {
$checker->configure($configuration);
} elseif ($checker instanceof Sniff) {
foreach ($configuration as $propertyName => $value) {
Assert::propertyExists($checker, $propertyName);
$checker->{$propertyName} = $value;
}
}

$this->builtCheckers[$checkerClass] = $checker;

return $checker;
}

/**
Expand Down Expand Up @@ -299,27 +431,4 @@ private function ensureCheckerClassesAreUnique(array $checkerClasses): void
);
throw new InvalidArgumentException($errorMessage);
}

/**
* @param class-string<FixerInterface|Sniff> $checkerClass
*/
private function autowireWhitespaceAwareFixer(string $checkerClass): void
{
if (! is_a($checkerClass, WhitespacesAwareFixerInterface::class, true)) {
return;
}

$this->extend(
$checkerClass,
static function (
WhitespacesAwareFixerInterface $whitespacesAwareFixer,
Container $container
): WhitespacesAwareFixerInterface {
$whitespacesFixerConfig = $container->make(WhitespacesFixerConfig::class);
$whitespacesAwareFixer->setWhitespacesConfig($whitespacesFixerConfig);

return $whitespacesAwareFixer;
}
);
}
}
5 changes: 3 additions & 2 deletions src/Console/Output/OutputFormatterCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ final class OutputFormatterCollector
/**
* @param OutputFormatterInterface[] $outputFormatters
*/
public function __construct(array $outputFormatters)
{
public function __construct(
array $outputFormatters
) {
foreach ($outputFormatters as $outputFormatter) {
$this->outputFormatters[$outputFormatter->getName()] = $outputFormatter;
}
Expand Down
Loading
Loading