• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / dresscode / 39958913551

23 Oct 2026 12:44PM UTC coverage: 96.921% (-0.9%) from 97.773%
39958913551

push

github

dg
uses the Runner of nette/command-line WIP

8 of 8 new or added lines in 1 file covered. (100.0%)

278 existing lines in 25 files now uncovered.

24711 of 25496 relevant lines covered (96.92%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.52
/src/Config/ConfigResolver.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the DressCode, a coding style and upgrade tool for PHP (https://dresscode.run)
5
 * Copyright (c) 2026 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace DressCode\Config;
9

10
use DressCode\{Config, ConfigurationException, Decision, DecisionKind, Plugin, Profile, Rule, RuleInfo, Value, Values};
11
use DressCode\Domains\Map;
12
use PhpSyntax\SymbolKind;
13
use function count, is_array, is_string;
14

15

16
/**
17
 * Lays the profiles that decide for a file one over another, each above the presets it uses, what the installed
18
 * packages say under them all, and resolves every decision and the rule owning it once into a ResolvedConfig.
19
 * @internal
20
 */
21
final class ConfigResolver
22
{
23
        /** the decision that lays the maps of the upgrading data of the packages under those of the project */
24
        private const PackagesDecision = 'upgrading.packages';
25

26
        /** @var array<string, string> */
27
        private array $warnings = [];
28

29
        /** @var array<string, Profile>  the presets by the path of their file */
30
        private array $presetFiles = [];
31

32
        /** @var list<string>  the preset files being laid, each used by the one before it */
33
        private array $using = [];
34

35
        /** @var \WeakMap<Config, array<class-string<Rule>, true>>  the rules some override of a resolved configuration turns on */
36
        private \WeakMap $rulesOfOverrides;
37

38

39
        public function __construct(
40
                private readonly PluginRegistry $registry,
1 ✔
41
                /**
42
                 * @var list<UpgradingData>  what the installed packages say, the maps of each laid under the project where it says
43
                 * `upgrading.packages: yes`
44
                 */
45
                private readonly array $upgradingData = [],
1 ✔
46
                /** the packages the project stands on, which decide whether a rule requiring one runs */
47
                private readonly ProjectPackages $project = new ProjectPackages,
1 ✔
48
                /** the run can get the types the configuration or the command line asks for; without them it resolves as if it asked for none */
49
                private readonly bool $typesAvailable = true,
1 ✔
50
                /** the directory a file in `use` of the configuration and its overrides is relative to */
51
                private readonly string $root = '.',
1 ✔
52
                /** @var list<string>  the packages whose plugins the project does not let in, so the maps of their rules are unknown */
53
                private readonly array $pluginsSkipped = [],
1 ✔
54
                /**
55
                 * @var list<string>  presets laid over those the configuration uses and beneath its own decisions and its overrides,
56
                 * so that the project keeps the last word, as `upgrade` lays its sets
57
                 */
58
                private readonly array $laid = [],
1 ✔
59
        ) {
60
                $this->rulesOfOverrides = new \WeakMap;
1 ✔
61
        }
1 ✔
62

63

64
        /**
65
         * What the caller should tell the user about the configuration, each thing once.
66
         * @return list<string>
67
         */
68
        public function getWarnings(): array
69
        {
70
                return array_values($this->warnings);
1 ✔
71
        }
72

73

74
        /**
75
         * The configuration as data for a file the given overrides match: what every decision ends up with and where it came
76
         * from, the rules owning those that take effect, and why a rule that does not run does not.
77
         * @param  string  $phpTarget  the versions the code is written for as a Composer constraint, unless a profile says another
78
         * @param  list<int>  $overrides  indexes of the overrides that match the file
79
         * @param  ?Profile  $commandLine  laid over everything else
80
         * @param  ?list<string>  $only  decisions, sections, presets and classes of rules the run is narrowed to
81
         * @throws ConfigurationException
82
         */
83
        public function resolve(
84
                Config $config,
85
                string $phpTarget,
86
                array $overrides = [],
87
                ?Profile $commandLine = null,
88
                ?array $only = null,
89
        ): ResolvedConfig
90
        {
91
                $fixRisky = $fixRiskyPaths = $reportOnly = $severities = $use = $suppressionComments = $suppressionExamples = [];
1 ✔
92
                $symbols = [SymbolKind::Function->name => [], SymbolKind::Constant->name => []];
1 ✔
93
                $php = $resolution = null;
1 ✔
94
                $typeAnalysis = ($commandLine instanceof Config ? $commandLine->typeAnalysis : null) ?? $config->typeAnalysis;
1 ✔
95
                $decisionLayers = [];
1 ✔
96
                // what the packages declare lies under every layer, which may add to it
97
                foreach ($this->upgradingData as $package) {
1 ✔
98
                        self::addSymbols($symbols, $package->namespaces, $package->layer->describe());
1 ✔
99
                }
100

101
                ['layers' => $collected, 'repeated' => $repeated] = $this->collectLayers(self::listProfiles($config, $overrides, $commandLine));
1 ✔
102
                foreach ($collected as [$layer, $profile]) {
1 ✔
103
                        try {
104
                                if ($layer->kind === LayerKind::Preset) {
1 ✔
105
                                        $use[] = (string) $layer->name;
1 ✔
106
                                }
107

108
                                $php = $profile->targets['php'] ?? $php;
1 ✔
109
                                $resolution = $profile->nameResolution ?? $resolution;
1 ✔
110

111
                                self::addSymbols($symbols, $profile->namespaces, $layer->describe());
1 ✔
112

113
                                if ($profile->decisions !== []) {
1 ✔
114
                                        new DecisionResolver($this->getCatalogue(), translator: $this->registry->translator)->checkLayer($profile->decisions);
1 ✔
115
                                        $decisionLayers[] = [$layer, $profile->decisions];
1 ✔
116
                                }
117

118
                                foreach ($profile->fixRisky as $name) {
1 ✔
119
                                        $expanded = $this->expandName($name);
1 ✔
120
                                        $fixRisky += array_fill_keys($expanded->rules, true);
1 ✔
121
                                        $fixRiskyPaths += array_fill_keys($expanded->paths, true);
1 ✔
122
                                }
123

124
                                foreach ($profile->reportOnly as $name) {
1 ✔
125
                                        $reportOnly += array_fill_keys($this->expandName($name)->paths, true);
1 ✔
126
                                }
127

128
                                $severities[] = $this->collectSeverity($profile->severity);
1 ✔
129

130
                                foreach ($profile->suppressionComments as $pattern => $entry) {
1 ✔
131
                                        // a pattern a later layer says again is that layer's, the example with it
132
                                        $suppressionComments[$pattern] = array_values(array_unique(array_merge(
1 ✔
133
                                                ...array_map($this->resolveSuppressedName(...), $entry['decisions']),
1 ✔
134
                                        )));
135
                                        $suppressionExamples[$pattern] = $entry['example'];
1 ✔
136
                                }
137

138
                        } catch (ConfigurationException $e) {
1 ✔
139
                                throw self::locate($e, $layer);
1 ✔
140
                        }
141
                }
142

143
                $narrowed = $only ? $this->resolveOnly($only) : null;
1 ✔
144
                // an override is resolved for a file it matches, so a name or an option it gets wrong would pass unnoticed until
145
                // such a file comes; each of them is resolved together with the configuration
146
                $resolvedOverrides = $overrides === []
1 ✔
147
                        ? array_map(
1 ✔
148
                                fn(int $index) => $this->resolve($config, $phpTarget, [$index], $commandLine, $only),
1 ✔
149
                                array_keys($config->overrides),
1 ✔
150
                        )
151
                        : [];
1 ✔
152
                // the rules some override turns on, whichever file it applies to, kept for the overrides resolved later; those
153
                // resolved above go without, only what runs being read of them
154
                if ($overrides === []) {
1 ✔
155
                        $ofOverrides = [];
1 ✔
156
                        foreach ($resolvedOverrides as $resolvedOverride) {
1 ✔
157
                                foreach ($resolvedOverride->getActiveRules() as $rule) {
1 ✔
158
                                        $ofOverrides[$rule->class] = true;
1 ✔
159
                                }
160
                        }
161

162
                        $this->rulesOfOverrides[$config] = $ofOverrides;
1 ✔
163
                }
164

165
                $ofOverrides = $this->rulesOfOverrides[$config] ?? [];
1 ✔
166

167
                [$phpTarget, $phpVersion] = $this->raiseToMinPhpVersion($php ?? $phpTarget);
1 ✔
168
                $resolver = new DecisionResolver(
1 ✔
169
                        $this->getCatalogue(),
1 ✔
170
                        $phpTarget,
171
                        $this->project,
1 ✔
172
                        typesAnalyzed: $typeAnalysis !== null && $this->typesAvailable,
1 ✔
173
                        certainNames: $resolution === 'certain',
1 ✔
174
                        translator: $this->registry->translator,
1 ✔
175
                );
176
                $decisions = $resolver->resolve($decisionLayers);
1 ✔
177
                if ($packageLayers = $this->collectPackageMaps($decisions, $narrowed)) {
1 ✔
178
                        $decisions = $resolver->resolve([...$packageLayers, ...$decisionLayers]);
1 ✔
179
                }
180

181
                $values = $resolver->createValues($decisions, $narrowed === null ? null : $this->collectSelection($narrowed));
1 ✔
182
                foreach ($this->getCatalogue()->getDecisions() as $path => $decision) {
1 ✔
183
                        // a map the run is narrowed away from is refused where its entries are wrong all the same, no rule reading it
184
                        if (
185
                                $decision->domain instanceof Map
1 ✔
186
                                && $decision->domain->grammar !== null
1 ✔
187
                                && !$values->isKept($path)
1 ✔
188
                                && !$values->isSelected($path)
1 ✔
189
                        ) {
190
                                $values->readMap($path);
1 ✔
191
                        }
192
                }
193

194
                $factories = $config->getRuleFactories() + $this->registry->ruleFactories;
1 ✔
195
                $byRule = $active = $inactive = [];
1 ✔
196
                foreach ($decisions as $path => $decision) {
1 ✔
197
                        foreach ($decision->rules as $class) {
1 ✔
198
                                $byRule[$class][$path] = $decision;
1 ✔
199
                        }
200
                }
201

202
                foreach ($this->getCatalogue()->getRuleOrder() as $class) {
1 ✔
203
                        $resolved = $this->resolveRule(
1 ✔
204
                                $class,
205
                                $byRule[$class] ?? [],
1 ✔
206
                                $resolver->findRuleReason($class),
1 ✔
207
                                $values,
208
                                $phpTarget,
209
                                $factories[$class] ?? null,
1 ✔
210
                                isset($fixRisky[$class]),
1 ✔
211
                                isset($ofOverrides[$class]),
1 ✔
212
                        );
213
                        $resolved->isActive() ? $active[$class] = $resolved : $inactive[$class] = $resolved;
1 ✔
214
                }
215

216
                $this->warnRepeated($repeated);
1 ✔
217
                if ($overrides === []) {
1 ✔
218
                        $narrowed === null
1 ✔
219
                                ? $this->checkFixPolicies($config, $active, $ofOverrides)
1 ✔
220
                                : $this->checkOnly($narrowed, $active, $inactive, $ofOverrides, $use);
1 ✔
221
                }
222

223
                // one entry per symbol, spelled the way the first layer naming it spells it
224
                $bySource = fn(array $entries): array => array_column($entries, 1, 0);
1 ✔
225
                return new ResolvedConfig(
1 ✔
226
                        [...array_values($active), ...array_values($inactive)],
1 ✔
227
                        self::resolveIndent($values->get('indentation.unit')),
1 ✔
228
                        self::resolveLineEnding($values->get('file.lineEnding')),
1 ✔
229
                        $phpVersion,
230
                        $use,
231
                        namespacedFunctions: $bySource($symbols[SymbolKind::Function->name]),
1 ✔
232
                        namespacedConstants: $bySource($symbols[SymbolKind::Constant->name]),
1 ✔
233
                        nameResolution: $resolution ?? 'uncertain',
1 ✔
234
                        lineLength: self::resolveLineLength($values->get('file.lineLength')),
1 ✔
235
                        tabWidth: $values->get('indentation.tabWidth')->getCountRange()[0],
1 ✔
236
                        typeAnalysis: $this->typesAvailable ? $typeAnalysis : null,
1 ✔
237
                        packageTargets: array_diff_key($config->targets, ['php' => true]),
1 ✔
238
                        plugins: array_map(
1 ✔
239
                                fn(string|Plugin $plugin) => is_string($plugin) ? $plugin : $plugin::class,
1 ✔
240
                                [...$config->plugins, ...$commandLine instanceof Config ? $commandLine->plugins : []],
1 ✔
241
                        ),
242
                        suppressionComments: $suppressionComments,
243
                        suppressionExamples: array_filter($suppressionExamples),
1 ✔
244
                        decisions: $decisions,
245
                        values: $values,
246
                        fixRisky: $fixRiskyPaths,
247
                        reportOnly: $reportOnly,
248
                        warningDecisions: self::findWarningDecisions($decisions, array_column($collected, 0), $severities),
1 ✔
249
                        overrides: $resolvedOverrides,
250
                );
251
        }
252

253

254
        /**
255
         * The decisions each of the standards makes, resolved alone.
256
         * @return array<string, array<string, ResolvedDecision>>  standard => path => its decision
257
         * @throws ConfigurationException
258
         */
259
        public function resolveStandards(string $phpTarget): array
260
        {
261
                $standards = [];
1 ✔
262
                foreach (CorePlugin::Standards as $standard) {
1 ✔
263
                        $standards[$standard] = $this->resolve(new Config(use: ["dresscode/$standard"]), $phpTarget)->decisions;
1 ✔
264
                }
265

266
                return $standards;
1 ✔
267
        }
268

269

270
        /**
271
         * The catalogue of the registered rules, as the registry keeps it.
272
         * @throws ConfigurationException
273
         */
274
        public function getCatalogue(): Catalogue
275
        {
276
                return $this->registry->getCatalogue();
1 ✔
277
        }
278

279

280
        /**
281
         * The maps of the upgrading files of the installed packages, and the requirements of the rules of their plugins they
282
         * turn on, as layers under every other one, where `upgrading.packages` is `yes`, or where the requirement laying a
283
         * file is not `keep` (`upgrading.php.deprecatedApi` for the data of PHP). A run narrowed by `only` gets the maps of
284
         * a package or a library it names, and those of the others only where another of its names covers the map, or the
285
         * requirement laying them.
286
         * @param  array<string, ResolvedDecision>  $decisions
287
         * @param  ?list<ExpandedName>  $narrowed
288
         * @return list<array{Layer, array<string, mixed>}>
289
         * @throws ConfigurationException  where a file turns on a rule another plugin or the core brings
290
         */
291
        private function collectPackageMaps(array $decisions, ?array $narrowed): array
292
        {
293
                $named = $covered = [];
1 ✔
294
                foreach ($narrowed ?? [] as $expanded) {
1 ✔
295
                        $expanded->package ? $named[$expanded->name] = true : $covered += array_fill_keys($expanded->paths, true);
1 ✔
296
                }
297

298
                $layers = [];
1 ✔
299
                foreach ($this->upgradingData as $package) {
1 ✔
300
                        $requirement = $package->layer->decision;
1 ✔
301
                        $layer = $package->buildDecisions(function (string $map) use ($package, $requirement, $narrowed, $named, $covered): ?string {
1 ✔
302
                                $path = $this->findDataPath($map);
1 ✔
303
                                if ($path === null && !in_array($package->layer->package, $this->pluginsSkipped, true)) { // the plugin skipped says so
1 ✔
304
                                        $this->warnings[$package->layer->describe() . " $map"] = "Map `$map`, which "
1 ✔
305
                                                . $package->layer->formatReference()
1 ✔
306
                                                . ' sets, is not known to this DressCode; skipped.';
1 ✔
307
                                } elseif (
308
                                        $path !== null
1 ✔
309
                                        && !$this->getCatalogue()->find($path)?->domain instanceof Map
1 ✔
310
                                        && !$this->isOfPackagePlugin($path, $package->layer->package)
1 ✔
311
                                ) {
312
                                        throw new ConfigurationException(
1 ✔
313
                                                $package->layer->formatReference() . " turns on `$path`, which is no rule of the plugin of its package.",
1 ✔
314
                                        );
315
                                }
316

317
                                return $path !== null
1 ✔
318
                                        && ($narrowed === null
1 ✔
319
                                                || isset($named[$package->layer->package ?? ''])
1 ✔
320
                                                || isset($named[$package->package])
1 ✔
321
                                                || isset($covered[$path])
1 ✔
322
                                                || isset($covered[$requirement ?? '']))
1 ✔
323
                                        ? $path
1 ✔
324
                                        : null;
1 ✔
325
                        });
1 ✔
326
                        $laid = $requirement === null
1 ✔
327
                                ? ($decisions[self::PackagesDecision] ?? null)?->value->getFlag()
1 ✔
328
                                : ($decisions[$requirement] ?? null)?->value->isKept() === false;
1 ✔
329
                        if ($layer !== [] && $laid) {
1 ✔
330
                                $layers[] = [$package->layer, $layer];
1 ✔
331
                        }
332
                }
333

334
                return $layers;
1 ✔
335
        }
336

337

338
        /** Whether the decision is one of the rules of the plugin the package ships. */
339
        private function isOfPackagePlugin(string $path, ?string $package): bool
340
        {
341
                $section = $package === null ? null : $this->registry->findPackageSection($package);
1 ✔
342
                return $section !== null && str_starts_with($path, "$section.");
1 ✔
343
        }
344

345

346
        /**
347
         * The decision a key of a section of an upgrading file sets: a map of `upgrading.api` by its name, a map of a plugin
348
         * or a requirement it turns on by its path.
349
         */
350
        private function findDataPath(string $key): ?string
351
        {
352
                $decision = $this->getCatalogue()->find($key);
1 ✔
353
                return match (true) {
354
                        $this->getCatalogue()->find("upgrading.api.$key")?->domain instanceof Map => "upgrading.api.$key",
1 ✔
355
                        $decision?->domain instanceof Map, $decision?->isRequirement() === true => $key,
1 ✔
356
                        default => null,
1 ✔
357
                };
358
        }
359

360

361
        /**
362
         * A rule runs where one of its requirements or facts takes effect and lies in the mask of the run; otherwise it says
363
         * why it does not. A preset may decide what cannot run here; a project deciding it asked for what it cannot get,
364
         * which is an error for the types and a warning for the target and for a package, unless another rule of the
365
         * decision runs.
366
         * @param  class-string<Rule>  $class
367
         * @param  array<string, ResolvedDecision>  $decisions  those the rule declares
368
         * @param  ?InactiveReason  $ruleReason  why the rule does not run whatever its values
369
         * @param  ?\Closure(): Rule  $factory
370
         * @throws ConfigurationException
371
         */
372
        private function resolveRule(
373
                string $class,
374
                array $decisions,
375
                ?InactiveReason $ruleReason,
376
                Values $values,
377
                string $phpTarget,
378
                ?\Closure $factory,
379
                bool $fixRisky,
380
                bool $ofOverride,
381
        ): ResolvedRule
382
        {
383
                $own = array_filter($decisions, fn(ResolvedDecision $decision) => $decision->decision->kind !== DecisionKind::Parameter);
1 ✔
384
                $effective = $ruleReason === null ? array_filter($own, fn(ResolvedDecision $decision) => $decision->inactive === null) : [];
1 ✔
385
                $asked = array_filter($own, function (ResolvedDecision $decision): bool {
1 ✔
386
                        $top = $decision->findTopLayer();
1 ✔
387
                        return $top?->origin?->isProject() === true && !$top->isKept() && $decision->inactive !== null;
1 ✔
388
                });
1 ✔
389
                // a value a group gives a decision its target leaves out is skipped silently, as one of a preset; the types never are
390
                $named = array_keys(array_filter($asked, fn(ResolvedDecision $decision) => !$decision->cascaded));
1 ✔
391
                $asked = array_keys($asked);
1 ✔
392
                $info = RuleInfo::fromRule($class);
1 ✔
393
                if ($asked !== [] && $ruleReason === InactiveReason::Types) {
1 ✔
394
                        throw new ConfigurationException($this->typesAvailable
1 ✔
395
                                ? "Decision `$asked[0]` needs the types of the code; set `typeAnalysis: phpstan` in the configuration and install `phpstan/phpstan` beside DressCode."
1 ✔
396
                                : "Decision `$asked[0]` needs the types of the code, but `phpstan/phpstan` is not installed beside DressCode.", docs: 'types#enable');
1 ✔
397
                } elseif ($named !== [] && $ruleReason === InactiveReason::Php) {
1 ✔
398
                        $this->warnings[$class] = "Decision `$named[0]` needs PHP {$info->requires['php']} and the target is $phpTarget; skipped.";
1 ✔
399
                } elseif ($named !== [] && $ruleReason === InactiveReason::Package) {
1 ✔
400
                        $this->warnings[$class] = "Decision `$named[0]` needs "
1 ✔
401
                                . ($this->project->findUnmetRequirement($info->getRequiredPackages()) ?? throw new \LogicException)->format()
1 ✔
402
                                . '; skipped.';
1 ✔
403
                }
404

405
                [$reason, $message] = match (true) {
1 ✔
406
                        $effective !== []
407
                                && array_any($effective, fn(ResolvedDecision $decision) => $values->isSelected($decision->decision->path)) => [
1 ✔
408
                                        null,
1 ✔
409
                                        null,
410
                                ],
411
                        $effective !== [] => [InactiveReason::Narrowed, 'the run is narrowed to other decisions'],
1 ✔
412
                        $ruleReason === InactiveReason::Php => [
1 ✔
413
                                InactiveReason::Php,
1 ✔
414
                                "it needs PHP {$info->requires['php']} and the target is $phpTarget",
1 ✔
415
                        ],
416
                        $ruleReason === InactiveReason::Package => [InactiveReason::Package, 'it needs a package the project does not have'],
1 ✔
417
                        $ruleReason === InactiveReason::Types
1 ✔
418
                                && array_any(
1 ✔
419
                                        $own,
420
                                        fn(ResolvedDecision $decision) => $decision->decision->isRequirement() && !$decision->value->isKept(),
1 ✔
421
                                ) => [InactiveReason::Types, $this->typesAvailable
1 ✔
422
                                        ? 'it needs the types of the code and the configuration sets no types'
1 ✔
423
                                        : 'it needs the types of the code and phpstan/phpstan is not installed beside DressCode'],
1 ✔
424
                        array_any($own, fn(ResolvedDecision $decision) => $decision->layers !== []) => [
1 ✔
425
                                InactiveReason::TurnedOff,
1 ✔
426
                                'its decisions are `keep`',
1 ✔
427
                        ],
428
                        $ofOverride => [InactiveReason::OnlyOverride, 'only an override turns it on'],
1 ✔
429
                        default => [InactiveReason::NotMentioned, 'no preset or layer of the configuration names its decisions'],
1 ✔
430
                };
431
                // who said the decisive value: of a decision that takes effect, or of the last one turned off
432
                $said = $reason === null ? $effective : array_filter($own, fn(ResolvedDecision $decision) => $decision->layers !== []);
1 ✔
433
                $top = array_values($said)[0]->layers ?? [];
1 ✔
434
                $source = $top === [] ? null : $top[count($top) - 1]->origin;
1 ✔
435
                return new ResolvedRule($class, $source, $reason, $message, $factory, $fixRisky);
1 ✔
436
        }
437

438

439
        /**
440
         * The severity a layer says of each requirement its names cover, the most specific name winning: a decision, a
441
         * deeper group, a shallower one, a section, the class of a rule, a preset.
442
         * @param  array<string, string>  $names  name => `warning` or `error`
443
         * @return array<string, array{int, string}>  path => the rank of the name and the severity
444
         * @throws ConfigurationException
445
         */
446
        private function collectSeverity(array $names): array
447
        {
448
                $severity = $by = [];
1 ✔
449
                foreach ($names as $name => $value) {
1 ✔
450
                        $expanded = $this->expandName($name);
1 ✔
451
                        $paths = array_filter(
1 ✔
452
                                $expanded->paths,
1 ✔
453
                                fn(string $path) => $this->getCatalogue()->find($path)?->kind !== DecisionKind::Parameter,
1 ✔
454
                        );
455
                        if ($paths === []) {
1 ✔
456
                                throw new ConfigurationException("Name `$name` in `severity` covers no decision that reports; a parameter has no severity of its own.");
1 ✔
457
                        }
458

459
                        $rank = match (true) {
1 ✔
460
                                $expanded->preset => 0,
1 ✔
461
                                $expanded->rule !== null => 1,
1 ✔
462
                                $this->getCatalogue()->find($name) !== null => PHP_INT_MAX,
1 ✔
463
                                default => 1 + count(explode('.', $expanded->name)),
1 ✔
464
                        };
465
                        foreach ($paths as $path) {
1 ✔
466
                                [$was, $wasValue] = $severity[$path] ?? [-1, null];
1 ✔
467
                                if ($was === $rank && $wasValue !== $value) {
1 ✔
468
                                        throw new ConfigurationException("Names `{$by[$path]}` and `$name` in `severity` say two severities of `$path`; name it alone.");
1 ✔
469
                                } elseif ($was < $rank) {
1 ✔
470
                                        $severity[$path] = [$rank, $value];
1 ✔
471
                                        $by[$path] = $name;
1 ✔
472
                                }
473
                        }
474
                }
475

476
                return $severity;
1 ✔
477
        }
478

479

480
        /**
481
         * The requirements whose violations only warn: a severity holds for a decision whose value the layer saying it, or
482
         * one below it, said, so a layer writing the value anew without a severity makes it an error again.
483
         * @param  array<string, ResolvedDecision>  $decisions
484
         * @param  list<Layer>  $layers  from the lowest
485
         * @param  list<array<string, array{int, string}>>  $severities  of each layer, by path
486
         * @return array<string, true>
487
         */
488
        private static function findWarningDecisions(array $decisions, array $layers, array $severities): array
489
        {
490
                $warnings = [];
1 ✔
491
                foreach ($decisions as $path => $decision) {
1 ✔
492
                        $top = $decision->findTopLayer()?->origin;
1 ✔
493
                        $from = $top === null ? 0 : max(0, (int) array_search($top, $layers, strict: true));
1 ✔
494
                        for ($i = count($layers) - 1; $i >= $from; $i--) {
1 ✔
495
                                if (isset($severities[$i][$path])) {
1 ✔
496
                                        if ($severities[$i][$path][1] === 'warning') {
1 ✔
497
                                                $warnings[$path] = true;
1 ✔
498
                                        }
499

500
                                        break;
1 ✔
501
                                }
502
                        }
503
                }
504

505
                return $warnings;
1 ✔
506
        }
507

508

509
        /**
510
         * The paths the decisions of the run are narrowed to, those the names of `only` stand for.
511
         * @param  list<ExpandedName>  $narrowed
512
         * @return list<string>
513
         */
514
        private function collectSelection(array $narrowed): array
515
        {
516
                return array_values(array_unique(array_merge(...array_map(fn(ExpandedName $name) => $name->paths, $narrowed))));
1 ✔
517
        }
518

519

520
        /**
521
         * The decisions a name of `suppressionComments` silences.
522
         * @return list<string>
523
         * @throws ConfigurationException
524
         */
525
        private function resolveSuppressedName(string $name): array
526
        {
527
                return $this->registry->expandSuppressedName($name)
1 ✔
528
                        ?: throw new ConfigurationException("`suppressionComments` names `$name`, which is no decision, section or rule.");
1 ✔
529
        }
530

531

532
        /**
533
         * Adds the functions and the constants the namespaces of a layer declare, each spelled the way the first layer
534
         * naming it spells it.
535
         * @param  array<string, array<string, array{string, string}>>  $symbols
536
         * @param  array{functions: list<string>, constants: list<string>}  $namespaces
537
         */
538
        private static function addSymbols(array &$symbols, array $namespaces, string $layer): void
539
        {
540
                foreach ([[SymbolKind::Function, 'functions'], [SymbolKind::Constant, 'constants']] as [$kind, $key]) {
1 ✔
541
                        foreach ($namespaces[$key] as $name) {
1 ✔
542
                                $symbols[$kind->name][self::toSymbolKey($kind, $name)] ??= [$name, $layer];
1 ✔
543
                        }
544
                }
545
        }
1 ✔
546

547

548
        /**
549
         * The key two spellings of one symbol share, the way PHP reads them: a function in any letter case, a constant
550
         * in any letter case of its namespace but not of its own name.
551
         */
552
        public static function toSymbolKey(SymbolKind $kind, string $name): string
553
        {
554
                $name = ltrim($name, '\\');
1 ✔
555
                $pos = (int) strrpos($name, '\\');
1 ✔
556
                return $kind === SymbolKind::Constant
1 ✔
557
                        ? strtolower(substr($name, 0, $pos)) . substr($name, $pos)
1 ✔
558
                        : strtolower($name);
1 ✔
559
        }
560

561

562
        /**
563
         * The versions the code is checked for and the lowest of them, the one a rule asks about: the target, or the oldest
564
         * DressCode fixes code for, with a warning.
565
         * @return array{string, string}
566
         */
567
        private function raiseToMinPhpVersion(string $target): array
568
        {
569
                $version = Versions::findLowestVersion($target);
1 ✔
570
                if ($version !== null && version_compare($version, Config::MinPhpVersion, '>=')) {
1 ✔
571
                        return [$target, $version];
1 ✔
572
                }
573

574
                $this->warnings['php'] = 'The target PHP ' . ($version ?? $target) . ' is older than PHP ' . Config::MinPhpVersion . ', the oldest DressCode fixes code for;'
1 ✔
575
                        . ' the code is checked as PHP ' . Config::MinPhpVersion . ', so a fix may write syntax the target does not have.';
1 ✔
576
                return [Config::MinPhpVersion, Config::MinPhpVersion];
1 ✔
577
        }
578

579

580
        /** The characters of a level of indentation the unit names, a tab where it is `keep`. */
581
        private static function resolveIndent(Value $unit): string
582
        {
583
                return $unit->isKept() || $unit->getWord() === 'tab' ? "\t" : str_repeat(' ', (int) $unit->getWord());
1 ✔
584
        }
585

586

587
        /** The line ending the decision names; null for the one each file uses most. */
588
        private static function resolveLineEnding(Value $lineEnding): ?string
589
        {
590
                return match ($lineEnding->isKept() ? null : $lineEnding->getWord()) {
1 ✔
591
                        'LF' => "\n",
1 ✔
592
                        'CRLF' => "\r\n",
1 ✔
593
                        default => null,
1 ✔
594
                };
595
        }
596

597

598
        /** The widest line, null for none. */
599
        private static function resolveLineLength(Value $length): ?int
600
        {
601
                return $length->content === 'none' ? null : $length->getCountRange()[0];
1 ✔
602
        }
603

604

605
        /**
606
         * The profiles that decide for a file matching the overrides, lowest first, each with the name an error or an origin
607
         * is told by.
608
         * @param  list<int>  $overrides
609
         * @return list<array{Layer, Profile}>
610
         */
611
        private static function listProfiles(Config $config, array $overrides, ?Profile $commandLine = null): array
612
        {
613
                $profiles = [[new Layer(LayerKind::Configuration), $config]];
1 ✔
614
                foreach ($overrides as $index) {
1 ✔
615
                        $profiles[] = [
1 ✔
616
                                new Layer(LayerKind::Override, implode(', ', $config->overrides[$index]->paths)),
1 ✔
617
                                $config->overrides[$index]->profile,
1 ✔
618
                        ];
619
                }
620

621
                if ($commandLine !== null) {
1 ✔
622
                        $profiles[] = [new Layer(LayerKind::CommandLine), $commandLine];
1 ✔
623
                }
624

625
                return $profiles;
1 ✔
626
        }
627

628

629
        /**
630
         * The profiles as layers, lowest first: each of them above what it uses, in the order it names them, and a preset
631
         * where it is named first; what a profile names that is already laid, it names for nothing.
632
         * @param  list<array{Layer, Profile}>  $profiles  who says it and what it says
633
         * @return array{layers: list<array{Layer, Profile}>, repeated: list<array{Layer, Layer, Layer}>}  the layers, each
634
         *   who says it and what it says; and every entry named for nothing, as its layer, the one that laid it and the one
635
         *   that named it again
636
         * @throws ConfigurationException
637
         */
638
        private function collectLayers(array $profiles): array
639
        {
640
                $layers = $visited = $repeated = [];
1 ✔
641
                foreach ($profiles as [$layer, $profile]) {
1 ✔
642
                        $this->collectUsed($profile, $layer, $layers, $visited, $repeated);
1 ✔
643
                        if ($layer->kind === LayerKind::Configuration && $this->laid !== []) {
1 ✔
644
                                $this->collectUsed(new Profile(use: $this->laid), new Layer(LayerKind::Caller), $layers, $visited, $repeated);
1 ✔
645
                        }
646

647
                        $layers[] = [$layer, $profile];
1 ✔
648
                }
649

650
                return ['layers' => $layers, 'repeated' => $repeated];
1 ✔
651
        }
652

653

654
        /**
655
         * Lays the presets the profile uses, each above what it uses in turn, once, where it is named first; a preset
656
         * that comes back to itself through what it uses is an error, unlike one named again by another layer, which
657
         * does nothing.
658
         * @param  list<array{Layer, Profile}>  $layers
659
         * @param  array<string, Layer>  $visited  the name of a preset => the layer that laid it
660
         * @param  list<array{Layer, Layer, Layer}>  $repeated
661
         * @throws ConfigurationException
662
         */
663
        private function collectUsed(Profile $profile, Layer $source, array &$layers, array &$visited, array &$repeated): void
664
        {
665
                foreach ($profile->use as $entry) {
1 ✔
666
                        if ($this->registry->isPluginPackage($entry)) { // it lets the plugin of the package in, which is no layer
1 ✔
667
                                continue;
1 ✔
668
                        }
669

670
                        try {
671
                                // a preset by its name, or a file of the same shape by its path, which is its name too
672
                                $name = str_ends_with($entry, '.neon')
1 ✔
673
                                        ? NeonReader::resolvePresetFile($entry, $this->root)
1 ✔
674
                                        : $this->registry->resolvePreset($entry);
1 ✔
675
                                $file = $this->registry->presets[$name] ?? $name;
1 ✔
676
                        } catch (ConfigurationException $e) {
1 ✔
677
                                throw self::locate($e, $source);
1 ✔
678
                        } catch (\InvalidArgumentException $e) {
1 ✔
679
                                throw self::locate(new ConfigurationException($e->getMessage(), previous: $e), $source);
1 ✔
680
                        }
681

682
                        $layer = new Layer(LayerKind::Preset, $name);
1 ✔
683
                        if (in_array($name, $this->using, true)) {
1 ✔
684
                                $cycle = implode('`, `', array_slice($this->using, array_search($name, $this->using, strict: true) + 1));
1 ✔
685
                                throw new ConfigurationException(($file === $name ? 'Preset file' : 'Preset') . " `$name` uses itself through `$cycle`; a preset cannot come back to itself.");
1 ✔
686
                        } elseif (isset($visited[$name])) {
1 ✔
687
                                $repeated[] = [$layer, $visited[$name], $source];
1 ✔
688
                                continue;
1 ✔
689
                        }
690

691
                        $visited[$name] = $source;
1 ✔
692
                        $preset = $this->presetFiles[$file] ??= NeonReader::readPreset($file, $file === $name ? null : $name);
1 ✔
693
                        $this->using[] = $name;
1 ✔
694
                        try {
695
                                $this->collectUsed($preset, $layer, $layers, $visited, $repeated);
1 ✔
696
                        } finally {
1 ✔
697
                                array_pop($this->using);
1 ✔
698
                        }
699

700
                        $layers[] = [$layer, $preset];
1 ✔
701
                }
702
        }
1 ✔
703

704

705
        /**
706
         * An entry named again where a layer below already laid it does nothing, and the run says so.
707
         * @param  list<array{Layer, Layer, Layer}>  $repeated
708
         */
709
        private function warnRepeated(array $repeated): void
710
        {
711
                foreach ($repeated as [$layer, $laidBy, $namedBy]) {
1 ✔
712
                        // what a preset or the caller lays is not the project's to change, and the command line reaches what an override
713
                        // does not
714
                        if (
715
                                $namedBy->kind !== LayerKind::Preset
1 ✔
716
                                && $namedBy->kind !== LayerKind::Caller
1 ✔
717
                                && $laidBy->kind !== LayerKind::Caller
1 ✔
718
                                && !($laidBy->kind === LayerKind::Override && $namedBy->kind === LayerKind::CommandLine)
1 ✔
719
                        ) {
720
                                $this->warnings['repeated ' . $layer->describe() . ' ' . $namedBy->describe()] = ucfirst($namedBy->formatReference())
1 ✔
721
                                        . ' uses ' . $layer->formatReference()
1 ✔
722
                                        . ', which ' . $laidBy->formatReference() . ' already brings; the entry does nothing.';
1 ✔
723
                        }
724
                }
725
        }
1 ✔
726

727

728
        /** An error said in a layer the reader has to be sent to, named in front of it; the configuration needs no pointing at. */
729
        private static function locate(ConfigurationException $e, Layer $layer): ConfigurationException
730
        {
731
                return $layer->kind === LayerKind::Configuration
1 ✔
732
                        ? $e
1 ✔
733
                        : new ConfigurationException(ucfirst($layer->formatReference()) . ": {$e->getMessage()}", $e->docs, $e);
1 ✔
734
        }
735

736

737
        /**
738
         * The rules a run narrowed by `only` keeps, per name it was given.
739
         * @param  list<string>  $names
740
         * @return list<ExpandedName>
741
         * @throws ConfigurationException
742
         */
743
        private function resolveOnly(array $names): array
744
        {
745
                return array_map($this->expandName(...), $names);
1 ✔
746
        }
747

748

749
        /**
750
         * What a name of `only`, `fixRisky` or `severity` stands for: a decision or a section for the decisions under it,
751
         * a package of upgrading data for what its files set, a library for what the files about it set, both for a name of
752
         * both, a rule for its own, a preset for every decision it and its parents make, each with the rules owning them. A
753
         * section named like a preset is written `section.*`, the bare name being refused as both.
754
         * @throws ConfigurationException
755
         */
756
        private function expandName(string $name): ExpandedName
757
        {
758
                $package = $this->findDataPackage($name);
1 ✔
759
                if ($package !== null) {
1 ✔
760
                        $paths = [];
1 ✔
761
                        foreach ($this->upgradingData as $data) {
1 ✔
762
                                if ($data->layer->package === $package || $data->package === $package) {
1 ✔
763
                                        $keys = [...array_keys($data->maps), ...array_keys($data->requirements)];
1 ✔
764
                                        array_push($paths, ...array_filter(array_map($this->findDataPath(...), $keys)));
1 ✔
765
                                }
766
                        }
767

768
                        $paths = array_values(array_unique($paths));
1 ✔
769
                        $rules = array_merge(...array_map($this->getCatalogue()->getRulesOf(...), $paths));
1 ✔
770
                        return new ExpandedName($package, false, null, array_values(array_unique($rules)), $paths, package: true);
1 ✔
771
                }
772

773
                $section = str_ends_with($name, '.*') ? substr($name, 0, -2) : null;
1 ✔
774
                $paths = array_keys($this->getCatalogue()->getDecisionsUnder($section ?? $name));
1 ✔
775
                $preset = $paths !== [] && $section === null && !str_contains($name, '.') ? $this->registry->findPreset($name) : null;
1 ✔
776
                if ($preset !== null) {
1 ✔
777
                        throw new ConfigurationException("Name `$name` is both a section of decisions and the preset `$preset`; write `$name.*` for the section or `$preset` for the preset.");
1 ✔
778
                }
779

780
                $preset = null;
1 ✔
781
                if ($paths === [] && $section !== null) {
1 ✔
782
                        throw new ConfigurationException("Unknown section `$section`.");
1 ✔
783
                } elseif ($paths === [] && in_array($name, $this->registry->rules, true)) {
1 ✔
784
                        $requirements = array_filter(
1 ✔
785
                                $this->getCatalogue()->getDecisionsOf($name),
1 ✔
786
                                fn(Decision $decision) => $decision->kind !== DecisionKind::Parameter,
1 ✔
787
                        );
788
                        return new ExpandedName($name, false, $name, [$name], array_keys($requirements));
1 ✔
789
                } elseif ($paths === [] && class_exists($name) && is_subclass_of($name, Rule::class)) {
1 ✔
790
                        throw new ConfigurationException("Rule `$name` is not registered; add it to `rules` of the configuration.");
1 ✔
791
                } elseif ($paths === []) {
1 ✔
792
                        $preset = $this->registry->findPreset($name) ?? throw $this->registry->createUnknownNameException($name);
1 ✔
793
                        foreach ($this->collectLayers([[new Layer(LayerKind::Caller), new Profile(use: [$preset])]])['layers'] as [, $profile]) {
1 ✔
794
                                array_push($paths, ...$this->collectPaths($profile->decisions));
1 ✔
795
                        }
796
                }
797

798
                $paths = array_values(array_unique($paths));
1 ✔
799
                $rules = array_merge(...array_map($this->getCatalogue()->getRulesOf(...), $paths));
1 ✔
800
                return new ExpandedName($preset ?? $section ?? $name, $preset !== null, null, array_values(array_unique($rules)), $paths);
1 ✔
801
        }
802

803

804
        /**
805
         * The full name of the installed package the name stands for, one shipping upgrading files or one they are about,
806
         * one of the core without its vendor too.
807
         */
808
        private function findDataPackage(string $name): ?string
809
        {
810
                $names = [$name, PluginRegistry::qualify($name)];
1 ✔
811
                foreach ($this->upgradingData as $data) {
1 ✔
812
                        if ($data->layer->name !== null && in_array($data->layer->package, $names, true)) {
1 ✔
813
                                return $data->layer->package;
1 ✔
814
                        } elseif (in_array($data->package, $names, true)) {
1 ✔
815
                                return $data->package;
1 ✔
816
                        }
817
                }
818

819
                return null;
1 ✔
820
        }
821

822

823
        /**
824
         * The paths of the decisions a layer makes, every one under a structure it makes as a whole.
825
         * @param  array<mixed>  $tree
826
         * @return list<string>
827
         */
828
        private function collectPaths(array $tree, string $prefix = ''): array
829
        {
830
                $paths = [];
1 ✔
831
                foreach ($tree as $key => $value) {
1 ✔
832
                        $path = $prefix . $key;
1 ✔
833
                        if ($this->getCatalogue()->find($path) !== null) {
1 ✔
834
                                $paths[] = $path;
1 ✔
835
                        } elseif (is_array($value)) {
1 ✔
836
                                array_push($paths, ...$this->collectPaths($value, "$path."));
1 ✔
837
                        } else {
838
                                array_push($paths, ...array_keys($this->getCatalogue()->getDecisionsUnder($path)));
1 ✔
839
                        }
840
                }
841

842
                return $paths;
1 ✔
843
        }
844

845

846
        /**
847
         * A name of `only` that lets in nothing that runs would make a run that checks nothing and says it is clean, and
848
         * a section or a preset of which only some rules run here would make one that checks less than it says; a rule that
849
         * runs only where an override enables it counts as running.
850
         * @param  list<ExpandedName>  $narrowed
851
         * @param  array<class-string<Rule>, ResolvedRule>  $active
852
         * @param  array<class-string<Rule>, ResolvedRule>  $inactive
853
         * @param  array<class-string<Rule>, true>  $ofOverrides
854
         * @param  list<string>  $use  the presets the run lays
855
         * @throws ConfigurationException
856
         */
857
        private function checkOnly(array $narrowed, array $active, array $inactive, array $ofOverrides, array $use): void
858
        {
859
                $skipped = [];
1 ✔
860
                foreach ($narrowed as $expanded) {
1 ✔
861
                        [$class, $rules] = [$expanded->rule, $expanded->rules];
1 ✔
862
                        $running = array_filter($rules, fn(string $rule) => isset($active[$rule]) || isset($ofOverrides[$rule]));
1 ✔
863
                        // a name whose rules the project cannot run, too new for its target or for its packages, is skipped, so that
864
                        // one command of `--only` serves projects of several targets
865
                        if (
866
                                !$running
1 ✔
867
                                && $rules !== []
1 ✔
868
                                && array_all($rules, fn(string $rule) => in_array(
1 ✔
869
                                        $inactive[$rule]->inactiveReason ?? null,
1 ✔
870
                                        [InactiveReason::Php, InactiveReason::Package],
1 ✔
871
                                        true,
1 ✔
872
                                ))
1 ✔
873
                        ) {
874
                                $skipped[$expanded->name] = [
1 ✔
875
                                        'Option `--only` names ' . ($class === null ? $expanded->format() : "rule `$class`")
1 ✔
876
                                        . ", which cannot run here: {$inactive[$rules[array_key_first($rules)]]->inactiveMessage}",
1 ✔
877
                                        $rules,
1 ✔
878
                                ];
879
                                continue;
1 ✔
880
                        }
881

882
                        if ($class !== null) {
1 ✔
883
                                if (!$running) {
1 ✔
884
                                        throw new ConfigurationException("Option `--only` names rule `$class`, which cannot run here: {$inactive[$class]->inactiveMessage}.");
1 ✔
885
                                }
886

887
                                continue;
1 ✔
888
                        }
889

890
                        // left out is what the project turned off, and where it does not use the name what the name would bring; a rule
891
                        // its standard turns off or that cannot run here, too new for the target or needing the types, is not
892
                        $collective = PluginRegistry::abbreviate($expanded->name);
1 ✔
893
                        $laid = in_array($collective, array_map(PluginRegistry::abbreviate(...), $use), true);
1 ✔
894
                        $left = array_filter($rules, fn(string $rule) => match ($inactive[$rule]->inactiveReason ?? null) {
1 ✔
895
                                InactiveReason::TurnedOff => $inactive[$rule]->source?->isProject() === true,
1 ✔
896
                                InactiveReason::NotMentioned => !$laid,
1 ✔
897
                                default => false,
1 ✔
898
                        });
1 ✔
899
                        // a preset the configuration does not use is brought by `--use`, a decision it does not make by `--set`
900
                        $hint = match (true) {
1 ✔
901
                                $expanded->preset => "`--use $collective --only $collective` runs all its rules",
1 ✔
902
                                $expanded->package
1 ✔
903
                                        => 'its upgrading files reach no version of a library the project has, or `upgrading.packages` is not `yes`',
1 ✔
904
                                $this->getCatalogue()->find($collective) !== null => "`--set $collective=<value>` decides it for the run",
1 ✔
905
                                default => '`--set <path>=<value>` decides one of them for the run',
1 ✔
906
                        };
907
                        if (!$running) {
1 ✔
908
                                throw new ConfigurationException('Option `--only` names ' . $expanded->format() . ', which has no rule that runs here' . ($laid ? '.' : "; $hint."));
1 ✔
909
                        } elseif ($left) {
1 ✔
910
                                // a rule left out is named by its requirements, which is what the user writes
911
                                $names = array_map(fn(string $path) => "`$path`", array_keys(array_filter(
1 ✔
912
                                        array_merge(...array_map(fn(string $rule) => $this->getCatalogue()->getDecisionsOf($rule), array_values($left))),
1 ✔
913
                                        fn(Decision $decision) => $decision->kind !== DecisionKind::Parameter,
1 ✔
914
                                )));
915
                                $this->warnings["only $expanded->name"] = 'Option `--only` keeps ' . count($running) . ' of the ' . (count(
1 ✔
916
                                        $running,
1 ✔
917
                                ) + count(
918
                                        $left,
1 ✔
919
                                ))
920
                                        . ' rules of ' . $expanded->format() . ' that may run here'
1 ✔
921
                                        . ($laid || !$expanded->preset
1 ✔
922
                                                ? ', without ' . implode(', ', array_slice($names, 0, 5)) . (count($names) > 5
1 ✔
923
                                                        ? ' and ' . (count($names) - 5) . ' more'
1 ✔
924
                                                        : '')
1 ✔
925
                                                        . ($laid ? ', which the configuration turns off.' : ", which the configuration does not decide; $hint.")
1 ✔
926
                                                : "; $hint.");
1 ✔
927
                        }
928
                }
929

930
                if ($skipped !== [] && count($skipped) === count($narrowed)) {
1 ✔
931
                        throw new ConfigurationException(reset($skipped)[0] . '.');
1 ✔
932
                }
933

934
                foreach ($skipped as $name => [$message, $rules]) {
1 ✔
935
                        // a rule whose decision the project wrote is said to be skipped already
936
                        if (!array_all($rules, fn(string $rule) => isset($this->warnings[$rule]))) {
1 ✔
937
                                $this->warnings["only $name"] = "$message; it is skipped.";
1 ✔
938
                        }
939
                }
940
        }
1 ✔
941

942

943
        /**
944
         * A name in fixRisky or reportOnly whose rules run nowhere, not even where an override turns them on, makes the entry
945
         * a line that does nothing, which is worth a warning.
946
         * @param  array<class-string<Rule>, ResolvedRule>  $active
947
         * @param  array<class-string<Rule>, true>  $ofOverrides
948
         * @throws ConfigurationException
949
         */
950
        private function checkFixPolicies(Config $config, array $active, array $ofOverrides): void
951
        {
952
                foreach (self::listProfiles($config, array_keys($config->overrides)) as [$layer, $profile]) {
1 ✔
953
                        foreach (['fixRisky' => $profile->fixRisky, 'reportOnly' => $profile->reportOnly] as $key => $entries) {
1 ✔
954
                                foreach ($entries as $entry) {
1 ✔
955
                                        try {
956
                                                $expanded = $this->expandName($entry);
1 ✔
UNCOV
957
                                        } catch (ConfigurationException $e) {
×
UNCOV
958
                                                throw self::locate($e, $layer);
×
959
                                        }
960

961
                                        if (!array_filter($expanded->rules, fn(string $class) => isset($active[$class]) || isset($ofOverrides[$class]))) {
1 ✔
962
                                                $this->warnings["$key $expanded->name"] = ucfirst($expanded->format())
1 ✔
963
                                                        . " is named in `$key` but runs nowhere; the entry does nothing.";
1 ✔
964
                                        }
965
                                }
966
                        }
967
                }
968
        }
1 ✔
969
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc