Skip to content
Open
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);
16 changes: 7 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
"php": ">=8.3",
"composer/pcre": "^3.3.2",
"composer/xdebug-handler": "^3.0.5",
"entropy/entropy": "dev-main",
"entropy/entropy": "dev-feature/ecs-console-and-container",
"friendsofphp/php-cs-fixer": "^3.95.1|^4.0",
"illuminate/container": "12.39.*",
"nette/utils": "4.0.8",
"sebastian/diff": "^6.0|^7.0",
"squizlabs/php_codesniffer": "^4.0.1",
Expand Down Expand Up @@ -56,6 +55,12 @@
"tests/functions.php"
]
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/TomasVotruba/entropy.git"
}
],
"config": {
"sort-packages": true,
"platform-check": false,
Expand Down Expand Up @@ -85,12 +90,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
217 changes: 152 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 = [];

/**
* Checkers already registered as a container service (registered exactly once,
* even when a class is declared in both a set and explicitly).
*
* @var array<class-string<Sniff|FixerInterface>, true>
*/
private array $checkerServiceRegistered = [];

/**
* 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,136 @@ public function boot(): void
}

/**
* @param string $abstract
* 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
{
// 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;
}

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

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

$checkerInstances[] = $this->make($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
{
parent::singleton($abstract, $concrete);
return array_values(array_filter(
array_keys($this->checkerConfiguration),
fn (string $checkerClass): bool => ! isset($this->removedCheckers[$checkerClass])
));
}

foreach (self::AUTOTAG_INTERFACES as $autotagInterface) {
if (! is_a($abstract, $autotagInterface, true)) {
/**
* 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;
}

$this->tag($abstract, $autotagInterface);
$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]);

// register the checker as a service exactly once; the factory reads the
// (possibly updated) configuration lazily at build time
if (isset($this->checkerServiceRegistered[$checkerClass])) {
return;
}

$this->checkerServiceRegistered[$checkerClass] = true;
$this->service($checkerClass, fn (): object => $this->buildConfiguredChecker($checkerClass));
}

/**
* @param class-string<Sniff|FixerInterface> $checkerClass
*/
private function buildConfiguredChecker(string $checkerClass): object
{
$checker = $this->build($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;
}
}

return $checker;
}

/**
Expand Down Expand Up @@ -299,27 +409,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