33declare (strict_types=1 );
44namespace Symplify \EasyCodingStandard \Config ;
55
6- use ECSPrefix202606 \Illuminate \Container \Container ;
6+ use ECSPrefix202606 \Entropy \Container \Container ;
77use Override ;
88use PHP_CodeSniffer \Sniffs \Sniff ;
99use PhpCsFixer \Fixer \ConfigurableFixerInterface ;
1313use PhpCsFixer \RuleSet \RuleSet ;
1414use PhpCsFixer \WhitespacesFixerConfig ;
1515use Symplify \EasyCodingStandard \Configuration \ECSConfigBuilder ;
16- use Symplify \EasyCodingStandard \Contract \Console \Output \OutputFormatterInterface ;
1716use Symplify \EasyCodingStandard \DependencyInjection \CompilerPass \ConflictingCheckersCompilerPass ;
1817use Symplify \EasyCodingStandard \DependencyInjection \CompilerPass \RemoveExcludedCheckersCompilerPass ;
1918use Symplify \EasyCodingStandard \DependencyInjection \CompilerPass \RemoveMutualCheckersCompilerPass ;
2726final class ECSConfig extends Container
2827{
2928 /**
30- * @var string[]
29+ * Registered checkers, mapped to their configuration (empty array = no configuration).
30+ *
31+ * @var array<class-string<Sniff|FixerInterface>, mixed[]>
3132 */
32- private const AUTOTAG_INTERFACES = [Sniff::class, FixerInterface::class, OutputFormatterInterface::class];
33+ private $ checkerConfiguration = [];
34+ /**
35+ * Checkers removed by compiler passes (skip, mutual exclusion, …).
36+ *
37+ * @var array<class-string<Sniff|FixerInterface>, true>
38+ */
39+ private $ removedCheckers = [];
40+ /**
41+ * Configured checker instances, built exactly once and shared, even when a
42+ * class is declared in both a set and explicitly.
43+ *
44+ * @var array<class-string<Sniff|FixerInterface>, Sniff|FixerInterface>
45+ */
46+ private $ builtCheckers = [];
47+ /**
48+ * Registration order, with duplicates preserved: a checker declared both in a
49+ * set and explicitly is listed twice (both share the last-wins configuration).
50+ *
51+ * @var list<class-string<Sniff|FixerInterface>>
52+ */
53+ private $ checkerRegistrationOrder = [];
3354 public static function configure (): ECSConfigBuilder
3455 {
3556 return new ECSConfigBuilder ();
@@ -66,14 +87,7 @@ public function sets(array $sets): void
6687 public function rule (string $ checkerClass ): void
6788 {
6889 $ this ->assertCheckerClass ($ checkerClass );
69- $ this ->singleton ($ checkerClass );
70- $ this ->autowireWhitespaceAwareFixer ($ checkerClass );
71- if (is_a ($ checkerClass , ConfigurableFixerInterface::class, \true)) {
72- $ this ->extend ($ checkerClass , static function (ConfigurableFixerInterface $ configurableFixer ): ConfigurableFixerInterface {
73- $ configurableFixer ->configure ([]);
74- return $ configurableFixer ;
75- });
76- }
90+ $ this ->registerChecker ($ checkerClass , []);
7791 }
7892 /**
7993 * @param array<class-string<Sniff|FixerInterface>> $checkerClasses
@@ -92,24 +106,10 @@ public function rules(array $checkerClasses): void
92106 public function ruleWithConfiguration (string $ checkerClass , array $ configuration ): void
93107 {
94108 $ this ->assertCheckerClass ($ checkerClass );
95- $ this ->singleton ($ checkerClass );
96- $ this ->autowireWhitespaceAwareFixer ($ checkerClass );
97109 if (is_a ($ checkerClass , FixerInterface::class, \true)) {
98110 Assert::isAOf ($ checkerClass , ConfigurableFixerInterface::class);
99- $ this ->extend ($ checkerClass , static function (ConfigurableFixerInterface $ configurableFixer ) use ($ configuration ): ConfigurableFixerInterface {
100- $ configurableFixer ->configure ($ configuration );
101- return $ configurableFixer ;
102- });
103- }
104- if (is_a ($ checkerClass , Sniff::class, \true)) {
105- $ this ->extend ($ checkerClass , static function (Sniff $ sniff ) use ($ configuration ): Sniff {
106- foreach ($ configuration as $ propertyName => $ value ) {
107- Assert::propertyExists ($ sniff , $ propertyName );
108- $ sniff ->{$ propertyName } = $ value ;
109- }
110- return $ sniff ;
111- });
112111 }
112+ $ this ->registerChecker ($ checkerClass , $ configuration );
113113 }
114114 /**
115115 * @param array<class-string<Sniff|FixerInterface>, mixed[]> $rulesWithConfiguration
@@ -206,19 +206,134 @@ public function boot(): void
206206 $ conflictingCheckersCompilerPass ->process ($ this );
207207 }
208208 /**
209- * @param string $abstract
210- * @param mixed $concrete
209+ * Checkers are returned fully configured; every other class is built by the parent container.
210+ *
211+ * @template TType as object
212+ *
213+ * @param class-string<TType> $class
214+ * @return TType
211215 */
212216 #[Override]
213- public function singleton ($ abstract , $ concrete = null ): void
217+ public function make (string $ class ): object
218+ {
219+ if (isset ($ this ->checkerConfiguration [$ class ])) {
220+ // a configured-checker key is always a Sniff|FixerInterface class-string
221+ /** @var class-string<Sniff|FixerInterface> $checkerClass */
222+ $ checkerClass = $ class ;
223+ /** @var TType $checker */
224+ $ checker = $ this ->buildConfiguredChecker ($ checkerClass );
225+ return $ checker ;
226+ }
227+ return parent ::make ($ class );
228+ }
229+ /**
230+ * Checkers are returned in registration order with duplicates preserved (a class
231+ * declared both in a set and explicitly appears twice, sharing one instance);
232+ * every other service is resolved by the parent container.
233+ *
234+ * @template TType as object
235+ *
236+ * @param class-string<TType> $contractClass
237+ * @return array<TType>
238+ */
239+ #[Override]
240+ public function findByContract (string $ contractClass ): array
241+ {
242+ // genuine (non-checker) services from the parent container
243+ $ instances = [];
244+ foreach (parent ::findByContract ($ contractClass ) as $ class => $ instance ) {
245+ if (isset ($ this ->checkerConfiguration [$ class ])) {
246+ // checkers are handled below, in registration order with duplicates
247+ continue ;
248+ }
249+ $ instances [] = $ instance ;
250+ }
251+ // checkers, in registration order, keeping duplicates and honouring removals
252+ $ checkerInstances = [];
253+ foreach ($ this ->checkerRegistrationOrder as $ checkerClass ) {
254+ if (isset ($ this ->removedCheckers [$ checkerClass ])) {
255+ continue ;
256+ }
257+ // avoid building checkers that cannot match the requested contract
258+ if (!is_a ($ checkerClass , $ contractClass , \true)) {
259+ continue ;
260+ }
261+ $ checkerInstances [] = $ this ->buildConfiguredChecker ($ checkerClass );
262+ }
263+ $ matchingCheckers = array_filter ($ checkerInstances , static function (object $ instance ) use ($ contractClass ): bool {
264+ return $ instance instanceof $ contractClass ;
265+ });
266+ return array_merge ($ instances , array_values ($ matchingCheckers ));
267+ }
268+ /**
269+ * Registered checker classes, minus those removed by compiler passes.
270+ *
271+ * @return array<class-string<Sniff|FixerInterface>>
272+ */
273+ public function getCheckerClasses (): array
274+ {
275+ return array_values (array_filter (array_keys ($ this ->checkerConfiguration ), function (string $ checkerClass ): bool {
276+ return !isset ($ this ->removedCheckers [$ checkerClass ]);
277+ }));
278+ }
279+ /**
280+ * Registered checkers and their configuration, used for cache invalidation.
281+ *
282+ * @return array<class-string<Sniff|FixerInterface>, mixed[]>
283+ */
284+ public function getCheckerConfiguration (): array
214285 {
215- parent :: singleton ( $ abstract , $ concrete ) ;
216- foreach (self :: AUTOTAG_INTERFACES as $ autotagInterface ) {
217- if (! is_a ( $ abstract , $ autotagInterface , \true )) {
286+ $ checkerConfiguration = [] ;
287+ foreach ($ this -> checkerConfiguration as $ checkerClass => $ configuration ) {
288+ if (isset ( $ this -> removedCheckers [ $ checkerClass ] )) {
218289 continue ;
219290 }
220- $ this -> tag ( $ abstract , $ autotagInterface ) ;
291+ $ checkerConfiguration [ $ checkerClass ] = $ configuration ;
221292 }
293+ return $ checkerConfiguration ;
294+ }
295+ /**
296+ * @param class-string<Sniff|FixerInterface> $checkerClass
297+ */
298+ public function removeChecker (string $ checkerClass ): void
299+ {
300+ $ this ->removedCheckers [$ checkerClass ] = \true;
301+ }
302+ /**
303+ * @param class-string<Sniff|FixerInterface> $checkerClass
304+ * @param mixed[] $configuration
305+ */
306+ private function registerChecker (string $ checkerClass , array $ configuration ): void
307+ {
308+ // last registration wins for configuration, mirroring the previous container override behaviour
309+ $ this ->checkerConfiguration [$ checkerClass ] = $ configuration ;
310+ $ this ->checkerRegistrationOrder [] = $ checkerClass ;
311+ unset($ this ->removedCheckers [$ checkerClass ]);
312+ }
313+ /**
314+ * @param class-string<Sniff|FixerInterface> $checkerClass
315+ */
316+ private function buildConfiguredChecker (string $ checkerClass ): object
317+ {
318+ if (isset ($ this ->builtCheckers [$ checkerClass ])) {
319+ return $ this ->builtCheckers [$ checkerClass ];
320+ }
321+ // parent::make() autowires the raw checker by reflection; this class' make() override would recurse here
322+ $ checker = parent ::make ($ checkerClass );
323+ if ($ checker instanceof WhitespacesAwareFixerInterface) {
324+ $ checker ->setWhitespacesConfig ($ this ->make (WhitespacesFixerConfig::class));
325+ }
326+ $ configuration = $ this ->checkerConfiguration [$ checkerClass ];
327+ if ($ checker instanceof ConfigurableFixerInterface) {
328+ $ checker ->configure ($ configuration );
329+ } elseif ($ checker instanceof Sniff) {
330+ foreach ($ configuration as $ propertyName => $ value ) {
331+ Assert::propertyExists ($ checker , $ propertyName );
332+ $ checker ->{$ propertyName } = $ value ;
333+ }
334+ }
335+ $ this ->builtCheckers [$ checkerClass ] = $ checker ;
336+ return $ checker ;
222337 }
223338 /**
224339 * @param class-string $checkerClass
@@ -245,18 +360,4 @@ private function ensureCheckerClassesAreUnique(array $checkerClasses): void
245360 $ errorMessage = sprintf ('There are duplicated classes in $rectorConfig->rules(): "%s". Make them unique to avoid unexpected behavior. ' , implode ('", " ' , $ duplicatedCheckerClasses ));
246361 throw new InvalidArgumentException ($ errorMessage );
247362 }
248- /**
249- * @param class-string<FixerInterface|Sniff> $checkerClass
250- */
251- private function autowireWhitespaceAwareFixer (string $ checkerClass ): void
252- {
253- if (!is_a ($ checkerClass , WhitespacesAwareFixerInterface::class, \true)) {
254- return ;
255- }
256- $ this ->extend ($ checkerClass , static function (WhitespacesAwareFixerInterface $ whitespacesAwareFixer , Container $ container ): WhitespacesAwareFixerInterface {
257- $ whitespacesFixerConfig = $ container ->make (WhitespacesFixerConfig::class);
258- $ whitespacesAwareFixer ->setWhitespacesConfig ($ whitespacesFixerConfig );
259- return $ whitespacesAwareFixer ;
260- });
261- }
262363}
0 commit comments