From b3927732bc2a33e2f4883a603c09e0f7d7d3813b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 10 Oct 2026 06:38:51 +0200 Subject: [PATCH 01/13] opened 1.4-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2d75b8b5a..3350de332 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } } } From 4cba03a2cde4d99d0c7fc35f994977777e254adf Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 12 Oct 2026 07:10:45 +0200 Subject: [PATCH 02/13] presets: symfony, and the functions of the container configurator of Symfony among those the packages bring --- AGENTS.md | 2 +- composer.json | 2 +- docs/internals.md | 8 +- docs/reference/decisions.md | 868 +++++++++--------- docs/reference/generate.php | 4 +- docs/reference/presets.md | 5 +- src/Config/ConfigResolver.php | 2 +- src/Config/CorePlugin.php | 3 +- src/Config/Layer.php | 3 +- src/Config/PackageDiscovery.php | 26 +- src/Config/UpgradingData.php | 6 +- src/Config/enums.php | 2 +- src/Presets/symfony.neon | 151 +++ tests/DressCode/Config/ConfigResolver.phpt | 8 +- tests/DressCode/Config/PackageDiscovery.phpt | 46 +- tests/DressCode/Config/PluginRegistry.phpt | 5 +- tests/DressCode/Console/explain.phpt | 5 +- .../Presets/fixtures/symfony/clean.code | 61 ++ .../Presets/fixtures/symfony/per.code | 38 + tests/DressCode/Presets/sets.phpt | 9 +- tests/DressCode/Presets/symfony.phpt | 126 +++ tests/DressCode/Rules/robustness.provider.php | 1 + 22 files changed, 897 insertions(+), 484 deletions(-) create mode 100644 src/Presets/symfony.neon create mode 100644 tests/DressCode/Presets/fixtures/symfony/clean.code create mode 100644 tests/DressCode/Presets/fixtures/symfony/per.code create mode 100644 tests/DressCode/Presets/symfony.phpt diff --git a/AGENTS.md b/AGENTS.md index 16c562bed..45f6b642d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ DressCode is a PHP code style checker and fixer built on a **lossless concrete s The tree itself is the `phpsyntax/phpsyntax` library (namespace `PhpSyntax`), developed in a repository of its own. This one holds `DressCode` (`src/`): engine, rules API, the library of generic rules, configuration, CLI. -Rules use only the public API of `PhpSyntax`; whatever a rule in DressCode needs from it is public API for plugins too. What the rules only share among themselves (`Rules\NodeHelpers`, `Rules\Compiler` and the helpers beside the rules that carry `@internal`) is `@internal`; the grammar of a map of members (`Upgrading\MemberMapGrammar`, the map itself `MemberMap` with `findAccess()`, its `MemberMapEntry` and `MemberMapMatch`, `MemberPattern`, `ArgumentPattern`, `FunctionPattern`) is not, a rule shipped by a package being fed the same way, and neither is `GlobalCalls`, which answers which global function a call calls and why that may be another one, nor `CodeWriter`, which writes code in the shape of the file (a class written the way the file reaches it, an import added the way the file writes its imports) for a rule of a package as for one of the core. Presets define style; DressCode has no style of its own. The three standards carry one: `dresscode/perCs` (PER Coding Style 3.1), `dresscode/psr12` and `dresscode/nette` (the Nette Coding Standard, PER with tabs, the layout of the Nette libraries and their departures, which the repository itself is checked with). What a project wants beyond the looks of its code comes in a set, a preset of one intent that decides nothing about the layout and is used beside the standard (`modernizations`, `deprecations`, `cleanup`, `correctness`, `types`, `compilerOptimizations`); a decision the core gains goes into its set, one that writes a newer construct and the replacement of what PHP deprecated alike into every set whose intent it meets (`upgrading.syntax.firstClassCallables` in `modernizations` and `deprecations`), and what no set holds the project decides itself. +Rules use only the public API of `PhpSyntax`; whatever a rule in DressCode needs from it is public API for plugins too. What the rules only share among themselves (`Rules\NodeHelpers`, `Rules\Compiler` and the helpers beside the rules that carry `@internal`) is `@internal`; the grammar of a map of members (`Upgrading\MemberMapGrammar`, the map itself `MemberMap` with `findAccess()`, its `MemberMapEntry` and `MemberMapMatch`, `MemberPattern`, `ArgumentPattern`, `FunctionPattern`) is not, a rule shipped by a package being fed the same way, and neither is `GlobalCalls`, which answers which global function a call calls and why that may be another one, nor `CodeWriter`, which writes code in the shape of the file (a class written the way the file reaches it, an import added the way the file writes its imports) for a rule of a package as for one of the core. Presets define style; DressCode has no style of its own. The four standards carry one: `dresscode/perCs` (PER Coding Style 3.1), `dresscode/psr12`, `dresscode/nette` (the Nette Coding Standard, PER with tabs, the layout of the Nette libraries and their departures, which the repository itself is checked with) and `dresscode/symfony`. What a project wants beyond the looks of its code comes in a set, a preset of one intent that decides nothing about the layout and is used beside the standard (`modernizations`, `deprecations`, `cleanup`, `correctness`, `types`, `compilerOptimizations`); a decision the core gains goes into its set, one that writes a newer construct and the replacement of what PHP deprecated alike into every set whose intent it meets (`upgrading.syntax.firstClassCallables` in `modernizations` and `deprecations`), and what no set holds the project decides itself. ## Essential commands diff --git a/composer.json b/composer.json index 3350de332..a3332fe37 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "dresscode/dresscode", - "description": "👔 DressCode: checks and fixes PHP coding style, and upgrades code to newer PHP and to new versions of libraries. Built on a lossless syntax tree, so it changes only what it touches. PER Coding Style, PSR-12, Nette.", + "description": "👔 DressCode: checks and fixes PHP coding style, and upgrades code to newer PHP and to new versions of libraries. Built on a lossless syntax tree, so it changes only what it touches. PER Coding Style, PSR-12, Nette and Symfony.", "keywords": ["code style", "coding standard", "formatter", "fixer", "linter", "refactoring", "modernization", "upgrade", "deprecations", "psr-12", "per-cs", "php-cs-fixer", "phpcs", "rector"], "homepage": "https://dresscode.run", "license": "MIT", diff --git a/docs/internals.md b/docs/internals.md index 53e5ff000..c20ab69b7 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -263,9 +263,9 @@ Such a file sets maps, the requirements of the rules of the plugin of its own pa A preset is a file of the shape of the configuration under a name, `src/Presets/.neon` for those of the core, a file a plugin names in its manifest, or any file `use` gives by its path. It carries decisions, the presets it uses, `suppressionComments` and `severity`, nothing else: what is the project's, the facts, the scope, `fixRisky`, `reportOnly`, a plugin and `rules`, `NeonReader::readPreset()` refuses, so a file laid by `use` never gains what the file of the project can do. A name without a vendor is the one of the core, so `perCs` and `dresscode/perCs` are the same preset. A preset opens with a comment, its title and what it is, and says what a decision does, where its key and value do not, in a short comment after the value. -The presets of the core are the three standards, "standard" being the word for a preset that carries a style and says how the code looks: `psr12`, `perCs` on top of it, `nette` on `perCs` and the sets `cleanup`, `types` and `correctness`, with tabs and the layout of the Nette libraries. What a project wants beyond the looks of its code is a set, a preset of one intent used beside the standard (`use: [nette, modernizations]`): `modernizations`, `deprecations`, `cleanup`, `correctness`, `types` and `compilerOptimizations`. A set decides nothing about the layout, so it never fights the standard, and a decision the core gains goes into its set, so it reaches every project using the set without a change of its configuration. A standard may use a set and keep some of its decisions, as `nette` keeps the native types and an else after a return; a project naming that set beside the standard gets none of them back, the set being laid once, below the standard, and the run says so; the project writes those decisions itself. `tests/DressCode/Presets/sets.phpt` holds that every construct and function newer PHP brought or retired is in a set and lists, with the reason, the decisions a project writes itself. A decision of a rule of a plugin reaches a project when a preset it uses or the project writes it. `psr12` and `perCs` follow their specifications section by section and add no style of their own, so a rule whose fixed behaviour contradicts a specification gets a value rather than a preset-side workaround. Without a configuration file nothing applies: DressCode has no style of its own to fall back on, so a run refuses, unless `--use` names what to use, which is then all the run has, so that a run over a foreign project can ask for that alone. +The presets of the core are the four standards, "standard" being the word for a preset that carries a style and says how the code looks: `psr12`, `perCs` on top of it, `nette` on `perCs` and the sets `cleanup`, `types` and `correctness`, with tabs and the layout of the Nette libraries, and `symfony` on `perCs`. What a project wants beyond the looks of its code is a set, a preset of one intent used beside the standard (`use: [nette, modernizations]`): `modernizations`, `deprecations`, `cleanup`, `correctness`, `types` and `compilerOptimizations`. A set decides nothing about the layout, so it never fights the standard, and a decision the core gains goes into its set, so it reaches every project using the set without a change of its configuration. A standard may use a set and keep some of its decisions, as `nette` keeps the native types and an else after a return; a project naming that set beside the standard gets none of them back, the set being laid once, below the standard, and the run says so; the project writes those decisions itself. `tests/DressCode/Presets/sets.phpt` holds that every construct and function newer PHP brought or retired is in a set and lists, with the reason, the decisions a project writes itself. A decision of a rule of a plugin reaches a project when a preset it uses or the project writes it. `psr12` and `perCs` follow their specifications section by section and add no style of their own, so a rule whose fixed behaviour contradicts a specification gets a value rather than a preset-side workaround. Without a configuration file nothing applies: DressCode has no style of its own to fall back on, so a run refuses, unless `--use` names what to use, which is then all the run has, so that a run over a foreign project can ask for that alone. -`multiline.trailingComma` removes the comma of a list written on one line wherever its place is not `keep`, so `perCs` and `nette` also fix `function f($a,)`, `use ($a,)` and a `match` on one line whose last arm is followed by a comma. +`multiline.trailingComma` removes the comma of a list written on one line wherever its place is not `keep`, so `perCs`, `nette` and `symfony` also fix `function f($a,)`, `use ($a,)` and a `match` on one line whose last arm is followed by a comma. For `symfony` that is a deliberate departure from `no_trailing_comma_in_singleline` of PHP CS Fixer, and so is the comma `optional` removes where the closing bracket follows it on the line of the last item, which `trailing_comma_in_multiline` leaves. ### Resolving the layers @@ -283,11 +283,11 @@ The excluded paths only ever add up, the default list, those of the plugins and ### What the commands print -`dresscode explain ` says what one decision is for: its description, the values it takes with what each means, its notes, the value here with the layer that said it, the values of the three standards and the rule that owns it with what it requires. With a section it explains every decision under it; without an argument it explains the configuration as one document, by sections, drawn on the console, and with `--format markdown` it writes that document to the standard output as Markdown instead of drawing it. Both are written in Markdown by one `Console\ExplainPrinter` and the console draws that Markdown (`Markup::formatMarkdown()`), so that a fact is written once and the two never part; there are no tables. +`dresscode explain ` says what one decision is for: its description, the values it takes with what each means, its notes, the value here with the layer that said it, the values of the four standards and the rule that owns it with what it requires. With a section it explains every decision under it; without an argument it explains the configuration as one document, by sections, drawn on the console, and with `--format markdown` it writes that document to the standard output as Markdown instead of drawing it. Both are written in Markdown by one `Console\ExplainPrinter` and the console draws that Markdown (`Markup::formatMarkdown()`), so that a fact is written once and the two never part; there are no tables. `dresscode config` prints that resolution in the shape of the file, every decision a layer said with the layer in a comment (`# dresscode/perCs`, `# the override for tests/`), why it takes no effect where it does not, whether it lies outside `--only` and whether its risky fixes are accepted, and `--preset ` prints one preset alone, what it uses included. `--file` narrows it to what the run uses for one file and `--format json` gives it as data: its `version`, the facts in the words of the configuration, the decisions by path, each with `value`, `layer`, `inactive`, `selected` and `severity` (`error` or `warning`), and the rules by class with the reason a rule does not run as a word (`turnedOff`, `php`, `package`, `types`, `narrowed`, `onlyOverride` or `notMentioned`) beside the sentence. A name in `fixRisky` whose rules run nowhere, not even in an override, is a warning of every run. -`dresscode catalogue` prints the catalogue of the installed set and `--format json` gives it as data, versioned (`Catalogue::Version`): every decision with its kind, its domain as data, its description and notes, the rules declaring it with what each requires and the values of the three standards as a configuration writes them. It is the interface an editor of the configuration is built on, and `composer reference` writes `docs/reference/decisions.md` from the same data. +`dresscode catalogue` prints the catalogue of the installed set and `--format json` gives it as data, versioned (`Catalogue::Version`): every decision with its kind, its domain as data, its description and notes, the rules declaring it with what each requires and the values of the four standards as a configuration writes them. It is the interface an editor of the configuration is built on, and `composer reference` writes `docs/reference/decisions.md` from the same data. ### The target version of PHP diff --git a/docs/reference/decisions.md b/docs/reference/decisions.md index 5e9423130..22770e899 100644 --- a/docs/reference/decisions.md +++ b/docs/reference/decisions.md @@ -75,7 +75,7 @@ A byte order mark at the start of the file. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Files\NoBomRule`, stage Structure ### file.closingTagAtEnd @@ -84,7 +84,7 @@ A `?>` ending a file of PHP only; a `?>` before HTML is content. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Files\NoClosingTagRule`, stage Structure ### file.finalLineEndings @@ -93,7 +93,7 @@ Exactly one line ending after the last line; a file ending in HTML is content. - By default `keep` - Takes: exactly 1; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` - Rule `DressCode\Rules\Files\FinalLineEndingsRule`, stage Formatting ### file.lineEnding @@ -102,7 +102,7 @@ The line ending of every line, which the code written new takes too; under `keep - By default `keep` - Takes: `LF` (every line ends with LF); `CRLF` (every line ends with CRLF); `majority` (every line ends as most lines of the file do, LF on a tie); `keep` -- The standards: perCs `majority`, psr12 `majority`, nette `majority` +- The standards: perCs `majority`, psr12 `majority`, nette `majority`, symfony `majority` - Rule `DressCode\Rules\Files\LineEndingRule`, stage Finishing ### file.lineLength @@ -111,7 +111,7 @@ The widest line, by which what spreads over lines is split. - By default `none` - Takes: a count from 1; `none` (no line is too wide) -- The standards: perCs `120`, psr12 `120`, nette `140` +- The standards: perCs `120`, psr12 `120`, nette `140`, symfony `none` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing ### file.lineLengthExcept @@ -120,7 +120,7 @@ What a line too wide leaves as it is: a construct named is not spread over lines - By default `[imports]` - Takes: a list of `imports` (a `use` import other than a group use on one line, which cannot be split); `strings` (a line that would fit were each of its string literals an empty pair of quotes, a string being never split); `argument` (the arguments of a call, of `new` and of an attribute, the variables of `unset()` and `isset()`); `parameter` (the parameters of a function or a method); `closureUse` (the variables of the `use` of a closure); `attribute` (the attributes of one group); `condition` (a condition of `if`, `elseif`, `while` and `do-while`); `chain` (a chain of method calls); `ternary` (a ternary); `for` (the header of a `for`); `logical` (the operands of a boolean operator outside a condition); `concatenation` (the operands of `.`); `arithmetic` (the operands of `+`, `-`, `*`, `/`, `%` and `**`); `bitwise` (the operands of `|`, `&`, `^`, `<<` and `>>`); `coalesce` (the operands of `??`); `pipe` (the operands of `|>`); `array` (the items of an array, which `multiline.maxWidth.array` spreads all the same); `list` (the items of `list()`); `implements` (the interfaces a class implements or an interface extends); `matchCondition` (the conditions of a `match` arm); regular expressions -- The standards: perCs `[imports]`, psr12 `[imports]`, nette `[imports, strings]` +- The standards: perCs `[imports]`, psr12 `[imports]`, nette `[imports, strings]`, symfony `[imports]` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing - Read by rule `DressCode\Rules\Arrays\MultilineArrayRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Classes\MultilineImplementsRule`, which this decision does not turn on @@ -141,7 +141,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing ### file.openingTag @@ -150,7 +150,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` - Rule `DressCode\Rules\Files\OpeningTagNotationRule`, stage Structure ### file.statementsPerLine @@ -159,7 +159,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - By default `keep` - Takes: exactly 1; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` - Rule `DressCode\Rules\Whitespace\NoStatementsSharingLineRule`, stage Formatting ### file.strictTypes.declaration @@ -168,7 +168,7 @@ Every file of PHP code declares `strict_types=1` as its first statement, a file - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Files\StrictTypesRequiredRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -178,7 +178,7 @@ Where the declaration of `strict_types` stands. - By default `keep` - Takes: `sameLine` (`getTotal()`, never `$order->gettotal()`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `declared` +- The standards: perCs `keep`, psr12 `keep`, nette `declared`, symfony `declared` - Rule `DressCode\Rules\Functions\MethodCasingRule`, stage Structure ### casing.trueFalseNull @@ -460,7 +460,7 @@ The case of `true`, `false` and `null`, a leading backslash staying. - By default `keep` - Takes: `lowercase` (`true`, `false`, `null`); `uppercase` (`TRUE`, `FALSE`, `NULL`); `keep` -- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase` +- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase`, symfony `lowercase` - Rule `DressCode\Rules\Literals\BuiltinCasingRule`, stage Structure ### casing.type @@ -469,7 +469,7 @@ The case of the built-in types of a declaration; `array`, `callable`, `static`, - By default `keep` - Takes: `lowercase` (`int`, `string`, `void`); `keep` -- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase` +- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase`, symfony `lowercase` - Rule `DressCode\Rules\Namespaces\DeclaredNameCasingRule`, stage Structure ## qualification @@ -480,7 +480,7 @@ A class, interface, trait or enum of the namespace of the file, `Order` in `name - By default `keep` - Takes: `bare` (bare, `Order` relative to the namespace); `backslashed` (with the leading backslash, `\Acme\Shop\Order`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `bare` +- The standards: perCs `keep`, psr12 `keep`, nette `bare`, symfony `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -490,7 +490,7 @@ A class of the global namespace referenced in a namespace, `DateTime`, which a b - By default `keep` - Takes: `imported` (imported, `use DateTime;` and `DateTime`); `backslashed` (with the leading backslash, `\DateTime`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `backslashed` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure ### qualification.class.otherNamespace @@ -501,7 +501,7 @@ A name relative to an import or to the namespace, `Shop\Order`, stays as it is. - By default `keep` - Takes: `imported` (imported, `use Acme\Shop\Order;` and `Order`); `backslashed` (with the leading backslash, `\Acme\Shop\Order`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `imported` +- The standards: perCs `keep`, psr12 `keep`, nette `imported`, symfony `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -513,7 +513,7 @@ A name relative to an import or to the namespace, `Shop\Order`, stays as it is, - By default `keep` - Takes: `imported` (imported, `use Acme\Shop\Order;` and `@param Order $order`, a class of the namespace of the file bare, a global one where the file imports it); `backslashed` (with the leading backslash, `@param \Acme\Shop\Order $order`, a name an import reaches); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `imported` +- The standards: perCs `keep`, psr12 `keep`, nette `imported`, symfony `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -523,7 +523,7 @@ A constant of the namespace of the file, `STATUS_PAID` in `namespace Acme\Shop`. - By default `keep` - Takes: `bare` (bare, `STATUS_PAID` relative to the namespace); `backslashed` (with the leading backslash, `\Acme\Shop\STATUS_PAID`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -533,7 +533,7 @@ A global constant in a namespace, `PHP_EOL`, but where the compiler computes wit - By default `keep` - Takes: `bare` (bare, `PHP_EOL` without an import or a backslash, reached by the fallback at run time); `imported` (imported, `use const PHP_EOL;` and `PHP_EOL`); `backslashed` (with the leading backslash, `\PHP_EOL`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -545,7 +545,7 @@ A constant merely passed to a call is not one the compiler computes with. - By default `keep` - Takes: `imported` (imported, `use const PHP_VERSION_ID;`, so that the compiler computes with it); `backslashed` (with the leading backslash, `\PHP_VERSION_ID`, so that the compiler computes with it); `bare` (bare, `PHP_VERSION_ID`, forgoing the optimization); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -555,7 +555,7 @@ A constant of a namespace other than that of the file. - By default `keep` - Takes: `imported` (imported, `use const Acme\Shop\STATUS_PAID;` and `STATUS_PAID`); `backslashed` (with the leading backslash, `\Acme\Shop\STATUS_PAID`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -565,7 +565,7 @@ How a class refers to itself in an expression: a static access, an instantiation - By default `keep` - Takes: `self` (the class named `self` inside itself); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `self` +- The standards: perCs `keep`, psr12 `keep`, nette `self`, symfony `keep` - Rule `DressCode\Rules\Classes\SelfForCurrentClassRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -575,7 +575,7 @@ A function of the namespace of the file, `normalize()` in `namespace Acme\Text`. - By default `keep` - Takes: `bare` (bare, `normalize()` relative to the namespace); `backslashed` (with the leading backslash, `\Acme\Text\normalize()`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -587,7 +587,7 @@ Not a function of the namespace of the file, nor a name in a file without a name - By default `keep` - Takes: `bare` (bare, `ucfirst()` without an import or a backslash, reached by the fallback at run time); `imported` (imported, `use function ucfirst;` and `ucfirst()`); `backslashed` (with the leading backslash, `\ucfirst()`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -599,7 +599,7 @@ A function is optimized in a namespace where one of its calls there is, an unpac - By default `keep` - Takes: `imported` (imported, `use function strlen;`, so that the compiler optimizes it); `backslashed` (with the leading backslash, `\strlen()`, so that the compiler optimizes it); `bare` (bare, `strlen()`, forgoing the optimization); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - Rule `DressCode\Rules\Namespaces\OptimizedCallNotationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -610,7 +610,7 @@ A function of a namespace other than that of the file. - By default `keep` - Takes: `imported` (imported, `use function Acme\Text\normalize;` and `normalize()`); `backslashed` (with the leading backslash, `\Acme\Text\normalize()`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -620,7 +620,7 @@ The leading backslash of a name where it changes nothing, in a file without a na - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Namespaces\UselessLeadingBackslashRule`, stage Structure ### qualification.staticInFinalClass @@ -631,7 +631,7 @@ The return type `static` stays, saying what a subclass would return. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\SelfForCurrentClassRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -643,7 +643,7 @@ The return type `static` stays, saying what a subclass would return. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Namespaces\UselessAliasRule`, stage Structure ### imports.fromCurrentNamespace @@ -652,7 +652,7 @@ An import of a class of the namespace it stands in, `use Acme\Shop\Order;` insid - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Namespaces\UselessCurrentNamespaceImportRule`, stage Structure ### imports.grouping.class @@ -661,7 +661,7 @@ How the imports of classes are put into `use` statements, a name of the global n - By default `keep` - Takes: `separated` (a `use` of its own for every name, `use Foo; use Bar;`); `combined` (all names of the kind in one `use`, `use Foo, Bar;`, one per namespace declaration); `grouped` (the names of one namespace in one group use, `use Acme\Shop\{Order, Cart};`, from `imports.groupMinNames` of them on); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[separated, grouped]` +- The standards: perCs `keep`, psr12 `keep`, nette `[separated, grouped]`, symfony `separated` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.grouping.constant @@ -670,7 +670,7 @@ How the imports of constants are put into `use` statements, a name of the global - By default `keep` - Takes: `separated` (a `use` of its own for every name, `use Foo; use Bar;`); `combined` (all names of the kind in one `use`, `use Foo, Bar;`, one per namespace declaration); `grouped` (the names of one namespace in one group use, `use Acme\Shop\{Order, Cart};`, from `imports.groupMinNames` of them on); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]` +- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]`, symfony `separated` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.grouping.function @@ -679,7 +679,7 @@ How the imports of functions are put into `use` statements, a name of the global - By default `keep` - Takes: `separated` (a `use` of its own for every name, `use Foo; use Bar;`); `combined` (all names of the kind in one `use`, `use Foo, Bar;`, one per namespace declaration); `grouped` (the names of one namespace in one group use, `use Acme\Shop\{Order, Cart};`, from `imports.groupMinNames` of them on); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]` +- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]`, symfony `separated` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.groupMinNames @@ -688,7 +688,7 @@ How many names of one namespace make a group use where `grouped` is written, few - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `2` +- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.leadingBackslash @@ -697,7 +697,7 @@ The leading backslash of an import, which changes nothing, `use \Foo\Bar;`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Namespaces\UselessLeadingBackslashRule`, stage Structure ### imports.order.caseSensitive @@ -706,7 +706,7 @@ Whether `Acme` sorts before `acme` rather than with it. - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` - Rule `DressCode\Rules\Namespaces\ImportOrderRule`, stage Structure ### imports.order.kinds @@ -715,7 +715,7 @@ The order of the kinds among consecutive imports, a kind not listed following in - By default `keep` - Takes: an order of `class` (the imports of classes); `function` (the imports of functions); `constant` (the imports of constants); `keep` -- The standards: perCs `[class, function, constant]`, psr12 `[class, function, constant]`, nette `[class, function, constant]` +- The standards: perCs `[class, function, constant]`, psr12 `[class, function, constant]`, nette `[class, function, constant]`, symfony `[class, function, constant]` - Rule `DressCode\Rules\Namespaces\ImportOrderRule`, stage Structure ### imports.order.names @@ -724,7 +724,7 @@ The order of the names of one kind among consecutive imports, the names of a sta - By default `keep` - Takes: `alphabetical` (the names of a kind sorted alphabetically); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `alphabetical` +- The standards: perCs `keep`, psr12 `keep`, nette `alphabetical`, symfony `alphabetical` - Rule `DressCode\Rules\Namespaces\ImportOrderRule`, stage Structure ### imports.unused @@ -733,7 +733,7 @@ An import nothing uses is removed, a class name in a doc comment using the impor - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Namespaces\NoUnusedImportsRule`, stage Structure ## spacing @@ -744,7 +744,7 @@ The whitespace wider than a single space around an operator, aligning a column o - By default `spaces` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `spaces` (spaces aligning a column staying, tabs not); `tabs` (tabs aligning a column staying, spaces not); `optional` (there or not, as the author wrote it) -- The standards: perCs `spaces`, psr12 `spaces`, nette `spaces` +- The standards: perCs `spaces`, psr12 `spaces`, nette `spaces`, symfony `forbidden` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### spacing.alignment.comma @@ -753,7 +753,7 @@ The whitespace wider than a single space after a comma, aligning the columns of - By default `tabs` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `spaces` (spaces aligning a column staying, tabs not); `tabs` (tabs aligning a column staying, spaces not); `optional` (there or not, as the author wrote it) -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `tabs` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `tabs`, symfony `optional` - Rule `DressCode\Rules\Whitespace\CommaSpacingRule`, stage Formatting ### spacing.alignment.comment @@ -762,7 +762,7 @@ The whitespace wider than a single space before a comment following code, aligni - By default `optional` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `optional` (there or not, as the author wrote it) -- The standards: perCs `optional`, psr12 `optional`, nette `optional` +- The standards: perCs `optional`, psr12 `optional`, nette `optional`, symfony `optional` - Rule `DressCode\Rules\Comments\CommentSpacingRule`, stage Finishing - Modifies comments @@ -772,7 +772,7 @@ The whitespace wider than a single space around `?` and `:`, aligning a column; - By default `optional` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `optional` (there or not, as the author wrote it) -- The standards: perCs `optional`, psr12 `optional`, nette `forbidden` +- The standards: perCs `optional`, psr12 `optional`, nette `forbidden`, symfony `optional` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting ### spacing.alternativeSyntax @@ -781,7 +781,7 @@ The space before the colon of the alternative syntax and of a label, and the sin - By default `keep` - Takes: `'if ($a):'` (no space before the colon); `'if ($a) :'` (a single space before the colon); `keep` -- The standards: perCs `'if ($a):'`, psr12 `'if ($a):'`, nette `'if ($a):'` +- The standards: perCs `'if ($a):'`, psr12 `'if ($a):'`, nette `'if ($a):'`, symfony `'if ($a):'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.anonymousClass @@ -790,7 +790,7 @@ The space between `class` and the arguments of an anonymous class. - By default `keep` - Takes: `'new class ($a)'` (a single space before the arguments); `'new class($a)'` (no space before the arguments); `keep` -- The standards: perCs `'new class ($a)'`, psr12 `'new class ($a)'`, nette `'new class ($a)'` +- The standards: perCs `'new class ($a)'`, psr12 `'new class ($a)'`, nette `'new class ($a)'`, symfony `'new class($a)'` - Rule `DressCode\Rules\Classes\ClassHeadSpacingRule`, stage Formatting ### spacing.arrowFunction @@ -799,7 +799,7 @@ The space between `fn` and its parenthesis. - By default `keep` - Takes: `'fn($x) => $x'` (no space before the parenthesis); `'fn ($x) => $x'` (a single space); `keep` -- The standards: perCs `'fn($x) => $x'`, psr12 `keep`, nette `'fn($x) => $x'` +- The standards: perCs `'fn($x) => $x'`, psr12 `keep`, nette `'fn($x) => $x'`, symfony `'fn ($x) => $x'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.attribute @@ -808,7 +808,7 @@ The whitespace inside the brackets of an attribute group and before the parenthe - By default `keep` - Takes: `'#[Foo($a)]'` (none inside the brackets, none before the parenthesis); `keep` -- The standards: perCs `'#[Foo($a)]'`, psr12 `keep`, nette `'#[Foo($a)]'` +- The standards: perCs `'#[Foo($a)]'`, psr12 `keep`, nette `'#[Foo($a)]'`, symfony `'#[Foo($a)]'` - Rule `DressCode\Rules\Whitespace\AttributeSpacingRule`, stage Formatting ### spacing.binaryOperator @@ -817,7 +817,7 @@ The spaces around a binary operator, an assignment, `instanceof`, `=>` and the ` - By default `keep` - Takes: `'$a + $b'` (a single space around); `keep` -- The standards: perCs `'$a + $b'`, psr12 `'$a + $b'`, nette `'$a + $b'` +- The standards: perCs `'$a + $b'`, psr12 `'$a + $b'`, nette `'$a + $b'`, symfony `'$a + $b'` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### spacing.body @@ -826,7 +826,7 @@ The space inside the braces of a body whose code stands on their line, of a func - By default `keep` - Takes: `'function () { return 1; }'` (a single space inside the braces); `keep` -- The standards: perCs `'function () { return 1; }'`, psr12 `'function () { return 1; }'`, nette `'function () { return 1; }'` +- The standards: perCs `'function () { return 1; }'`, psr12 `'function () { return 1; }'`, nette `'function () { return 1; }'`, symfony `'function () { return 1; }'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.call @@ -835,7 +835,7 @@ The whitespace between the name of a function and its parenthesis, of a call and - By default `keep` - Takes: `'foo($a, $b)'` (no space before the parenthesis); `keep` -- The standards: perCs `'foo($a, $b)'`, psr12 `'foo($a, $b)'`, nette `'foo($a, $b)'` +- The standards: perCs `'foo($a, $b)'`, psr12 `'foo($a, $b)'`, nette `'foo($a, $b)'`, symfony `'foo($a, $b)'` - Rule `DressCode\Rules\Functions\FunctionNameSpacingRule`, stage Formatting ### spacing.cast @@ -844,7 +844,7 @@ The space between a cast and its operand, which stays on the line of the cast. - By default `keep` - Takes: `'(int) $x'` (a single space after the cast); `'(int)$x'` (no space after the cast); `keep` -- The standards: perCs `'(int) $x'`, psr12 `'(int) $x'`, nette `'(int) $x'` +- The standards: perCs `'(int) $x'`, psr12 `'(int) $x'`, nette `'(int) $x'`, symfony `'(int) $x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.catchType @@ -853,7 +853,7 @@ The space around the bar between the types of a `catch`. - By default `keep` - Takes: `'catch (A | B $e)'` (a single space around the bar); `'catch (A|B $e)'` (no space around the bar); `keep` -- The standards: perCs `'catch (A|B $e)'`, psr12 `'catch (A | B $e)'`, nette `'catch (A|B $e)'` +- The standards: perCs `'catch (A|B $e)'`, psr12 `'catch (A | B $e)'`, nette `'catch (A|B $e)'`, symfony `'catch (A|B $e)'` - Rule `DressCode\Rules\Types\TypeDeclarationSpacingRule`, stage Formatting ### spacing.comma @@ -862,7 +862,7 @@ The whitespace around a comma, which stays on the line of what is before it, a l - By default `keep` - Takes: `'$a, $b'` (none before, a single space after); `keep` -- The standards: perCs `'$a, $b'`, psr12 `'$a, $b'`, nette `'$a, $b'` +- The standards: perCs `'$a, $b'`, psr12 `'$a, $b'`, nette `'$a, $b'`, symfony `'$a, $b'` - Rule `DressCode\Rules\Whitespace\CommaSpacingRule`, stage Formatting ### spacing.comment.afterCode @@ -871,7 +871,7 @@ The whitespace before a comment that follows code on its line. - By default `keep` - Takes: `'$a; // text'` (at least one space); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'$a; // text'` +- The standards: perCs `keep`, psr12 `keep`, nette `'$a; // text'`, symfony `'$a; // text'` - Rule `DressCode\Rules\Comments\CommentSpacingRule`, stage Finishing - Modifies comments @@ -881,7 +881,7 @@ The space after the marker of a comment and before the closing one, unless anoth - By default `keep` - Takes: `'// text'` (a single space after the marker); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'// text'` +- The standards: perCs `keep`, psr12 `keep`, nette `'// text'`, symfony `'// text'` - Rule `DressCode\Rules\Comments\CommentSpacingRule`, stage Finishing - Modifies comments @@ -891,7 +891,7 @@ The space around the concatenation operator, unless it sits at a line break. - By default `keep` - Takes: `'$a . $b'` (a single space around); `'$a.$b'` (no space around); `keep` -- The standards: perCs `'$a . $b'`, psr12 `'$a . $b'`, nette `'$a . $b'` +- The standards: perCs `'$a . $b'`, psr12 `'$a . $b'`, nette `'$a . $b'`, symfony `'$a.$b'` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### spacing.declare @@ -900,7 +900,7 @@ The whitespace inside a `declare` statement. - By default `keep` - Takes: `'declare(strict_types=1)'` (none inside); `keep` -- The standards: perCs `'declare(strict_types=1)'`, psr12 `'declare(strict_types=1)'`, nette `'declare(strict_types=1)'` +- The standards: perCs `'declare(strict_types=1)'`, psr12 `'declare(strict_types=1)'`, nette `'declare(strict_types=1)'`, symfony `'declare(strict_types=1)'` - Rule `DressCode\Rules\Files\DeclareSpacingRule`, stage Formatting ### spacing.doubleColon @@ -909,7 +909,7 @@ The whitespace around the double colon. - By default `keep` - Takes: `'Foo::bar()'` (none around); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'Foo::bar()'` +- The standards: perCs `keep`, psr12 `keep`, nette `'Foo::bar()'`, symfony `keep` - Rule `DressCode\Rules\Expressions\MemberAccessSpacingRule`, stage Formatting ### spacing.import @@ -918,7 +918,7 @@ The space around the braces of a group use, which hug the names. - By default `keep` - Takes: `'use Acme\{Foo, Bar};'` (no space around the braces); `keep` -- The standards: perCs `'use Acme\{Foo, Bar};'`, psr12 `'use Acme\{Foo, Bar};'`, nette `'use Acme\{Foo, Bar};'` +- The standards: perCs `'use Acme\{Foo, Bar};'`, psr12 `'use Acme\{Foo, Bar};'`, nette `'use Acme\{Foo, Bar};'`, symfony `'use Acme\{Foo, Bar};'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.keyword @@ -927,7 +927,7 @@ The space after a keyword followed by code on its line, `return`, `new`, `if`, ` - By default `keep` - Takes: `'return $x'` (a single space); `keep` -- The standards: perCs `'return $x'`, psr12 `'return $x'`, nette `'return $x'` +- The standards: perCs `'return $x'`, psr12 `'return $x'`, nette `'return $x'`, symfony `'return $x'` - Rule `DressCode\Rules\Classes\ClassHeadSpacingRule`, stage Formatting - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting @@ -937,7 +937,7 @@ The whitespace between `!` and its operand, which stay on one line. - By default `keep` - Takes: `'!$x'` (no space after the operator); `'! $x'` (a single space after the operator); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `['!$x', '! $x']`, psr12 `['!$x', '! $x']`, nette `'!$x'` +- The standards: perCs `['!$x', '! $x']`, psr12 `['!$x', '! $x']`, nette `'!$x'`, symfony `'!$x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.namedArgument @@ -946,7 +946,7 @@ The whitespace around the colon of a named argument. - By default `keep` - Takes: `'foo(name: $x)'` (none before the colon, a single space after); `keep` -- The standards: perCs `'foo(name: $x)'`, psr12 `keep`, nette `'foo(name: $x)'` +- The standards: perCs `'foo(name: $x)'`, psr12 `keep`, nette `'foo(name: $x)'`, symfony `'foo(name: $x)'` - Rule `DressCode\Rules\Functions\NamedArgumentSpacingRule`, stage Formatting ### spacing.objectOperator @@ -955,7 +955,7 @@ The whitespace around the object operator. - By default `keep` - Takes: `'$a->b()'` (none around); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'$a->b()'` +- The standards: perCs `keep`, psr12 `keep`, nette `'$a->b()'`, symfony `'$a->b()'` - Rule `DressCode\Rules\Expressions\MemberAccessSpacingRule`, stage Formatting ### spacing.openBrace @@ -964,7 +964,7 @@ The space before the brace or the body a structure opens on the line of its head - By default `keep` - Takes: `'if ($a) {'` (a single space); `keep` -- The standards: perCs `'if ($a) {'`, psr12 `'if ($a) {'`, nette `'if ($a) {'` +- The standards: perCs `'if ($a) {'`, psr12 `'if ($a) {'`, nette `'if ($a) {'`, symfony `'if ($a) {'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.padding.argument @@ -973,7 +973,7 @@ The whitespace inside the parentheses of the arguments of a call, of `new` and o - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.array @@ -982,7 +982,7 @@ The whitespace inside the brackets or the parentheses of an array. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.closureUse @@ -991,7 +991,7 @@ The whitespace inside the parentheses of the `use` of a closure. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.controlStructure @@ -1000,7 +1000,7 @@ The whitespace inside the parentheses of `if`, `elseif`, `while`, `for`, `foreac - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.grouping @@ -1009,7 +1009,7 @@ The whitespace inside the parentheses grouping an expression, `($a + $b) * $c`. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.list @@ -1018,7 +1018,7 @@ The whitespace inside the brackets or the parentheses of a destructuring. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.offset @@ -1027,7 +1027,7 @@ The whitespace inside the brackets of an offset access and before them. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `exceptLiterals` (a single space around an index other than a string or an integer literal, `$a[ $i ]` but `$a[0]`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.parameter @@ -1036,7 +1036,7 @@ The whitespace inside the parentheses of the parameters of a function, a method, - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.reference @@ -1045,7 +1045,7 @@ The whitespace between `&` and its operand. - By default `keep` - Takes: `'&$x'` (none between); `keep` -- The standards: perCs `'&$x'`, psr12 `'&$x'`, nette `'&$x'` +- The standards: perCs `'&$x'`, psr12 `'&$x'`, nette `'&$x'`, symfony `'&$x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.semicolon.after @@ -1054,7 +1054,7 @@ The whitespace after a semicolon that more code follows on its line. - By default `keep` - Takes: `'for ($i = 0; $i < 5; $i++)'` (a single space after a semicolon); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'for ($i = 0; $i < 5; $i++)'` +- The standards: perCs `keep`, psr12 `keep`, nette `'for ($i = 0; $i < 5; $i++)'`, symfony `'for ($i = 0; $i < 5; $i++)'` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting ### spacing.semicolon.before @@ -1063,7 +1063,7 @@ The whitespace before a semicolon, which stays on the line of what it ends. - By default `keep` - Takes: `'$a;'` (no space before a semicolon); `keep` -- The standards: perCs `'$a;'`, psr12 `keep`, nette `'$a;'` +- The standards: perCs `'$a;'`, psr12 `keep`, nette `'$a;'`, symfony `'$a;'` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting ### spacing.spread @@ -1072,7 +1072,7 @@ The whitespace between `...` and its operand. - By default `keep` - Takes: `'...$x'` (none between); `keep` -- The standards: perCs `'...$x'`, psr12 `'...$x'`, nette `'...$x'` +- The standards: perCs `'...$x'`, psr12 `'...$x'`, nette `'...$x'`, symfony `'...$x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.switchCase @@ -1081,7 +1081,7 @@ The whitespace before the colon of a `case`. - By default `keep` - Takes: `'case 1:'` (none before the colon); `keep` -- The standards: perCs `'case 1:'`, psr12 `'case 1:'`, nette `'case 1:'` +- The standards: perCs `'case 1:'`, psr12 `'case 1:'`, nette `'case 1:'`, symfony `'case 1:'` - Rule `DressCode\Rules\ControlFlow\SwitchCaseSpacingRule`, stage Formatting ### spacing.ternary @@ -1090,7 +1090,7 @@ The whitespace around `?` and `:` of a ternary, and around `?:` as a whole, unle - By default `keep` - Takes: `'$a ? $b : $c'` (whitespace around `?` and `:`); `keep` -- The standards: perCs `'$a ? $b : $c'`, psr12 `'$a ? $b : $c'`, nette `'$a ? $b : $c'` +- The standards: perCs `'$a ? $b : $c'`, psr12 `'$a ? $b : $c'`, nette `'$a ? $b : $c'`, symfony `'$a ? $b : $c'` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting ### spacing.typeDeclaration @@ -1099,7 +1099,7 @@ The whitespace of a type declaration, which stays on one line with what it descr - By default `keep` - Takes: `'?int $x'` (none inside a type, a single space before the name); `keep` -- The standards: perCs `'?int $x'`, psr12 `'?int $x'`, nette `'?int $x'` +- The standards: perCs `'?int $x'`, psr12 `'?int $x'`, nette `'?int $x'`, symfony `'?int $x'` - Rule `DressCode\Rules\Types\TypeDeclarationSpacingRule`, stage Formatting ### spacing.unaryOperator @@ -1108,7 +1108,7 @@ The whitespace between a unary operator other than `!` and its operand, which st - By default `keep` - Takes: `'-$x'` (no space after the operator); `keep` -- The standards: perCs `'-$x'`, psr12 `'-$x'`, nette `'-$x'` +- The standards: perCs `'-$x'`, psr12 `'-$x'`, nette `'-$x'`, symfony `'-$x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ## multiline @@ -1119,7 +1119,7 @@ The attributes of a class, a function, a method, a property, a constant, a case - By default `keep` - Takes: `above` (every group on a line of its own above the declaration); `sameLine` (every group on the line of the declaration); `keep` -- The standards: perCs `above`, psr12 `keep`, nette `above` +- The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `above` - Rule `DressCode\Rules\Whitespace\AttributePositionRule`, stage Formatting ### multiline.attributePosition.parameter @@ -1128,7 +1128,7 @@ Where the attributes of a parameter stand. - By default `keep` - Takes: `above` (every group on a line of its own above the parameter in a list of parameters spread over lines, on the line of the parameter in a list on one line); `sameLine` (every group on the line of the parameter, in a list spread over lines too); `keep` -- The standards: perCs `above`, psr12 `keep`, nette `above` +- The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `keep` - Rule `DressCode\Rules\Whitespace\AttributePositionRule`, stage Formatting ### multiline.edges.argument @@ -1137,7 +1137,7 @@ Where the arguments spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first argument on the line after `(`, `)` on a line of its own); `attached` (the first argument on the line of `(`, `)` right after the last one); `closingApart` (the first argument on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `apart` +- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting ### multiline.edges.array @@ -1146,7 +1146,7 @@ Where the items spread over lines begin against the opening bracket and where th - By default `keep` - Takes: `apart` (the first item on the line after the opening bracket, the closing one on a line of its own); `attached` (the first item on the line of the opening bracket, the closing one right after the last one); `closingApart` (the first item on the line of the opening bracket, the closing one on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]` +- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]`, symfony `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.edges.attribute @@ -1155,7 +1155,7 @@ Where the attributes spread over lines begin against `#[` and where `]` stands. - By default `keep` - Takes: `apart` (the first attribute on the line after `#[`, `]` on a line of its own); `attached` (the first attribute on the line of `#[`, `]` right after the last one); `closingApart` (the first attribute on the line of `#[`, `]` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Whitespace\MultilineAttributeRule`, stage Formatting ### multiline.edges.chain @@ -1164,7 +1164,7 @@ Where the links spread over lines begin against the object. - By default `keep` - Takes: `apart` (the first link on the line after the object); `attached` (the links up to the first method call on the line of the object); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `keep`, nette `attached` +- The standards: perCs `apart`, psr12 `keep`, nette `attached`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineChainRule`, stage Formatting ### multiline.edges.closureUse @@ -1173,7 +1173,7 @@ Where the variables spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first variable on the line after `(`, `)` on a line of its own); `attached` (the first variable on the line of `(`, `)` right after the last one); `closingApart` (the first variable on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\MultilineClosureUseRule`, stage Formatting ### multiline.edges.condition @@ -1182,7 +1182,7 @@ Where the parts spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first part on the line after `(`, `)` on a line of its own); `attached` (the first part on the line of `(`, `)` right after the last one); `closingApart` (the first part on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `[apart, closingApart]` +- The standards: perCs `apart`, psr12 `apart`, nette `[apart, closingApart]`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting ### multiline.edges.implements @@ -1191,7 +1191,7 @@ Where the interfaces spread over lines begin against the keyword. - By default `keep` - Takes: `apart` (the first interface on the line after the keyword); `attached` (the first interface on the line of the keyword); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `apart` +- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep` - Rule `DressCode\Rules\Classes\MultilineImplementsRule`, stage Formatting ### multiline.edges.import @@ -1200,7 +1200,7 @@ Where the names spread over lines begin against `{` and where `}` stands. - By default `keep` - Takes: `apart` (the first name on the line after `{`, `}` on a line of its own); `attached` (the first name on the line of `{`, `}` right after the last one); `closingApart` (the first name on the line of `{`, `}` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `apart` +- The standards: perCs `keep`, psr12 `keep`, nette `apart`, symfony `keep` - Rule `DressCode\Rules\Namespaces\MultilineImportRule`, stage Formatting ### multiline.edges.list @@ -1209,7 +1209,7 @@ Where the items spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first item on the line after `(`, `)` on a line of its own); `attached` (the first item on the line of `(`, `)` right after the last one); `closingApart` (the first item on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]` +- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]`, symfony `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.edges.parameter @@ -1218,7 +1218,7 @@ Where the parameters spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first parameter on the line after `(`, `)` on a line of its own); `attached` (the first parameter on the line of `(`, `)` right after the last one); `closingApart` (the first parameter on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `apart` +- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting ### multiline.expressionStart.arrayItem @@ -1227,7 +1227,7 @@ Where an expression spanning several lines after the `=>` of an array item begin - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### multiline.expressionStart.arrowFunction @@ -1236,7 +1236,7 @@ Where an expression spanning several lines after the `=>` of an arrow function b - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### multiline.expressionStart.assignment @@ -1245,7 +1245,7 @@ Where an expression spanning several lines after the `=` of an assignment, a con - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### multiline.expressionStart.echo @@ -1254,7 +1254,7 @@ Where an expression spanning several lines after `echo` and `print` begins. - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.expressionStart.return @@ -1263,7 +1263,7 @@ Where an expression spanning several lines after `return` begins. - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.expressionStart.throw @@ -1272,7 +1272,7 @@ Where an expression spanning several lines after `throw` begins. - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.expressionStart.yield @@ -1281,7 +1281,7 @@ Where an expression spanning several lines after `yield` and `yield from` begins - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.fillMinItems.array @@ -1290,7 +1290,7 @@ From how many items without keys an array written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `6` +- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.fillMinItems.import @@ -1299,7 +1299,7 @@ From how many names a group use written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `2` +- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2` - Rule `DressCode\Rules\Namespaces\MultilineImportRule`, stage Formatting ### multiline.fillMinItems.list @@ -1308,7 +1308,7 @@ From how many items without keys `list()` written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `6` +- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.fillMinItems.matchCondition @@ -1317,7 +1317,7 @@ From how many conditions an arm written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `6` +- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2` - Rule `DressCode\Rules\Expressions\MultilineMatchArmRule`, stage Formatting ### multiline.items.argument @@ -1328,7 +1328,7 @@ A sole argument that is a string, a message, is not spread for its width - By default `keep` - Takes: `perLine` (every argument on a line of its own); `fill` (the arguments may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting ### multiline.items.arithmetic @@ -1339,7 +1339,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.array @@ -1350,7 +1350,7 @@ Written as `fill`, the items without keys fill the lines evenly from `multiline. - By default `keep` - Takes: `perLine` (every item on a line of its own); `fill` (the items may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `fill` +- The standards: perCs `perLine`, psr12 `keep`, nette `fill`, symfony `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.items.attribute @@ -1359,7 +1359,7 @@ The attributes of one group spread over lines by their author or for their width - By default `keep` - Takes: `perLine` (every attribute on a line of its own); `fill` (the attributes may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Whitespace\MultilineAttributeRule`, stage Formatting ### multiline.items.bitwise @@ -1370,7 +1370,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.chain @@ -1379,7 +1379,7 @@ The links of a chain of method calls and property accesses spread over lines by - By default `keep` - Takes: `perLine` (every link on a line of its own); `fill` (the links may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `perLine` +- The standards: perCs `perLine`, psr12 `keep`, nette `perLine`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineChainRule`, stage Formatting ### multiline.items.closureUse @@ -1388,7 +1388,7 @@ The variables of the `use` of a closure spread over lines by their author or for - By default `keep` - Takes: `perLine` (every variable on a line of its own); `fill` (the variables may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\MultilineClosureUseRule`, stage Formatting ### multiline.items.coalesce @@ -1399,7 +1399,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.concatenation @@ -1410,7 +1410,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.condition @@ -1419,7 +1419,7 @@ A condition of `if`, `elseif`, `while` and `do-while` joined by boolean operator - By default `keep` - Takes: `perLine` (every part on a line of its own); `fill` (the parts may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting ### multiline.items.for @@ -1428,7 +1428,7 @@ The sections of the header of a `for` spread over lines by its author or for the - By default `keep` - Takes: `perLine` (every section on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\MultilineForRule`, stage Formatting ### multiline.items.implements @@ -1437,7 +1437,7 @@ The interfaces a class or an enum implements, or an interface extends, spread ov - By default `keep` - Takes: `perLine` (every interface on a line of its own); `fill` (the interfaces may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` - Rule `DressCode\Rules\Classes\MultilineImplementsRule`, stage Formatting ### multiline.items.import @@ -1448,7 +1448,7 @@ Written as `fill`, the names fill the lines evenly from `multiline.fillMinItems. - By default `keep` - Takes: `perLine` (every name on a line of its own); `fill` (the names may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `fill` +- The standards: perCs `keep`, psr12 `keep`, nette `fill`, symfony `keep` - Rule `DressCode\Rules\Namespaces\MultilineImportRule`, stage Formatting ### multiline.items.list @@ -1459,7 +1459,7 @@ Written as `fill`, the items without keys fill the lines evenly from `multiline. - By default `keep` - Takes: `perLine` (every item on a line of its own); `fill` (the items may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `fill` +- The standards: perCs `perLine`, psr12 `keep`, nette `fill`, symfony `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.items.logical @@ -1470,7 +1470,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.matchArm @@ -1481,7 +1481,7 @@ Not about the conditions of one arm, which `multiline.items.matchCondition` spre - By default `keep` - Takes: `perLine` (every arm on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `perLine` +- The standards: perCs `keep`, psr12 `keep`, nette `perLine`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineMatchRule`, stage Formatting ### multiline.items.matchCondition @@ -1492,7 +1492,7 @@ Written as `fill`, the conditions fill the lines evenly from `multiline.fillMinI - By default `keep` - Takes: `perLine` (every condition on a line of its own); `fill` (the conditions may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `fill` +- The standards: perCs `perLine`, psr12 `perLine`, nette `fill`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineMatchArmRule`, stage Formatting ### multiline.items.parameter @@ -1501,7 +1501,7 @@ The parameters of a function or a method spread over lines by their author or fo - By default `keep` - Takes: `perLine` (every parameter on a line of its own); `fill` (the parameters may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting ### multiline.items.pipe @@ -1512,7 +1512,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.ternary @@ -1521,7 +1521,7 @@ A ternary spread over lines by its author, or for the width of its line. - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `perLine` +- The standards: perCs `perLine`, psr12 `keep`, nette `perLine`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineTernaryRule`, stage Formatting ### multiline.maxWidth.array @@ -1530,7 +1530,7 @@ The width from bracket to bracket over which an array of several items written o - By default `none` - Takes: a count from 1; `none` (never spread for its width) -- The standards: perCs `none`, psr12 `none`, nette `130` +- The standards: perCs `none`, psr12 `none`, nette `130`, symfony `none` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.operatorPosition.arithmetic @@ -1539,7 +1539,7 @@ Where `+`, `-`, `*`, `/`, `%` and `**` stand at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.bitwise @@ -1548,7 +1548,7 @@ Where `|`, `&`, `^`, `<<` and `>>` stand at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.coalesce @@ -1557,7 +1557,7 @@ Where `??` stands at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.comparison @@ -1566,7 +1566,7 @@ Where a comparison stands at a line break: one ending its line moves only where - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting - Read by rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, which this decision does not turn on @@ -1576,7 +1576,7 @@ Where `.` stands at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.condition @@ -1585,7 +1585,7 @@ Where a boolean operator chaining a condition of `if`, `elseif`, `while` and `do - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting ### multiline.operatorPosition.logical @@ -1594,7 +1594,7 @@ Where `&&`, `||`, `and`, `or` and `xor` outside a condition stand at a line brea - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.pipe @@ -1603,7 +1603,7 @@ Where `|>` stands at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.ternary @@ -1612,7 +1612,7 @@ Where `?`, `:` and `?:` of a ternary stand at a line break, of a ternary spread - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart` +- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting - Read by rule `DressCode\Rules\Expressions\MultilineTernaryRule`, which this decision does not turn on @@ -1622,7 +1622,7 @@ Where the semicolon of a statement spanning several lines stands. - By default `keep` - Takes: `sameLine` (closing the last line of the statement); `nextLine` (on a line of its own below a chain of method calls spread over lines, closing the last line of any other statement); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting ### multiline.spreadAlways @@ -1631,7 +1631,7 @@ What is spread over lines whatever its width. - By default `[]` - Takes: a list of `promotedProperty` (the parameters of a signature declaring a promoted property) -- The standards: perCs `[]`, psr12 `[]`, nette `[promotedProperty]` +- The standards: perCs `[]`, psr12 `[]`, nette `[promotedProperty]`, symfony `[]` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting ### multiline.spreadItem @@ -1640,7 +1640,7 @@ What a call too wide spreads where an argument could spread in its place; one so - By default `never` - Takes: `never` (the call spreads its arguments); `sole` (a sole argument that is a call, `new` or an array spreads in the parentheses, `foo(bar(` and `))` together); `last` (as `sole`, and so does the last argument after those before it where the line fits up to its bracket, `foo($a, [` and `])` together) -- The standards: perCs `never`, psr12 `never`, nette `sole` +- The standards: perCs `never`, psr12 `never`, nette `sole`, symfony `never` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting ### multiline.trailingComma.argument @@ -1649,7 +1649,7 @@ The trailing comma of the multi-line arguments of a call and the variables of `i - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.array @@ -1658,7 +1658,7 @@ The trailing comma of a multi-line array, a destructuring written with `[...]` i - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.closureUse @@ -1667,7 +1667,7 @@ The trailing comma of a multi-line `use` of a closure; any value but `keep` also - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.import @@ -1676,7 +1676,7 @@ The trailing comma of the names of a multi-line group use; any value but `keep` - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `optional`, psr12 `keep`, nette `required` +- The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.list @@ -1685,7 +1685,7 @@ The trailing comma of a multi-line `list()`; any value but `keep` also removes t - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `optional`, psr12 `keep`, nette `required` +- The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.matchArm @@ -1694,7 +1694,7 @@ The trailing comma of the arms of a multi-line `match`; any value but `keep` als - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.parameter @@ -1703,7 +1703,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ## braces @@ -1714,7 +1714,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\NoAlternativeSyntaxRule`, stage Structure ### braces.bareStatementGroup @@ -1723,7 +1723,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\UselessBracesRule`, stage Structure ### braces.bracelessBody @@ -1732,7 +1732,7 @@ The body of a control structure written without braces, `if ($a) foo();`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\NoBracelessBodiesRule`, stage Structure - Modifies comments @@ -1742,7 +1742,7 @@ How the empty body of an anonymous class is written, one written on one line sta - By default `keep` - Takes: `collapsed` (`{}` on the line of the head, one holding a comment staying on its line as written); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `keep`, nette `keep` +- The standards: perCs `collapsed`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.class @@ -1751,7 +1751,7 @@ How the empty body of a class, an interface, a trait and an enum is written; a c - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.closure @@ -1760,7 +1760,7 @@ How the empty body of a closure is written; a comment inside makes it not empty. - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.controlStructure @@ -1769,7 +1769,7 @@ How the empty body of an `if`, `else`, a loop, `declare`, `try`, `catch` and `fi - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.function @@ -1778,7 +1778,7 @@ How the empty body of a function and a method is written; a comment inside makes - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.propertyHook @@ -1787,7 +1787,7 @@ How the empty body of a property hook is written; a comment inside makes it not - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.anonymousClass @@ -1796,7 +1796,7 @@ Where the `{` of an anonymous class stands, on the next line always where its in - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.class @@ -1805,7 +1805,7 @@ Where the `{` of a class, an interface, a trait and an enum stands, the members - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine` +- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.closure @@ -1814,7 +1814,7 @@ Where the `{` of a closure stands, the body starting a line below it and the `}` - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.continuingKeyword @@ -1823,7 +1823,7 @@ Where the keyword continuing a structure after its `}` stands: `else`, `elseif`, - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.controlStructure @@ -1832,7 +1832,7 @@ Where the `{` of `if`, `else`, a loop, `declare`, `try`, `catch`, `finally`, `sw - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.function @@ -1841,7 +1841,7 @@ Where the `{` of a function and of a method whose parameters stand on one line s - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine` +- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.multilineSignature @@ -1850,7 +1850,7 @@ Where the `{` of a function and of a method whose parameters are spread over lin - By default `keep` - Takes: `sameLine` (on the line of the `)`); `nextLine` (on the line below the `)`); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.typedMultilineSignature @@ -1859,7 +1859,7 @@ Where the `{` of a function and of a method whose parameters are spread over lin - By default `keep` - Takes: `sameLine` (on the line of the return type); `nextLine` (on the line below the return type); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `nextLine` +- The standards: perCs `keep`, psr12 `keep`, nette `nextLine`, symfony `keep` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.singlelineClosure @@ -1868,7 +1868,7 @@ A closure written whole on one line. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `keep` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ## blankLines @@ -1879,7 +1879,7 @@ Between the last group of attributes and the declaration, the member, the parame - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `0`, psr12 `keep`, nette `0` +- The standards: perCs `0`, psr12 `keep`, nette `0`, symfony `0` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterDeclare @@ -1888,7 +1888,7 @@ After `declare(...);`, before whatever follows it but a namespace declaration. - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `keep` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterImports @@ -1897,7 +1897,7 @@ After the last import, before the rest of the code. - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterNamespace @@ -1906,7 +1906,7 @@ After an unbraced namespace declaration. - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterOpeningTag @@ -1915,7 +1915,7 @@ After the line of `bar - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Expressions\InstantiationWrappingRule`, stage Structure - Needs PHP >=8.4 @@ -2279,7 +2279,7 @@ The annotations and the attributes that keep an internal class as it is, an enti - By default `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]` - Takes: a list of names -- The standards: perCs `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, psr12 `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, nette `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]` +- The standards: perCs `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, psr12 `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, nette `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, symfony `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]` - Rule `DressCode\Rules\Classes\FinalForInternalClassRule`, stage Structure ### classes.internalClass.final @@ -2288,7 +2288,7 @@ The `final` of a class its author marked as internal, which nothing outside the - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\FinalForInternalClassRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2298,7 +2298,7 @@ The annotations, with their `@`, and the attributes, by their fully qualified cl - By default `[@internal]` - Takes: a list of names -- The standards: perCs `[@internal]`, psr12 `[@internal]`, nette `[@internal]` +- The standards: perCs `[@internal]`, psr12 `[@internal]`, nette `[@internal]`, symfony `[@internal]` - Rule `DressCode\Rules\Classes\FinalForInternalClassRule`, stage Structure ### classes.members.grouping.constant @@ -2307,7 +2307,7 @@ A declaration of several constants of a class, `const A = 1, B = 2;`. - By default `keep` - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` -- The standards: perCs `separated`, psr12 `keep`, nette `keep` +- The standards: perCs `separated`, psr12 `keep`, nette `keep`, symfony `separated` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure ### classes.members.grouping.property @@ -2316,7 +2316,7 @@ A declaration of several properties, `public $a, $b;`. - By default `keep` - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` -- The standards: perCs `separated`, psr12 `separated`, nette `separated` +- The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure ### classes.members.grouping.traitUse @@ -2325,7 +2325,7 @@ A `use` of several traits, `use A, B;`. - By default `keep` - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` -- The standards: perCs `separated`, psr12 `separated`, nette `separated` +- The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure ### classes.members.order @@ -2334,7 +2334,7 @@ The order of the members of a class by kind, a member taking the most particular - By default `keep` - Takes: an order of `traitUse` (a trait use); `enumCase` (a case of an enum); `constant` (a constant of any visibility); `publicConstant` (a public constant); `protectedConstant` (a protected constant); `privateConstant` (a private constant); `property` (a property of any visibility); `publicProperty` (a public property); `protectedProperty` (a protected property); `privateProperty` (a private property); `publicStaticProperty` (a public static property); `protectedStaticProperty` (a protected static property); `privateStaticProperty` (a private static property); `method` (a method of any visibility); `publicMethod` (a public method); `protectedMethod` (a protected method); `privateMethod` (a private method); `publicStaticMethod` (a public static method); `protectedStaticMethod` (a protected static method); `privateStaticMethod` (a private static method); `constructor` (the constructor); `destructor` (the destructor); `magicMethod` (a method whose name begins with `__`); `keep` -- The standards: perCs `[traitUse]`, psr12 `[traitUse]`, nette `[traitUse, enumCase, publicConstant, protectedConstant, privateConstant, publicProperty, protectedProperty, privateProperty]` +- The standards: perCs `[traitUse]`, psr12 `[traitUse]`, nette `[traitUse, enumCase, publicConstant, protectedConstant, privateConstant, publicProperty, protectedProperty, privateProperty]`, symfony `[traitUse]` - Rule `DressCode\Rules\Classes\MemberOrderRule`, stage Structure ### classes.modifiers.implied @@ -2343,7 +2343,7 @@ A modifier PHP implies anyway, `final` in a final class, `readonly` in a readonl - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Classes\UselessModifierRule`, stage Structure ### classes.modifiers.order @@ -2352,7 +2352,7 @@ The order of the modifiers of a property, a promoted one included, a method, a c - By default `keep` - Takes: `canonical` (`abstract` or `final`, visibility, set visibility, `static`, `readonly`); `keep` -- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical` +- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical` - Rule `DressCode\Rules\Classes\ModifierOrderRule`, stage Structure ### classes.overridingMethod.parameterName @@ -2361,7 +2361,7 @@ The names of the parameters of a method overriding one of a parent class or an i - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\OverridingSignatureRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - Modifies comments @@ -2373,7 +2373,7 @@ A method that only forwards to the parent with the same arguments adds nothing. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Classes\UselessOverridingMethodRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2384,7 +2384,7 @@ The attribute where the method declares the return type of what it implements. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Classes\UselessReturnTypeWillChangeRule`, stage Structure ### classes.overridingMethod.signature @@ -2393,7 +2393,7 @@ The return type, the types of the parameters, the parameters added with a defaul - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\OverridingSignatureRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - Modifies comments @@ -2405,7 +2405,7 @@ A `protected` member of an enum, which is `private`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Classes\PrivateForProtectedRule`, stage Structure ### classes.protectedInFinalClass @@ -2414,7 +2414,7 @@ A `protected` member of a final class with no parent and no trait, which is `pri - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden` - Rule `DressCode\Rules\Classes\PrivateForProtectedRule`, stage Structure ### classes.staticMethod @@ -2423,7 +2423,7 @@ The `static` keyword of a method that does not use `$this`, of the visibilities - By default `keep` - Takes: `required` (always there, `static function`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\StaticForMethodWithoutThisRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2433,7 +2433,7 @@ The visibilities of the methods made static. - By default `[private]` - Takes: a list of `private` (a private method); `protected` (a protected method, where no child and no ancestor can declare it); `public` (a public method, where no child and no ancestor can declare it) -- The standards: perCs `[private]`, psr12 `[private]`, nette `[private]` +- The standards: perCs `[private]`, psr12 `[private]`, nette `[private]`, symfony `[private]` - Rule `DressCode\Rules\Classes\StaticForMethodWithoutThisRule`, stage Structure ### classes.untypedPropertyNullDefault @@ -2442,7 +2442,7 @@ The visibilities of the methods made static. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Classes\UselessNullInitializationRule`, stage Structure ### classes.visibility.constant @@ -2451,7 +2451,7 @@ The visibility of a constant of a class, an interface, a trait or an enum. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.interfaceMethod @@ -2460,7 +2460,7 @@ The `public` of a method of an interface, which is public whatever it says. - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `forbidden` +- The standards: perCs `required`, psr12 `required`, nette `forbidden`, symfony `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.method @@ -2469,7 +2469,7 @@ The visibility of a method outside an interface, whose methods take `interfaceMe - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.property @@ -2478,7 +2478,7 @@ The visibility of a property, a promoted one included, `var` becoming `public` a - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.publicWithSet @@ -2487,7 +2487,7 @@ The `public` of a property with a set visibility, `public protected(set) int $x` - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `required`, nette `required` +- The standards: perCs `keep`, psr12 `required`, nette `required`, symfony `required` - Rule `DressCode\Rules\Classes\PublicWithSetVisibilityRule`, stage Structure - Needs PHP >=8.4 @@ -2501,7 +2501,7 @@ Not an arrow function, whose value is its expression. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments @@ -2511,7 +2511,7 @@ A class constant declares the type of its value. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\ConstantTypeRequiredRule`, stage Structure - Needs PHP >=8.3 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2524,7 +2524,7 @@ Not a parameter of a closure, whose types belong to the call it is passed to. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2535,7 +2535,7 @@ A property declares its type, a promoted one included, taken from `@var`, or the - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2546,7 +2546,7 @@ A function or a method declares its return type, taken from `@return`, `void` wh - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2557,7 +2557,7 @@ How a native type of one type and `null` is written. - By default `keep` - Takes: `'?int'` (a single type with `null` written with `?`); `'int|null'` (a union with `null`, which `types.nullPosition` places); `keep` -- The standards: perCs `'?int'`, psr12 `keep`, nette `'?int'` +- The standards: perCs `'?int'`, psr12 `keep`, nette `'?int'`, symfony `'?int'` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure ### types.nullPosition @@ -2566,7 +2566,7 @@ Where `null` stands in a native union type. - By default `keep` - Takes: `last` (`int|string|null`); `first` (`null|int|string`); `keep` -- The standards: perCs `last`, psr12 `keep`, nette `last` +- The standards: perCs `last`, psr12 `keep`, nette `last`, symfony `last` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure ### types.traversableClasses @@ -2575,7 +2575,7 @@ Classes treated like `array` and `iterable`, whose annotation says what their it - By default `[Traversable]` - Takes: a list of names -- The standards: perCs `[Traversable]`, psr12 `[Traversable]`, nette `[Traversable]` +- The standards: perCs `[Traversable]`, psr12 `[Traversable]`, nette `[Traversable]`, symfony `[Traversable]` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments @@ -2586,7 +2586,7 @@ The order of the types of a native union. - By default `keep` - Takes: `alphabetical` (the types beside `null` sorted by name, case-insensitively, an intersection by its first name); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure ## functions @@ -2597,7 +2597,7 @@ The order of the types of a native union. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `keep` +- The standards: perCs `required`, psr12 `keep`, nette `keep`, symfony `required` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure ### functions.arrowFunction.oneExpression @@ -2608,7 +2608,7 @@ Not about the `static` of the arrow function, which `functions.staticClosure` de - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure ### functions.arrowFunctionNested @@ -2617,7 +2617,7 @@ Whether a closure holding another closure or arrow function becomes one too. - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `yes` +- The standards: perCs `no`, psr12 `no`, nette `yes`, symfony `no` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure ### functions.innerFunction @@ -2626,7 +2626,7 @@ A named function declared inside a function, a method, a closure or a property h - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoInnerFunctionsRule`, stage Structure ### functions.namedBooleanArgument @@ -2635,7 +2635,7 @@ A `true` or `false` passed by position to a parameter declared `bool`, written w - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\NamedArgumentForBooleanRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2645,7 +2645,7 @@ The functions and methods whose arguments stay positional, a method written with - By default `[]` - Takes: a list of names -- The standards: perCs `[]`, psr12 `[]`, nette `[]` +- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]` - Rule `DressCode\Rules\Functions\NamedArgumentForBooleanRule`, stage Structure ### functions.staticClosure @@ -2654,7 +2654,7 @@ The `static` keyword of a closure or an arrow function that does not use `$this` - By default `keep` - Takes: `required` (always there, `static function`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\StaticForClosureWithoutThisRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2664,7 +2664,7 @@ The `static` keyword of a closure or an arrow function that does not use `$this` - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\UselessReturnRule`, stage Structure ### functions.unreachableParameterDefault @@ -2673,7 +2673,7 @@ A default a required parameter after it makes unreachable. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\UselessParameterDefaultRule`, stage Structure ## hygiene @@ -2684,7 +2684,7 @@ The sets of functions whose aliases are written by their canonical names. - By default `keep` - Takes: a list of `all` (every set below); `core` (the core functions, `sizeof()` is `count()`, `join()` is `implode()`); `imap` (`imap_*`); `ldap` (`ldap_*`); `mysqli` (`mysqli_*`); `pg` (`pg_exec()` is `pg_query()`); `oci` (`oci_*`); `odbc` (`odbc_*`); `openssl` (`openssl_*`); `sodium` (`sodium_*`); `ftp` (`ftp_quit()` is `ftp_close()`); `posix` (`posix_errno()` is `posix_get_last_error()`); `pcntl` (`pcntl_errno()` is `pcntl_get_last_error()`); `time` (`mktime()` and `gmmktime()` without arguments are `time()`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[core, imap, pg]` +- The standards: perCs `keep`, psr12 `keep`, nette `[core, imap, pg]`, symfony `keep` - Rule `DressCode\Rules\Functions\NoAliasFunctionsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2694,7 +2694,7 @@ The sets of functions whose aliases are written by their canonical names. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoCallUserFuncRule`, stage Structure - Needs PHP >=7.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2705,7 +2705,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` - Rule `DressCode\Rules\Functions\NoCallUserFuncRule`, stage Structure - Needs PHP >=7.0 @@ -2715,7 +2715,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Classes\ClassNameNotationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2725,7 +2725,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Classes\ClassNameNotationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2737,7 +2737,7 @@ Not about which name of `floatval()` the code writes, `doubleval()` being an ali - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoConversionFunctionsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2747,7 +2747,7 @@ An argument PHP ignores or that repeats the default. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\RedundantArgumentsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2757,7 +2757,7 @@ An argument PHP ignores or that repeats the default. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoDirnameOfFileRule`, stage Structure ### hygiene.is_null @@ -2766,7 +2766,7 @@ An argument PHP ignores or that repeats the default. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoIsNullRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2776,7 +2776,7 @@ A `dirname()` of a `dirname()`, which is one call with the `levels` argument. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoDirnameOfFileRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2786,7 +2786,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\NoRepeatedCatchesRule`, stage Structure - Needs PHP >=7.1 @@ -2796,7 +2796,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Variables\NoSeparateIssetsRule`, stage Structure ### hygiene.separateUnsets @@ -2805,7 +2805,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Variables\NoSeparateUnsetsRule`, stage Structure ### hygiene.settype @@ -2814,7 +2814,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoSettypeRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2824,7 +2824,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoManualEmptyStringTestsRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2835,7 +2835,7 @@ The variable of a `catch` that is never read, `catch (E)` written instead. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\UselessCatchVariableRule`, stage Structure - Needs PHP >=8.0 @@ -2845,7 +2845,7 @@ The variable of a `catch` that is never read, `catch (E)` written instead. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Functions\NoExplicitInvokeCallsRule`, stage Structure ## controlFlow @@ -2858,7 +2858,7 @@ Not about an `if` and an `else` each returning a value, which `controlFlow.ifEls - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\UselessElseRule`, stage Structure ### controlFlow.elseif @@ -2867,7 +2867,7 @@ The keyword of a branch that follows `if`. - By default `keep` - Takes: `'elseif'` (`elseif` in one word, never `else if`); `keep` -- The standards: perCs `'elseif'`, psr12 `'elseif'`, nette `'elseif'` +- The standards: perCs `'elseif'`, psr12 `'elseif'`, nette `'elseif'`, symfony `'elseif'` - Rule `DressCode\Rules\ControlFlow\ElseifNotationRule`, stage Structure ### controlFlow.elseifAfterReturn @@ -2876,7 +2876,7 @@ An `elseif` after an `if` that always leaves, which becomes an `if` of its own w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\UselessElseRule`, stage Structure ### controlFlow.emptyStatement @@ -2885,7 +2885,7 @@ The second `;` of `foo();;`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\NoEmptyStatementsRule`, stage Structure ### controlFlow.ifElseReturning @@ -2896,7 +2896,7 @@ Not about the ternary of `true` and `false` it may write, which `expressions.ter - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\TernaryForIfRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2908,7 +2908,7 @@ Not about branches returning other values, which `controlFlow.ifElseReturning` w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\ReturnForBooleanIfRule`, stage Structure ### controlFlow.switch.fallThroughComment @@ -2917,7 +2917,7 @@ The text of the comment a non-empty `case` falling through to the next one carri - By default `keep` - Takes: a text, or a list of them, every one passing and the first written; `keep` -- The standards: perCs `no break`, psr12 `no break`, nette `break omitted` +- The standards: perCs `no break`, psr12 `no break`, nette `break omitted`, symfony `no break` - Rule `DressCode\Rules\ControlFlow\FallThroughCommentRule`, stage Structure - Modifies comments @@ -2927,7 +2927,7 @@ An `if` ending a function or a loop body becomes a guard that leaves early, and - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\EarlyExitForTrailingIfRule`, stage Structure ### controlFlow.trailingIfMinStatements @@ -2936,7 +2936,7 @@ The statements the body of a trailing `if` has at least for it to become a guard - By default `2` - Takes: a count from 1 -- The standards: perCs `2`, psr12 `2`, nette `2` +- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2` - Rule `DressCode\Rules\ControlFlow\EarlyExitForTrailingIfRule`, stage Structure ## expressions @@ -2947,7 +2947,7 @@ The statements the body of a trailing `if` has at least for it to become a guard - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Expressions\IncrementRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2957,7 +2957,7 @@ An increment or a decrement whose value is not read, a statement or a step of `f - By default `keep` - Takes: `prefix` (`++$i`, `--$i`); `postfix` (`$i++`, `$i--`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `postfix` +- The standards: perCs `keep`, psr12 `keep`, nette `postfix`, symfony `prefix` - Rule `DressCode\Rules\Expressions\IncrementRule`, stage Structure ### expressions.assignment.targetInValue @@ -2968,7 +2968,7 @@ Not about the `$a += 1` it may write, which `expressions.assignment.addingOne` w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Expressions\CombinedAssignmentForRepeatedTargetRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2978,7 +2978,7 @@ The type name of a cast. - By default `keep` - Takes: `canonical` (`(int)`, `(bool)`, `(float)`, `(string)` in lowercase, never `(integer)`, `(Boolean)`, `(double)`, `(binary)`); `keep` -- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical` +- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical` - Rule `DressCode\Rules\Expressions\CastCanonicalTypeRule`, stage Structure ### expressions.coalesce @@ -2987,7 +2987,7 @@ The type name of a cast. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Expressions\NullCoalescingForNullTernaryRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2997,7 +2997,7 @@ A loose comparison, `==`, `!=` or `<>`, is written `===` or `!==`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Expressions\NoLooseComparisonsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3009,7 +3009,7 @@ Not about whether the comparison is loose, which `expressions.comparison.loose` - By default `keep` - Takes: `'!='` (`!=`, never `<>`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'!='` +- The standards: perCs `keep`, psr12 `keep`, nette `'!='`, symfony `'!='` - Rule `DressCode\Rules\Expressions\NotEqualsNotationRule`, stage Structure ### expressions.comparison.yoda @@ -3018,7 +3018,7 @@ A Yoda comparison, the constant on its left side, a comparison of two variables - By default `keep` - Takes: `forbidden` (never there, the variable on the left and the constant on the right, `$a === 1`); `required` (always there, the constant on the left, `1 === $a`, so that an accidental assignment does not compile); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `required` - Rule `DressCode\Rules\Expressions\YodaRule`, stage Structure ### expressions.doubleNegation @@ -3027,7 +3027,7 @@ A Yoda comparison, the constant on its left side, a comparison of two variables - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Expressions\NoDoubleNegationsRule`, stage Structure ### expressions.explicitPrecedence @@ -3036,7 +3036,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Expressions\ExplicitPrecedenceRequiredRule`, stage Structure ### expressions.parenthesesAfterConstruct @@ -3045,7 +3045,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Expressions\UselessParenthesesAfterConstructRule`, stage Structure ### expressions.ternary.ofTrueFalse @@ -3054,7 +3054,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Expressions\UselessTernaryOperatorRule`, stage Structure ### expressions.ternary.short @@ -3063,7 +3063,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Expressions\ShortTernaryRule`, stage Structure ### expressions.wordLogicalOperator @@ -3072,7 +3072,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Expressions\LogicalOperatorNotationRule`, stage Structure ## literals @@ -3083,7 +3083,7 @@ The syntax of an array. - By default `keep` - Takes: `short` (`[1, 2]`, never `array(1, 2)`); `long` (`array(1, 2)`, never `[1, 2]`); `keep` -- The standards: perCs `short`, psr12 `keep`, nette `short` +- The standards: perCs `short`, psr12 `keep`, nette `short`, symfony `short` - Rule `DressCode\Rules\Arrays\ArraySyntaxRule`, stage Structure ### literals.arraySyntax.list @@ -3092,7 +3092,7 @@ The syntax of a destructuring. - By default `keep` - Takes: `short` (`[$a, $b] =`, never `list($a, $b) =`); `long` (`list($a, $b) =`, never `[$a, $b] =`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `short` +- The standards: perCs `keep`, psr12 `keep`, nette `short`, symfony `keep` - Rule `DressCode\Rules\Arrays\ArraySyntaxRule`, stage Structure ### literals.classNameInString @@ -3101,7 +3101,7 @@ The syntax of a destructuring. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Classes\ClassKeywordForStringRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3111,7 +3111,7 @@ Two string literals concatenated on one line, which are one literal, `'ab'` for - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure ### literals.concatenatedLiteralsOverLines @@ -3120,7 +3120,7 @@ Whether two string literals concatenated over a line break are one literal too. - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure ### literals.digitSeparator.fraction @@ -3129,7 +3129,7 @@ The separator grouping by three the digits of the fraction of a decimal number, - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.digitSeparator.integer @@ -3138,7 +3138,7 @@ The separator grouping by three the digits of a decimal integer, or of the integ - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.digitSeparatorMinDigits.fraction @@ -3147,7 +3147,7 @@ The number of digits from which `literals.digitSeparator.fraction` groups them. - By default `4` - Takes: a count from 1 -- The standards: perCs `4`, psr12 `4`, nette `20` +- The standards: perCs `4`, psr12 `4`, nette `20`, symfony `4` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.digitSeparatorMinDigits.integer @@ -3156,7 +3156,7 @@ The number of digits from which `literals.digitSeparator.integer` groups them. - By default `4` - Takes: a count from 1 -- The standards: perCs `4`, psr12 `4`, nette `7` +- The standards: perCs `4`, psr12 `4`, nette `7`, symfony `4` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.emptyStringConcatenation @@ -3165,7 +3165,7 @@ The concatenation with an empty string, which is a cast or nothing, `(string) $x - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure ### literals.heredocWithoutInterpolation @@ -3174,7 +3174,7 @@ A heredoc with nothing to interpolate, which is written as a nowdoc. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Literals\NowdocForHeredocRule`, stage Structure ### literals.implicitBackslash @@ -3183,7 +3183,7 @@ A backslash of a string that escapes nothing, written `\\` instead. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Literals\NoImplicitBackslashesRule`, stage Structure ### literals.quotes @@ -3192,7 +3192,7 @@ The quotes of a plain string, one without interpolation, without an escape seque - By default `keep` - Takes: `single` (`'text'` where double quotes gain nothing); `double` (`"text"` for every plain string, the backslashes of a single-quoted one escaped); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `single` +- The standards: perCs `keep`, psr12 `keep`, nette `single`, symfony `single` - Rule `DressCode\Rules\Literals\StringQuotesRule`, stage Structure ## comments @@ -3203,7 +3203,7 @@ An empty comment, which is removed. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Comments\NoEmptyCommentsRule`, stage Finishing - Modifies comments @@ -3213,7 +3213,7 @@ The marker of a single-line comment. - By default `keep` - Takes: `'//'` (`//`, never `#`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'//'` +- The standards: perCs `keep`, psr12 `keep`, nette `'//'`, symfony `'//'` - Rule `DressCode\Rules\Comments\NoHashCommentsRule`, stage Finishing - Modifies comments @@ -3225,7 +3225,7 @@ The doc comment stands above the attributes. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocAboveAttributesRule`, stage Structure - Modifies comments @@ -3235,7 +3235,7 @@ The letter case of a known annotation. - By default `keep` - Takes: `canonical` (as it is known, `@inheritDoc`, `@phpstan-var`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `canonical` +- The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\AnnotationCasingRule`, stage Structure - Modifies comments @@ -3245,7 +3245,7 @@ A `@var` on a class constant that says nothing. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\UselessConstantVarAnnotationRule`, stage Finishing - Modifies comments @@ -3255,7 +3255,7 @@ A second `@param` of one parameter. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.duplicateReturn @@ -3264,7 +3264,7 @@ A second `@return` of a function. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.duplicateVar @@ -3273,7 +3273,7 @@ A second `@var` of a property. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.empty @@ -3282,7 +3282,7 @@ A `@param`, `@return`, `@var` or `@see` without content. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.extraParam @@ -3291,7 +3291,7 @@ A `@param` naming a parameter the function does not have. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.inlineVar @@ -3300,7 +3300,7 @@ An inline `@var` says what `assert($x instanceof Foo)` checks. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\AssertForInlineVarRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3311,7 +3311,7 @@ An array, iterable or traversable declaration has an annotation saying what its - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments @@ -3321,7 +3321,7 @@ A `@param`, `@return` or `@var` without a description that says only the native - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\UselessTypeAnnotationsRule`, stage Structure - Modifies comments @@ -3331,7 +3331,7 @@ An annotation in a plain comment, `/* @var */` or `/**@var` without the blank, w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoPlainPhpdocsRule`, stage Finishing - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3342,7 +3342,7 @@ The blank lines inside a doc comment. - By default `keep` - Takes: `trimmed` (none at the start or the end, at most one in a row inside); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `trimmed` +- The standards: perCs `keep`, psr12 `keep`, nette `trimmed`, symfony `trimmed` - Rule `DressCode\Rules\PhpDoc\PhpdocBlankLinesRule`, stage Finishing - Modifies comments @@ -3352,7 +3352,7 @@ An empty doc comment, which is removed. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Comments\NoEmptyCommentsRule`, stage Finishing - Modifies comments @@ -3362,7 +3362,7 @@ The annotations removed from doc comments, written with the `@`, such as `@autho - By default `keep` - Takes: a list of annotations, each with its `@`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[@access, @author, @copyright, @created, @license, @package, @since, @subpackage, @todo, @version]` +- The standards: perCs `keep`, psr12 `keep`, nette `[@access, @author, @copyright, @created, @license, @package, @since, @subpackage, @todo, @version]`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\ForbiddenAnnotationsRule`, stage Structure - Modifies comments @@ -3372,7 +3372,7 @@ The patterns of the lines of a description removed from doc comments, such as `~ - By default `keep` - Takes: a list of regular expressions; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `['~^(?:(?!private|protected|static)\S+ )?(?:con|de)structor\.\z~i', '~^\S+ [gs]etter\.\z~i']` +- The standards: perCs `keep`, psr12 `keep`, nette `['~^(?:(?!private|protected|static)\S+ )?(?:con|de)structor\.\z~i', '~^\S+ [gs]etter\.\z~i']`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\ForbiddenPhpdocLinesRule`, stage Structure - Modifies comments @@ -3382,7 +3382,7 @@ A doc comment of `@inheritDoc` alone. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\UselessInheritdocRule`, stage Structure - Modifies comments @@ -3392,7 +3392,7 @@ A property doc comment with a single annotation and no description. - By default `keep` - Takes: `collapsed` (on one line, `/** @var int */`); `expanded` (over three lines, the annotation on the one in the middle); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `collapsed` +- The standards: perCs `keep`, psr12 `keep`, nette `collapsed`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\PropertyPhpdocLineSpanRule`, stage Formatting - Modifies comments @@ -3402,7 +3402,7 @@ A plain comment describing a property, `//` or `/* */` where `/** */` belongs, r - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoPlainPhpdocsRule`, stage Finishing - Modifies comments @@ -3412,7 +3412,7 @@ The annotation of a promoted property. - By default `keep` - Takes: `'@var'` (a `@var` at the property, not a `@param` of the constructor); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'@var'` +- The standards: perCs `keep`, psr12 `keep`, nette `'@var'`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\PromotedPropertyAnnotationPositionRule`, stage Structure - Modifies comments @@ -3422,7 +3422,7 @@ The `*` opening each line of a doc comment. - By default `keep` - Takes: `aligned` (each `*` below the first `*` of the opening `/**`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `aligned` +- The standards: perCs `keep`, psr12 `keep`, nette `aligned`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocAlignmentRule`, stage Formatting - Modifies comments @@ -3432,7 +3432,7 @@ How an array type of a doc comment is written; `list` is a different type and - By default `keep` - Takes: `'array'` (the generic notation); `'T[]'` (the brackets); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3442,7 +3442,7 @@ How a built-in type in a doc comment is written, `int` and never `integer` or `I - By default `keep` - Takes: `canonical` (the short lowercase name of a built-in type, `never` for `no-return`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `canonical` +- The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `canonical` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3452,7 +3452,7 @@ A type a union of a doc comment names twice, `int|integer`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3462,7 +3462,7 @@ How a type of a doc comment of one type and `null` is written. - By default `keep` - Takes: `'?T'` (a single type with `null` written with `?`); `'T|null'` (a union with `null`, which `phpdoc.types.nullPosition` places); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3472,7 +3472,7 @@ Where `null` stands in a union type of a doc comment. - By default `keep` - Takes: `last` (`int|string|null`); `first` (`null|int|string`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `last` +- The standards: perCs `keep`, psr12 `keep`, nette `last`, symfony `last` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3482,7 +3482,7 @@ The order of the types of a union type of a doc comment. - By default `keep` - Takes: `alphabetical` (the types beside `null` sorted by name, case-insensitively); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3494,7 +3494,7 @@ A doc comment followed by another one, the first lost to reflection, which is on - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\NoConsecutivePhpdocsRule`, stage Finishing ### mistakes.continueInSwitch @@ -3503,7 +3503,7 @@ A `continue` that only leaves a `switch`, where `break` is meant, which is only - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\NoContinueInSwitchRule`, stage Structure ### mistakes.debugOutput @@ -3512,7 +3512,7 @@ A statement calling a debugging function to print, changed into a comment, `// v - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\NoDebugOutputRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3523,7 +3523,7 @@ The debugging functions whose calls print. - By default `[print_r, var_dump, var_export]` - Takes: a list of names -- The standards: perCs `[print_r, var_dump, var_export]`, psr12 `[print_r, var_dump, var_export]`, nette `[print_r, var_dump, var_export]` +- The standards: perCs `[print_r, var_dump, var_export]`, psr12 `[print_r, var_dump, var_export]`, nette `[print_r, var_dump, var_export]`, symfony `[print_r, var_dump, var_export]` - Rule `DressCode\Rules\Functions\NoDebugOutputRule`, stage Structure - Modifies comments @@ -3533,7 +3533,7 @@ The `@` operator, which silences errors and is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Expressions\NoErrorSuppressionRule`, stage Structure ### mistakes.exceptionWhereThrowableBelongs @@ -3542,7 +3542,7 @@ The `@` operator, which silences errors and is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\ThrowableForExceptionRule`, stage Structure ### mistakes.finalParent @@ -3551,7 +3551,7 @@ A class extending a final one, which is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\NoFinalParentRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3561,7 +3561,7 @@ The `global` statement, which is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Variables\NoGlobalStatementsRule`, stage Structure ### mistakes.invisibleCharacter @@ -3570,7 +3570,7 @@ Non-breaking, zero-width and bidi characters, replaced in comments and escaped i - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Files\NoInvisibleCharactersRule`, stage Structure - Modifies comments @@ -3580,7 +3580,7 @@ The return type `never` on a closure that always throws or exits, though its sig - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Types\NeverForThrowingFunctionRule`, stage Structure - Needs PHP >=8.1 @@ -3590,7 +3590,7 @@ The return type `never` of PHP 8.1 on a function that always throws or exits. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Types\NeverForThrowingFunctionRule`, stage Structure - Needs PHP >=8.1 @@ -3600,7 +3600,7 @@ The return type `never` on a method that always throws or exits, only where no d - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Types\NeverForThrowingFunctionRule`, stage Structure - Needs PHP >=8.1 @@ -3610,7 +3610,7 @@ A property written only in the constructor, or not at all. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\ReadonlyForUnwrittenPropertyRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3621,7 +3621,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Variables\NoRepeatedAssignmentsRule`, stage Structure ### mistakes.staticSetState @@ -3630,7 +3630,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Classes\StaticSetStateRequiredRule`, stage Structure ### mistakes.strictArgument @@ -3639,7 +3639,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Functions\StrictComparisonArgumentRequiredRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3649,7 +3649,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Classes\NoThisOutsideObjectRule`, stage Structure ### mistakes.trailingWhitespaceInString @@ -3658,7 +3658,7 @@ At the end of the lines of a multi-line string or inline HTML, which changes the - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Literals\NoTrailingWhitespaceInStringRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3668,7 +3668,7 @@ An inherited abstract method a class that is not abstract leaves unimplemented, - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\NoUnimplementedAbstractMethodsRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3678,7 +3678,7 @@ A `catch` after one catching `Throwable` or a class it catches, which is only re - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\NoUnreachableCatchesRule`, stage Structure ### mistakes.__debugInfoReturningNull @@ -3687,7 +3687,7 @@ The `null` that `__debugInfo()` returns, which becomes `[]`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Classes\NoNullDebugInfoRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3697,7 +3697,7 @@ The `null` that `__debugInfo()` returns, which becomes `[]`. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\SerializeMethodsRequiredRule`, stage Structure ## upgrading @@ -3708,7 +3708,7 @@ An attribute written for an annotation (`@ORM\Entity: ORM\Entity`). - By default `keep` - Takes: a map of names and patterns with `*` to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\AttributeForAnnotationRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on @@ -3720,7 +3720,7 @@ An attribute written for a member the class declares by convention. - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\AttributeForMemberRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on - Modifies comments @@ -3731,7 +3731,7 @@ A class that may not be used, with what to do instead (`Acme\Legacy\Db: "use the - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on @@ -3741,7 +3741,7 @@ A global constant that may not be used, with what to do instead (`E_STRICT: "it - By default `keep` - Takes: a map of names and patterns with `*` to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure ### upgrading.api.forbiddenFunctions @@ -3750,7 +3750,7 @@ A function that may not be called, with what to do instead. - By default `keep` - Takes: a map of names and patterns with `*` to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedFunctionsRule`, which this decision does not turn on @@ -3760,7 +3760,7 @@ A constant, a method or a property that may not be used, with what to do instead - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenMembersRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, which this decision does not turn on @@ -3773,7 +3773,7 @@ A call written as the template says (`Acme\Mail\Mailer::send($to, $body): send(n - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedFunctionCallsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, which this decision does not turn on @@ -3786,7 +3786,7 @@ A class written instead of another one, both fully qualified (`Acme\Old\Mailer: - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on - Modifies comments @@ -3797,7 +3797,7 @@ A function written instead of another one (`acme_send: Acme\Mail\send`). - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ReplacedFunctionsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedFunctionsRule`, which this decision does not turn on - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3808,7 +3808,7 @@ A constant, a method or a property written instead of another one of the class ( - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedMembersRule`, which this decision does not turn on @@ -3821,7 +3821,7 @@ A constant, a method or a property written instead of another one of the class ( - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\OverrideAttributeRequiredRule`, stage Structure - Needs PHP >=8.3 - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3832,7 +3832,7 @@ A constant, a method or a property written instead of another one of the class ( - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\SensitiveParameterRequiredRule`, stage Structure - Needs PHP >=8.2 @@ -3842,7 +3842,7 @@ The names of the parameters holding a secret, whatever their letter case and und - By default `[password, passphrase, secret, apiKey, privateKey]` - Takes: a list of names -- The standards: perCs `[password, passphrase, secret, apiKey, privateKey]`, psr12 `[password, passphrase, secret, apiKey, privateKey]`, nette `[password, passphrase, secret, apiKey, privateKey]` +- The standards: perCs `[password, passphrase, secret, apiKey, privateKey]`, psr12 `[password, passphrase, secret, apiKey, privateKey]`, nette `[password, passphrase, secret, apiKey, privateKey]`, symfony `[password, passphrase, secret, apiKey, privateKey]` - Rule `DressCode\Rules\Functions\SensitiveParameterRequiredRule`, stage Structure - Needs PHP >=8.2 @@ -3852,7 +3852,7 @@ A class with `__toString()` declares it. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\StringableRequiredRule`, stage Structure - Needs PHP >=8.0 @@ -3862,7 +3862,7 @@ A class whose declaration is `@deprecated`, replaced by the class the deprecatio - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - Modifies comments @@ -3874,7 +3874,7 @@ A function whose declaration is `@deprecated`, replaced by the function the depr - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedFunctionsRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3885,7 +3885,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedMembersRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3896,7 +3896,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\ArrayFirstLastNotationRule`, stage Structure - Needs PHP >=8.5 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3907,7 +3907,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\ArraySearchForFilterRule`, stage Structure - Rule `DressCode\Rules\ControlFlow\ArraySearchForForeachRule`, stage Structure - Needs PHP >=8.4 @@ -3919,7 +3919,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Arrays\NoManualListTestsRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3930,7 +3930,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\ClampForMinMaxRule`, stage Structure - Needs PHP >=8.6 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3941,7 +3941,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\GetDebugTypeForTernaryRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3952,7 +3952,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\JsonValidateForDecodeRule`, stage Structure - Needs PHP >=8.3 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3963,7 +3963,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\RoundingModeNotationRule`, stage Structure - Needs PHP >=8.4 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3974,7 +3974,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\NoManualSubstringTestsRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3985,7 +3985,7 @@ Whether the code follows the upgrading files of the installed packages, their ma - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` - Rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedFunctionsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, stage Structure @@ -4003,7 +4003,7 @@ The backtick operator PHP 8.5 deprecated, written as a `shell_exec()` call. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Expressions\NoBacktickOperatorsRule`, stage Structure ### upgrading.php.caseSemicolon @@ -4012,7 +4012,7 @@ The backtick operator PHP 8.5 deprecated, written as a `shell_exec()` call. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\ControlFlow\SwitchCaseNotationRule`, stage Structure ### upgrading.php.constructorReturnValue @@ -4021,7 +4021,7 @@ A `return` with a value in a constructor or destructor, deprecated by PHP 8.6. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\NoConstructorReturnValuesRule`, stage Structure ### upgrading.php.csvEscapeArgument @@ -4030,7 +4030,7 @@ The `escape` argument of `fgetcsv()` and kin written out, since PHP 8.4 deprecat - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\CsvEscapeArgumentRequiredRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4040,7 +4040,7 @@ A function, a constant or a member PHP deprecated, written as what replaces it a - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedFunctionCallsRule`, stage Structure @@ -4054,7 +4054,7 @@ An argument whose value PHP deprecated is reported, `E_USER_ERROR` given to `tri - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedPhpArgumentsRule`, stage Structure ### upgrading.php.dollarBraceInterpolation @@ -4063,7 +4063,7 @@ An argument whose value PHP deprecated is reported, `E_USER_ERROR` given to `tri - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Literals\NoDollarBraceInterpolationsRule`, stage Structure ### upgrading.php.implicitNullable @@ -4072,7 +4072,7 @@ A parameter type made nullable by its default `null` alone, `int $x = null`, dep - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` - Rule `DressCode\Rules\Types\NullableTypeForDefaultNullRule`, stage Structure ### upgrading.php.nullArrayKey @@ -4081,7 +4081,7 @@ A parameter type made nullable by its default `null` alone, `int $x = null`, dep - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Arrays\NoNullArrayKeysRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4091,7 +4091,7 @@ Names PHP 8.6 reserved, in declarations and imports. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Namespaces\NoReservedNamesRule`, stage Structure ### upgrading.php.returnInFinally @@ -4100,7 +4100,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\NoReturnsInFinallyRule`, stage Structure ### upgrading.php.sleepAndWakeup @@ -4109,7 +4109,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\NoSleepAndWakeupRule`, stage Structure ### upgrading.phpdoc.deprecated @@ -4118,7 +4118,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\PhpDoc\DeprecatedAttributeForAnnotationRule`, stage Structure - Needs PHP >=8.4 - Modifies comments @@ -4130,7 +4130,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\ReadonlyForAnnotationRule`, stage Structure - Needs PHP >=8.1 - Modifies comments @@ -4142,7 +4142,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\CloneWithNotationRule`, stage Structure - Needs PHP >=8.5 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4153,7 +4153,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\ClassConstantForConstantCallRule`, stage Structure - Needs PHP >=8.3 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4164,7 +4164,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\CallableNotationRule`, stage Structure - Rule `DressCode\Rules\Functions\FirstClassCallableForStringRule`, stage Structure - Needs PHP >=8.1 @@ -4176,7 +4176,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\MatchForSwitchRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4187,7 +4187,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Functions\NewInitializerForNullDefaultRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4198,7 +4198,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\NullsafeForGuardedAccessRule`, stage Structure - Needs PHP >=8.0 @@ -4208,7 +4208,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` - Rule `DressCode\Rules\Literals\OctalNotationRule`, stage Structure - Needs PHP >=8.1 @@ -4218,7 +4218,7 @@ Calls nested one in another, each handing its result to the next, written with t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Expressions\PipeForNestedCallsRule`, stage Structure - Needs PHP >=8.5 @@ -4228,7 +4228,7 @@ The calls a nest has at least for the pipe operator to be written. - By default `3` - Takes: a count from 2 -- The standards: perCs `3`, psr12 `3`, nette `3` +- The standards: perCs `3`, psr12 `3`, nette `3`, symfony `3` - Rule `DressCode\Rules\Expressions\PipeForNestedCallsRule`, stage Structure - Needs PHP >=8.5 @@ -4238,7 +4238,7 @@ A constructor parameter only assigned to a property. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\PromotedPropertyForAssignmentRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4249,7 +4249,7 @@ A class whose every property is readonly. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Classes\ReadonlyClassForReadonlyPropertiesRule`, stage Structure - Needs PHP >=8.2 @@ -4259,7 +4259,7 @@ A class whose every property is readonly. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\Arrays\SpreadForArrayMergeRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4270,7 +4270,7 @@ A class whose every property is readonly. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` - Rule `DressCode\Rules\ControlFlow\ThrowExpressionForNullGuardRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4283,7 +4283,7 @@ The constants the namespaces of the project declare, each of them listed once th - A fact of the project, which its rule guards - Takes: a list of names -- The standards: perCs `[]`, psr12 `[]`, nette `[]` +- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]` - Rule `DressCode\Rules\Namespaces\NoUnlistedNamespacedDeclarationsRule`, stage Structure ### namespaces.functions @@ -4292,5 +4292,5 @@ The functions the namespaces of the project declare, each of them listed once th - A fact of the project, which its rule guards - Takes: a list of names -- The standards: perCs `[]`, psr12 `[]`, nette `[]` +- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]` - Rule `DressCode\Rules\Namespaces\NoUnlistedNamespacedDeclarationsRule`, stage Structure diff --git a/docs/reference/generate.php b/docs/reference/generate.php index 137af10a4..0002d3ec2 100644 --- a/docs/reference/generate.php +++ b/docs/reference/generate.php @@ -66,7 +66,7 @@ function formatValue(DressCode\Value $value): string $catalogue = $resolver->getCatalogue(); $riskyDecisions = findRiskyDecisions(); $standards = []; -foreach (['perCs', 'psr12', 'nette'] as $standard) { +foreach (['perCs', 'psr12', 'nette', 'symfony'] as $standard) { $standards[$standard] = $resolver->resolve(new Config(use: ["dresscode/$standard"]), Config::DefaultPhpVersion)->decisions; } @@ -187,7 +187,7 @@ function formatValue(DressCode\Value $value): string $presets = $registry->presets; $out = "# Presets\n\nGenerated by `composer reference` from the preset files; do not edit. " . count($presets) . " presets.\n\n" - . "A project takes one of the standards (`dresscode/perCs`, `dresscode/psr12`, `dresscode/nette`)\n" + . "A project takes one of the standards (`dresscode/perCs`, `dresscode/psr12`, `dresscode/nette`, `dresscode/symfony`)\n" . "as its base, and the description of every other preset says what it adds to it.\n\n" . "| Preset | Builds on | |\n|---|---|---|\n"; foreach ($presets as $name => $file) { diff --git a/docs/reference/presets.md b/docs/reference/presets.md index 48673b774..93338153d 100644 --- a/docs/reference/presets.md +++ b/docs/reference/presets.md @@ -1,8 +1,8 @@ # Presets -Generated by `composer reference` from the preset files; do not edit. 9 presets. +Generated by `composer reference` from the preset files; do not edit. 10 presets. -A project takes one of the standards (`dresscode/perCs`, `dresscode/psr12`, `dresscode/nette`) +A project takes one of the standards (`dresscode/perCs`, `dresscode/psr12`, `dresscode/nette`, `dresscode/symfony`) as its base, and the description of every other preset says what it adds to it. | Preset | Builds on | | @@ -10,6 +10,7 @@ as its base, and the description of every other preset says what it adds to it. | `dresscode/perCs` | `dresscode/psr12` | PER Coding Style 3.1 | | `dresscode/psr12` | | PSR-12 Extended Coding Style | | `dresscode/nette` | `dresscode/perCs`, `dresscode/cleanup`, `dresscode/types`, `dresscode/correctness` | Nette Coding Standard | +| `dresscode/symfony` | `dresscode/perCs` | Symfony Coding Standards | | `dresscode/cleanup` | | Cleanup | | `dresscode/compilerOptimizations` | | Compiler optimizations | | `dresscode/correctness` | | Correctness | diff --git a/src/Config/ConfigResolver.php b/src/Config/ConfigResolver.php index f33f067b1..3cc5933c4 100644 --- a/src/Config/ConfigResolver.php +++ b/src/Config/ConfigResolver.php @@ -246,7 +246,7 @@ public function resolve( /** - * The decisions each of the three standards makes, resolved alone. + * The decisions each of the four standards makes, resolved alone. * @return array> standard => path => its decision * @throws ConfigurationException */ diff --git a/src/Config/CorePlugin.php b/src/Config/CorePlugin.php index f8afde371..dba3cbd90 100644 --- a/src/Config/CorePlugin.php +++ b/src/Config/CorePlugin.php @@ -26,7 +26,7 @@ final class CorePlugin implements Plugin /** * The presets that are complete standards; the other presets are sets. */ - public const Standards = ['perCs', 'psr12', 'nette']; + public const Standards = ['perCs', 'psr12', 'nette', 'symfony']; private const QualifiedName = '\\\\?\w+(\\\\\w+)*'; @@ -39,6 +39,7 @@ public function getManifest(): PluginManifest 'dresscode/perCs' => dirname(__DIR__) . '/Presets/perCs.neon', 'dresscode/psr12' => dirname(__DIR__) . '/Presets/psr12.neon', 'dresscode/nette' => dirname(__DIR__) . '/Presets/nette.neon', + 'dresscode/symfony' => dirname(__DIR__) . '/Presets/symfony.neon', 'dresscode/cleanup' => dirname(__DIR__) . '/Presets/cleanup.neon', 'dresscode/compilerOptimizations' => dirname(__DIR__) . '/Presets/compilerOptimizations.neon', 'dresscode/correctness' => dirname(__DIR__) . '/Presets/correctness.neon', diff --git a/src/Config/Layer.php b/src/Config/Layer.php index 21fe7de32..fd55afe0e 100644 --- a/src/Config/Layer.php +++ b/src/Config/Layer.php @@ -35,7 +35,8 @@ public function describe(): string LayerKind::Override => "the override for $this->name", LayerKind::Preset => (string) $this->name, LayerKind::Package => match (true) { - $this->package === null => (string) $this->name, + $this->name === null => "DressCode for $this->package", + $this->package === null => $this->name, default => "$this->name of $this->package", }, }; diff --git a/src/Config/PackageDiscovery.php b/src/Config/PackageDiscovery.php index 79ae52d2c..7e5328fb2 100644 --- a/src/Config/PackageDiscovery.php +++ b/src/Config/PackageDiscovery.php @@ -8,7 +8,7 @@ namespace DressCode\Config; use Composer\InstalledVersions; -use DressCode\{ConfigurationException, Plugin}; +use DressCode\{ConfigurationException, Plugin, Profile}; use DressCode\Engine\Helpers; use function array_key_exists, is_array, is_string; @@ -38,6 +38,19 @@ /** the requirement that lays the upgrading data of PHP, under which what they say is reported */ public const PhpDecision = 'upgrading.php.deprecatedApi'; + /** + * What packages declare in their namespaces for the files a project writes into them, which a package does not say + * itself yet: the container configurator of Symfony, whose functions the files that configure the services call + * without importing them. + */ + private const Known = [ + 'symfony/dependency-injection' => [ + 'functions' => [ + 'Symfony\Component\DependencyInjection\Loader\Configurator\{abstract_arg, closure, env, expr, inline_service, iterator, lazy_proxy, param, service, service_closure, service_locator, tagged_class_map, tagged_iterator, tagged_locator}', + ], + ], + ]; + private function __construct( /** @var list the root package first */ @@ -95,6 +108,17 @@ public static function discover(ProjectPackages $project): self } } + foreach (self::Known as $package => $namespaces) { + if ($project->has($package)) { + $upgradingData[] = new UpgradingData( + new Layer(LayerKind::Package, package: $package), + $package, + new Profile(namespaces: $namespaces)->namespaces, + [], + ); + } + } + $php = self::readPhpData($project); return new self($php === null ? $upgradingData : [...$upgradingData, $php], $plugins, $warnings); } diff --git a/src/Config/UpgradingData.php b/src/Config/UpgradingData.php index 872052bed..e9737fd9b 100644 --- a/src/Config/UpgradingData.php +++ b/src/Config/UpgradingData.php @@ -12,9 +12,9 @@ /** - * What one upgrading file says to the project: the maps of the sections the lowest version the code must work with - * reaches, those of the later ones it may run on as what they retire, the rules of the plugin of the package it turns - * on, and what the package declares in its namespaces. + * What one upgrading file, or what DressCode itself knows of a package, says to the project: the maps of the sections + * the lowest version the code must work with reaches, those of the later ones it may run on as what they retire, the + * rules of the plugin of the package it turns on, and what the package declares in its namespaces. * @internal */ final readonly class UpgradingData diff --git a/src/Config/enums.php b/src/Config/enums.php index 9ca79f66b..46c2b04f2 100644 --- a/src/Config/enums.php +++ b/src/Config/enums.php @@ -16,7 +16,7 @@ enum LayerKind case Override; case Preset; - /** an upgrading file of a package */ + /** an upgrading file of a package, or what DressCode itself knows of one */ case Package; /** a name a caller asks about, outside every layer of the configuration */ diff --git a/src/Presets/symfony.neon b/src/Presets/symfony.neon new file mode 100644 index 000000000..13f16084e --- /dev/null +++ b/src/Presets/symfony.neon @@ -0,0 +1,151 @@ +# Symfony Coding Standards +# +# The Symfony Coding Standards as the @Symfony rule set of PHP CS Fixer defines them, without most of its rules about +# phpDoc, of which it takes only an empty doc comment, the trimming and the canonical types: PER Coding Style, which the +# set builds on, with the casing, the imports, the blank lines and the whitespace Symfony adds, and without the rules +# that would break a construct over lines, which Symfony leaves to the author. + +use: perCs + +file: + lineLength: none + longLines: keep + +indentation: + binaryOperator: keep + chain: keep + heredoc: keep # heredoc_indentation belongs to another set + +casing: + magicConstant: uppercase + class: declared # `DateTime`, not `datetime` + function: declared # `strlen()`, not `StrLen()` + method: declared # `__toString()`, other methods with `typeAnalysis` + hexDigit: keep # integer_literal_case left to the project + +# global_namespace_import imports nothing: a class is written with the backslash, a function and a constant too where +# they are qualified, and a bare one stays bare +qualification: + class: + global: backslashed # `\DateTime` + function: + global: [backslashed, bare] # `\ucfirst()` or `ucfirst()`, never imported + optimized: [backslashed, bare] + constant: + global: [backslashed, bare] + optimized: [backslashed, bare] + +imports: + order: + names: alphabetical # classes, functions, constants, each alphabetically + grouping: separated # `use A; use B;`, not `use A, B;` or `use Acme\{A, B};` + unused: forbidden + aliasEqualToName: forbidden # `use Foo;`, not `use Foo as Foo;` + +spacing: + arrowFunction: 'fn ($x) => $x' + alignment: + comma: optional # aligning spaces and tabs may stay + binaryOperator: forbidden # no extra spaces around an operator + semicolon: + after: 'for ($i = 0; $i < 5; $i++)' + logicalNot: '!$x' # never `! $x` + concatenation: '$a.$b' + objectOperator: '$a->b()' + padding: + array: forbidden # `[1, 2]`, `[$a, $b] = $x` + list: forbidden + offset: forbidden # `$a[0]` + anonymousClass: 'new class($a)' + comment: + marker: '// text' # also `#` and `/* */` + afterCode: '$a; // text' # at least one space + +multiline: + # no fixer of the set breaks a call, a signature, a chain, a condition, a ternary, an operator or an array + items: keep + edges: keep + operatorPosition: # operator_linebreak moves only a boolean operator + operators: keep + logical: lineStart + condition: lineStart + trailingComma: + argument: optional # may stay or not where spread, never on one line + closureUse: keep + attributePosition: + parameter: keep + +braces: + emptyBody: # not `{}` as in PER Coding Style + class: expanded + function: expanded + closure: expanded + propertyHook: expanded + anonymousClass: keep + singlelineClosure: keep + bareStatementGroup: forbidden # no `{ ... }` that nothing opens + alternativeSyntax: forbidden # no `if: ... endif;` + +# a range `0-1` leaves the choice to the author, `1+` is one or more +blankLines: + afterDeclare: keep # @Symfony leaves the line below a declare + aroundDeclarations: 0-1 # around one among statements + aroundMethods: 1 + aroundInterfaceMethods: 1 + betweenTraitUses: keep + afterTraitUses: keep + beforeStatement: {return: 1+} # a `return` set apart from the code above it + belowComment: + inlineVar: 0 # `/** @var Order $order */` right above its statement + +classes: + visibility: + publicWithSet: required # `public private(set)`, as modifier_keywords writes it + emptyParentheses: + attribute: keep # `#[Foo()]` stays, as in @Symfony + untypedPropertyNullDefault: forbidden # `public $a;`, not `public $a = null;` + instantiationWrapping: keep # @Symfony leaves the parentheses + protectedInFinalClass: forbidden # `private` in a final class with no parent and no trait + +functions: + trailingBareReturn: forbidden # no `return;` closing a function + +controlFlow: + elseAfterReturn: forbidden # the `else` after a `return` goes, its body following the `if` + +expressions: + comparison: + notEquals: '!=' # not `<>` + yoda: required # `null === $a` + assignment: + addingOne: forbidden # `++$i`, not `$i += 1` + increment: prefix # `++$i;` + doubleNegation: forbidden # `(bool) $a`, not `!!$a` + parenthesesAfterConstruct: forbidden # `return $x;`, not `return ($x);` + +literals: + quotes: single + heredocWithoutInterpolation: keep # heredoc_to_nowdoc belongs to another set + concatenatedLiterals: keep # no_useless_concat_operator left to the project + +comments: + singleline: '//' # not `#` + empty: forbidden + +phpdoc: + aboveAttributes: keep # @Symfony leaves the order alone, unlike PER Coding Style + blankLines: trimmed + empty: forbidden + types: + builtin: canonical # `int`, not `integer` + duplicate: forbidden # each type of a union once + nullPosition: last + +mistakes: + continueInSwitch: forbidden # a `continue` only leaving a `switch`, reported + +upgrading: + php: + dollarBraceInterpolation: forbidden # `{$name}`, not `${name}` + implicitNullable: forbidden # `?int $x = null`, not `int $x = null` + backticks: forbidden # `shell_exec()` diff --git a/tests/DressCode/Config/ConfigResolver.phpt b/tests/DressCode/Config/ConfigResolver.phpt index 92ce9e7cb..26a1dc783 100644 --- a/tests/DressCode/Config/ConfigResolver.phpt +++ b/tests/DressCode/Config/ConfigResolver.phpt @@ -750,7 +750,7 @@ test('the version of PHP a profile says is the target of its files, raised to th test('what the namespaces declare adds up over the layers, and only the configuration makes it certain', function () { $declaring = [new Config\UpgradingData( - new Config\Layer(Config\LayerKind::Package, 'upgrading.neon', 'fw/config'), + new Config\Layer(Config\LayerKind::Package, package: 'fw/config'), 'fw/config', new Profile(namespaces: ['functions' => ['Fw\Config\{service, param}'], 'constants' => ['Fw\VERSION']])->namespaces, [], @@ -763,13 +763,13 @@ test('what the namespaces declare adds up over the layers, and only the configur ); Assert::same( [ - 'Fw\Config\service' => 'upgrading.neon of fw/config', - 'Fw\Config\param' => 'upgrading.neon of fw/config', + 'Fw\Config\service' => 'DressCode for fw/config', + 'Fw\Config\param' => 'DressCode for fw/config', 'App\helper' => 'the configuration', ], $uncertain->namespacedFunctions, ); - Assert::same(['Fw\VERSION' => 'upgrading.neon of fw/config', 'Fw\version' => 'the configuration'], $uncertain->namespacedConstants); + Assert::same(['Fw\VERSION' => 'DressCode for fw/config', 'Fw\version' => 'the configuration'], $uncertain->namespacedConstants); Assert::same('uncertain', $uncertain->nameResolution); Assert::false($uncertain->toNamespacedSymbols()->complete); Assert::true($uncertain->toNamespacedSymbols()->hasFunction('App\helper')); diff --git a/tests/DressCode/Config/PackageDiscovery.phpt b/tests/DressCode/Config/PackageDiscovery.phpt index 6b91ec4b4..cdbcdf5f0 100644 --- a/tests/DressCode/Config/PackageDiscovery.phpt +++ b/tests/DressCode/Config/PackageDiscovery.phpt @@ -640,23 +640,31 @@ test('dresscode config lists the upgrading data with the sections the lowest ver }); -test('what a package declares in its namespaces lies under the lists of the project', function () { - $root = project( - 'namespaces', - [ - 'acme/lib' => ['3.2.0.0', ['upgrading-dir' => 'upgrading']], - ], - [ - 'vendor/acme/lib/upgrading/upgrading.neon' => <<<'XX' - package: acme/lib - namespaces: - functions: ['Acme\Lib\{helper}'] +test( + 'what a package declares in its namespaces lies under the lists of the project, and DressCode knows that of the configurator of Symfony', + function () { + $root = project( + 'namespaces', + [ + 'acme/lib' => ['3.2.0.0', ['upgrading-dir' => 'upgrading']], + 'symfony/dependency-injection' => ['7.3.0.0', []], + ], + [ + 'vendor/acme/lib/upgrading/upgrading.neon' => <<<'XX' + package: acme/lib + namespaces: + functions: ['Acme\Lib\{helper}'] - XX, - ], - ); - $factory = new RunnerFactory; - $functions = $factory->resolve(new Config(namespaces: ['functions' => ['App\format']]), $root)->resolvedConfig->namespacedFunctions; - Assert::same('the configuration', $functions['App\format']); - Assert::same('upgrading/upgrading.neon of acme/lib', $functions['Acme\Lib\helper']); -}); + XX, + ], + ); + $factory = new RunnerFactory; + $functions = $factory->resolve(new Config(namespaces: ['functions' => ['App\format']]), $root)->resolvedConfig->namespacedFunctions; + Assert::same('the configuration', $functions['App\format']); + Assert::same('upgrading/upgrading.neon of acme/lib', $functions['Acme\Lib\helper']); + Assert::same( + 'DressCode for symfony/dependency-injection', + $functions['Symfony\Component\DependencyInjection\Loader\Configurator\service'], + ); + }, +); diff --git a/tests/DressCode/Config/PluginRegistry.phpt b/tests/DressCode/Config/PluginRegistry.phpt index 1ba071cb9..377c408ff 100644 --- a/tests/DressCode/Config/PluginRegistry.phpt +++ b/tests/DressCode/Config/PluginRegistry.phpt @@ -192,8 +192,9 @@ test('presets', function () { Assert::same('dresscode/perCs', $registry->resolvePreset('perCs')); Assert::same('test/preset', $registry->resolvePreset('test/preset')); Assert::same([ - 'dresscode/perCs', 'dresscode/psr12', 'dresscode/nette', 'dresscode/cleanup', 'dresscode/compilerOptimizations', - 'dresscode/correctness', 'dresscode/deprecations', 'dresscode/modernizations', 'dresscode/types', 'test/preset', + 'dresscode/perCs', 'dresscode/psr12', 'dresscode/nette', 'dresscode/symfony', 'dresscode/cleanup', + 'dresscode/compilerOptimizations', 'dresscode/correctness', 'dresscode/deprecations', 'dresscode/modernizations', 'dresscode/types', + 'test/preset', ], array_keys($registry->presets)); Assert::match('%a%/src/Presets/perCs.neon', $registry->presets['dresscode/perCs']); Assert::exception(fn() => $registry->resolvePreset('none'), ConfigurationException::class, 'Unknown preset `none`.'); diff --git a/tests/DressCode/Console/explain.phpt b/tests/DressCode/Console/explain.phpt index 95c6c8bd7..d739a9140 100644 --- a/tests/DressCode/Console/explain.phpt +++ b/tests/DressCode/Console/explain.phpt @@ -110,7 +110,10 @@ test('explain of a decision writes what it is, its value here and those of the s Assert::same(0, $code); Assert::contains('`spacing.call`', $text); Assert::contains("Here `'foo(\$a, \$b)'`, set by dresscode/psr12.", $text); - Assert::contains("The standards: perCs `'foo(\$a, \$b)'`, psr12 `'foo(\$a, \$b)'`, nette `'foo(\$a, \$b)'`.", $text); + Assert::contains( + "The standards: perCs `'foo(\$a, \$b)'`, psr12 `'foo(\$a, \$b)'`, nette `'foo(\$a, \$b)'`, symfony `'foo(\$a, \$b)'`.", + $text, + ); Assert::contains('See https://dresscode.run/decisions/spacing.call', $text); }); diff --git a/tests/DressCode/Presets/fixtures/symfony/clean.code b/tests/DressCode/Presets/fixtures/symfony/clean.code new file mode 100644 index 000000000..e1697240a --- /dev/null +++ b/tests/DressCode/Presets/fixtures/symfony/clean.code @@ -0,0 +1,61 @@ + + */ + private array $names = []; + + public function __construct( + private readonly Connection $connection, + private readonly ?LoggerInterface $logger = null, + ) { + } + + /** + * @param list $names + * + * @return list + */ + public function findByNames(array $names, int $limit = self::DEFAULT_LIMIT): array + { + $rows = $this->connection->fetchAll($names, $limit); + $labels = array_map(fn ($row) => $row->name, $rows); + $matrix = [ + 1, 2, 3, + 4, 5, 6, + ]; + + try { + return array_map(static function ($row) { + return new User($row->id, $row->name); + }, $rows); + } catch (\RuntimeException|\LogicException $e) { + $this->logger?->error($e->getMessage()); + + throw new RepositoryException($e->getMessage(), 0, $e); + } + } + + public function count(): int + { + if (0 === \count($this->names)) { + return 0; + } + + return $this->connection->count(); + } +} diff --git a/tests/DressCode/Presets/fixtures/symfony/per.code b/tests/DressCode/Presets/fixtures/symfony/per.code new file mode 100644 index 000000000..4ce01c52c --- /dev/null +++ b/tests/DressCode/Presets/fixtures/symfony/per.code @@ -0,0 +1,38 @@ + $this->subject, + Priority::High => strtoupper($this->subject), + }; + } + + public function help(): string + { + return <<resolve(new Config(use: ['symfony']), Config::DefaultPhpVersion); +$rules = RuleBuilder::buildRules($resolved); +// which rules run, not in what order +$slugs = array_map(fn($rule) => ruleSlug($rule), $rules); +sort($slugs); +$expected = [ + 'delimiterSpacing', + 'arraySyntax', + 'trailingComma', + 'classHeadSpacing', + 'nameCasing', + 'memberOrder', + 'privateForProtected', + 'noGroupedDeclarations', + 'uselessNullInitialization', + 'visibilityRequired', + 'modifierOrder', + 'commentSpacing', + 'noEmptyComments', + 'noHashComments', + 'noBracelessBodies', + 'elseifNotation', + 'fallThroughComment', + 'multilineCondition', + 'noAlternativeSyntax', + 'noContinueInSwitch', + 'noEmptyStatements', + 'switchCaseNotation', + 'switchCaseSpacing', + 'uselessBraces', + 'uselessParenthesesAfterConstruct', + 'uselessElse', + 'uselessReturn', + 'binaryOperatorSpacing', + 'castCanonicalType', + 'operandSpacing', + 'emptyArgumentParentheses', + 'increment', + 'multilineBinaryOperator', + 'noDoubleNegations', + 'notEqualsNotation', + 'memberAccessSpacing', + 'ternaryOperatorSpacing', + 'yoda', + 'declareSpacing', + 'finalLineEndings', + 'openingTagNotation', + 'lineEnding', + 'noBom', + 'noClosingTag', + 'noTrailingWhitespace', + 'strictTypesRequired', + 'arrowFunctionForClosure', + 'functionNameSpacing', + 'methodCasing', + 'namedArgumentSpacing', + 'noDollarBraceInterpolations', + 'builtinCasing', + 'noBacktickOperators', + 'stringQuotes', + 'importNotation', + 'declaredNameCasing', + 'importOrder', + 'globalNameQualification', + 'optimizedCallNotation', + 'uselessLeadingBackslash', + 'noUnusedImports', + 'uselessAlias', + 'phpdocBlankLines', + 'phpdocTypeNotation', + 'nullableTypeForDefaultNull', + 'typeDeclarationSpacing', + 'typeNotation', + 'attributePosition', + 'attributeSpacing', + 'statementBlankLines', + 'memberBlankLines', + 'blockBlankLines', + 'bracesPosition', + 'commaSpacing', + 'constructSpacing', + 'indentation', + 'semicolonSpacing', + 'noStatementsSharingLine', +]; +sort($expected); +Assert::same($expected, $slugs); +Assert::same([' ', null], [$resolved->indent, $resolved->lineEnding]); + +$style = $resolved->createStyle(); +$processor = new PhpFileProcessor( + $rules, + $resolved->createAnalyses($style), + Config::DefaultPhpVersion, + $style, + policy: new ReportPolicy($registry->expandSuppressedName(...)), +); + +foreach (glob(__DIR__ . '/fixtures/symfony/*.code') ?: [] as $file) { + $code = (string) file_get_contents($file); + $target = (string) preg_replace('~\.code$~', '.expected', $file); + $expected = is_file($target) ? (string) file_get_contents($target) : $code; + $result = $processor->process(basename($file), $code); + Assert::null($result->syntaxError, basename($file)); + Assert::null($result->failure, basename($file)); + Assert::same($expected, $result->output, basename($file)); + Assert::same($expected === $code, !$result->violations, basename($file)); +} diff --git a/tests/DressCode/Rules/robustness.provider.php b/tests/DressCode/Rules/robustness.provider.php index 14b20811f..422c0fdc1 100644 --- a/tests/DressCode/Rules/robustness.provider.php +++ b/tests/DressCode/Rules/robustness.provider.php @@ -8,4 +8,5 @@ 'psr12' => ['presets' => ['psr12']], 'perCs' => ['presets' => ['perCs', 'modernizations', 'cleanup']], 'nette' => ['presets' => ['nette', 'modernizations', 'deprecations']], + 'symfony' => ['presets' => ['symfony', 'correctness']], ]; From 65be34a38a36a303af5a0f47f613f0815d92517e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 12 Oct 2026 13:21:53 +0200 Subject: [PATCH 03/13] presets: laravel, the style of the preset laravel of Laravel Pint, which init proposes for a project formatted by Pint PER Coding Style with what Pint 1.32 decides otherwise and without what it leaves to the author: the concatenation without spaces, `! $x`, `new Foo`, `fn ($x) =>`, the trailing comma in arrays alone, a blank line before `return` and `continue`, the enum cases kept together, the names in doc comments imported, and no construct spread for its width. A standard of the core beside the others, so the reference, explain and init list it with them; init writes it where the project requires laravel/pint, the standard of the preset a pint.json names, laravel without one. --- AGENTS.md | 2 +- composer.json | 2 +- docs/internals.md | 6 +- docs/reference/decisions.md | 868 ++++++++++----------- docs/reference/generate.php | 6 +- docs/reference/presets.md | 5 +- src/Config/ConfigResolver.php | 2 +- src/Config/CorePlugin.php | 3 +- src/Presets/laravel.neon | 214 +++++ tests/DressCode/Config/PluginRegistry.phpt | 2 +- tests/DressCode/Console/explain.phpt | 2 +- tests/DressCode/Presets/sets.phpt | 7 +- 12 files changed, 667 insertions(+), 452 deletions(-) create mode 100644 src/Presets/laravel.neon diff --git a/AGENTS.md b/AGENTS.md index 45f6b642d..55b619f56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ DressCode is a PHP code style checker and fixer built on a **lossless concrete s The tree itself is the `phpsyntax/phpsyntax` library (namespace `PhpSyntax`), developed in a repository of its own. This one holds `DressCode` (`src/`): engine, rules API, the library of generic rules, configuration, CLI. -Rules use only the public API of `PhpSyntax`; whatever a rule in DressCode needs from it is public API for plugins too. What the rules only share among themselves (`Rules\NodeHelpers`, `Rules\Compiler` and the helpers beside the rules that carry `@internal`) is `@internal`; the grammar of a map of members (`Upgrading\MemberMapGrammar`, the map itself `MemberMap` with `findAccess()`, its `MemberMapEntry` and `MemberMapMatch`, `MemberPattern`, `ArgumentPattern`, `FunctionPattern`) is not, a rule shipped by a package being fed the same way, and neither is `GlobalCalls`, which answers which global function a call calls and why that may be another one, nor `CodeWriter`, which writes code in the shape of the file (a class written the way the file reaches it, an import added the way the file writes its imports) for a rule of a package as for one of the core. Presets define style; DressCode has no style of its own. The four standards carry one: `dresscode/perCs` (PER Coding Style 3.1), `dresscode/psr12`, `dresscode/nette` (the Nette Coding Standard, PER with tabs, the layout of the Nette libraries and their departures, which the repository itself is checked with) and `dresscode/symfony`. What a project wants beyond the looks of its code comes in a set, a preset of one intent that decides nothing about the layout and is used beside the standard (`modernizations`, `deprecations`, `cleanup`, `correctness`, `types`, `compilerOptimizations`); a decision the core gains goes into its set, one that writes a newer construct and the replacement of what PHP deprecated alike into every set whose intent it meets (`upgrading.syntax.firstClassCallables` in `modernizations` and `deprecations`), and what no set holds the project decides itself. +Rules use only the public API of `PhpSyntax`; whatever a rule in DressCode needs from it is public API for plugins too. What the rules only share among themselves (`Rules\NodeHelpers`, `Rules\Compiler` and the helpers beside the rules that carry `@internal`) is `@internal`; the grammar of a map of members (`Upgrading\MemberMapGrammar`, the map itself `MemberMap` with `findAccess()`, its `MemberMapEntry` and `MemberMapMatch`, `MemberPattern`, `ArgumentPattern`, `FunctionPattern`) is not, a rule shipped by a package being fed the same way, and neither is `GlobalCalls`, which answers which global function a call calls and why that may be another one, nor `CodeWriter`, which writes code in the shape of the file (a class written the way the file reaches it, an import added the way the file writes its imports) for a rule of a package as for one of the core. Presets define style; DressCode has no style of its own. The five standards carry one: `dresscode/perCs` (PER Coding Style 3.1), `dresscode/psr12`, `dresscode/nette` (the Nette Coding Standard, PER with tabs, the layout of the Nette libraries and their departures, which the repository itself is checked with), `dresscode/symfony` and `dresscode/laravel` (the preset laravel of Laravel Pint). What a project wants beyond the looks of its code comes in a set, a preset of one intent that decides nothing about the layout and is used beside the standard (`modernizations`, `deprecations`, `cleanup`, `correctness`, `types`, `compilerOptimizations`); a decision the core gains goes into its set, one that writes a newer construct and the replacement of what PHP deprecated alike into every set whose intent it meets (`upgrading.syntax.firstClassCallables` in `modernizations` and `deprecations`), and what no set holds the project decides itself. ## Essential commands diff --git a/composer.json b/composer.json index a3332fe37..f29ba454e 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "dresscode/dresscode", - "description": "👔 DressCode: checks and fixes PHP coding style, and upgrades code to newer PHP and to new versions of libraries. Built on a lossless syntax tree, so it changes only what it touches. PER Coding Style, PSR-12, Nette and Symfony.", + "description": "👔 DressCode: checks and fixes PHP coding style, and upgrades code to newer PHP and to new versions of libraries. Built on a lossless syntax tree, so it changes only what it touches. PER Coding Style, PSR-12, Nette, Symfony and Laravel.", "keywords": ["code style", "coding standard", "formatter", "fixer", "linter", "refactoring", "modernization", "upgrade", "deprecations", "psr-12", "per-cs", "php-cs-fixer", "phpcs", "rector"], "homepage": "https://dresscode.run", "license": "MIT", diff --git a/docs/internals.md b/docs/internals.md index c20ab69b7..5c456c1a9 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -263,7 +263,7 @@ Such a file sets maps, the requirements of the rules of the plugin of its own pa A preset is a file of the shape of the configuration under a name, `src/Presets/.neon` for those of the core, a file a plugin names in its manifest, or any file `use` gives by its path. It carries decisions, the presets it uses, `suppressionComments` and `severity`, nothing else: what is the project's, the facts, the scope, `fixRisky`, `reportOnly`, a plugin and `rules`, `NeonReader::readPreset()` refuses, so a file laid by `use` never gains what the file of the project can do. A name without a vendor is the one of the core, so `perCs` and `dresscode/perCs` are the same preset. A preset opens with a comment, its title and what it is, and says what a decision does, where its key and value do not, in a short comment after the value. -The presets of the core are the four standards, "standard" being the word for a preset that carries a style and says how the code looks: `psr12`, `perCs` on top of it, `nette` on `perCs` and the sets `cleanup`, `types` and `correctness`, with tabs and the layout of the Nette libraries, and `symfony` on `perCs`. What a project wants beyond the looks of its code is a set, a preset of one intent used beside the standard (`use: [nette, modernizations]`): `modernizations`, `deprecations`, `cleanup`, `correctness`, `types` and `compilerOptimizations`. A set decides nothing about the layout, so it never fights the standard, and a decision the core gains goes into its set, so it reaches every project using the set without a change of its configuration. A standard may use a set and keep some of its decisions, as `nette` keeps the native types and an else after a return; a project naming that set beside the standard gets none of them back, the set being laid once, below the standard, and the run says so; the project writes those decisions itself. `tests/DressCode/Presets/sets.phpt` holds that every construct and function newer PHP brought or retired is in a set and lists, with the reason, the decisions a project writes itself. A decision of a rule of a plugin reaches a project when a preset it uses or the project writes it. `psr12` and `perCs` follow their specifications section by section and add no style of their own, so a rule whose fixed behaviour contradicts a specification gets a value rather than a preset-side workaround. Without a configuration file nothing applies: DressCode has no style of its own to fall back on, so a run refuses, unless `--use` names what to use, which is then all the run has, so that a run over a foreign project can ask for that alone. +The presets of the core are the five standards, "standard" being the word for a preset that carries a style and says how the code looks: `psr12`, `perCs` on top of it, `nette` on `perCs` and the sets `cleanup`, `types` and `correctness`, with tabs and the layout of the Nette libraries, `symfony` on `perCs` and `laravel` on `perCs`, the preset laravel of Laravel Pint. What a project wants beyond the looks of its code is a set, a preset of one intent used beside the standard (`use: [nette, modernizations]`): `modernizations`, `deprecations`, `cleanup`, `correctness`, `types` and `compilerOptimizations`. A set decides nothing about the layout, so it never fights the standard, and a decision the core gains goes into its set, so it reaches every project using the set without a change of its configuration. A standard may use a set and keep some of its decisions, as `nette` keeps the native types and an else after a return; a project naming that set beside the standard gets none of them back, the set being laid once, below the standard, and the run says so; the project writes those decisions itself. `tests/DressCode/Presets/sets.phpt` holds that every construct and function newer PHP brought or retired is in a set and lists, with the reason, the decisions a project writes itself. A decision of a rule of a plugin reaches a project when a preset it uses or the project writes it. `psr12` and `perCs` follow their specifications section by section and add no style of their own, so a rule whose fixed behaviour contradicts a specification gets a value rather than a preset-side workaround. Without a configuration file nothing applies: DressCode has no style of its own to fall back on, so a run refuses, unless `--use` names what to use, which is then all the run has, so that a run over a foreign project can ask for that alone. `multiline.trailingComma` removes the comma of a list written on one line wherever its place is not `keep`, so `perCs`, `nette` and `symfony` also fix `function f($a,)`, `use ($a,)` and a `match` on one line whose last arm is followed by a comma. For `symfony` that is a deliberate departure from `no_trailing_comma_in_singleline` of PHP CS Fixer, and so is the comma `optional` removes where the closing bracket follows it on the line of the last item, which `trailing_comma_in_multiline` leaves. @@ -283,11 +283,11 @@ The excluded paths only ever add up, the default list, those of the plugins and ### What the commands print -`dresscode explain ` says what one decision is for: its description, the values it takes with what each means, its notes, the value here with the layer that said it, the values of the four standards and the rule that owns it with what it requires. With a section it explains every decision under it; without an argument it explains the configuration as one document, by sections, drawn on the console, and with `--format markdown` it writes that document to the standard output as Markdown instead of drawing it. Both are written in Markdown by one `Console\ExplainPrinter` and the console draws that Markdown (`Markup::formatMarkdown()`), so that a fact is written once and the two never part; there are no tables. +`dresscode explain ` says what one decision is for: its description, the values it takes with what each means, its notes, the value here with the layer that said it, the values of the five standards and the rule that owns it with what it requires. With a section it explains every decision under it; without an argument it explains the configuration as one document, by sections, drawn on the console, and with `--format markdown` it writes that document to the standard output as Markdown instead of drawing it. Both are written in Markdown by one `Console\ExplainPrinter` and the console draws that Markdown (`Markup::formatMarkdown()`), so that a fact is written once and the two never part; there are no tables. `dresscode config` prints that resolution in the shape of the file, every decision a layer said with the layer in a comment (`# dresscode/perCs`, `# the override for tests/`), why it takes no effect where it does not, whether it lies outside `--only` and whether its risky fixes are accepted, and `--preset ` prints one preset alone, what it uses included. `--file` narrows it to what the run uses for one file and `--format json` gives it as data: its `version`, the facts in the words of the configuration, the decisions by path, each with `value`, `layer`, `inactive`, `selected` and `severity` (`error` or `warning`), and the rules by class with the reason a rule does not run as a word (`turnedOff`, `php`, `package`, `types`, `narrowed`, `onlyOverride` or `notMentioned`) beside the sentence. A name in `fixRisky` whose rules run nowhere, not even in an override, is a warning of every run. -`dresscode catalogue` prints the catalogue of the installed set and `--format json` gives it as data, versioned (`Catalogue::Version`): every decision with its kind, its domain as data, its description and notes, the rules declaring it with what each requires and the values of the four standards as a configuration writes them. It is the interface an editor of the configuration is built on, and `composer reference` writes `docs/reference/decisions.md` from the same data. +`dresscode catalogue` prints the catalogue of the installed set and `--format json` gives it as data, versioned (`Catalogue::Version`): every decision with its kind, its domain as data, its description and notes, the rules declaring it with what each requires and the values of the five standards as a configuration writes them. It is the interface an editor of the configuration is built on, and `composer reference` writes `docs/reference/decisions.md` from the same data. ### The target version of PHP diff --git a/docs/reference/decisions.md b/docs/reference/decisions.md index 22770e899..2d0610439 100644 --- a/docs/reference/decisions.md +++ b/docs/reference/decisions.md @@ -75,7 +75,7 @@ A byte order mark at the start of the file. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Files\NoBomRule`, stage Structure ### file.closingTagAtEnd @@ -84,7 +84,7 @@ A `?>` ending a file of PHP only; a `?>` before HTML is content. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Files\NoClosingTagRule`, stage Structure ### file.finalLineEndings @@ -93,7 +93,7 @@ Exactly one line ending after the last line; a file ending in HTML is content. - By default `keep` - Takes: exactly 1; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Files\FinalLineEndingsRule`, stage Formatting ### file.lineEnding @@ -102,7 +102,7 @@ The line ending of every line, which the code written new takes too; under `keep - By default `keep` - Takes: `LF` (every line ends with LF); `CRLF` (every line ends with CRLF); `majority` (every line ends as most lines of the file do, LF on a tie); `keep` -- The standards: perCs `majority`, psr12 `majority`, nette `majority`, symfony `majority` +- The standards: perCs `majority`, psr12 `majority`, nette `majority`, symfony `majority`, laravel `majority` - Rule `DressCode\Rules\Files\LineEndingRule`, stage Finishing ### file.lineLength @@ -111,7 +111,7 @@ The widest line, by which what spreads over lines is split. - By default `none` - Takes: a count from 1; `none` (no line is too wide) -- The standards: perCs `120`, psr12 `120`, nette `140`, symfony `none` +- The standards: perCs `120`, psr12 `120`, nette `140`, symfony `none`, laravel `none` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing ### file.lineLengthExcept @@ -120,7 +120,7 @@ What a line too wide leaves as it is: a construct named is not spread over lines - By default `[imports]` - Takes: a list of `imports` (a `use` import other than a group use on one line, which cannot be split); `strings` (a line that would fit were each of its string literals an empty pair of quotes, a string being never split); `argument` (the arguments of a call, of `new` and of an attribute, the variables of `unset()` and `isset()`); `parameter` (the parameters of a function or a method); `closureUse` (the variables of the `use` of a closure); `attribute` (the attributes of one group); `condition` (a condition of `if`, `elseif`, `while` and `do-while`); `chain` (a chain of method calls); `ternary` (a ternary); `for` (the header of a `for`); `logical` (the operands of a boolean operator outside a condition); `concatenation` (the operands of `.`); `arithmetic` (the operands of `+`, `-`, `*`, `/`, `%` and `**`); `bitwise` (the operands of `|`, `&`, `^`, `<<` and `>>`); `coalesce` (the operands of `??`); `pipe` (the operands of `|>`); `array` (the items of an array, which `multiline.maxWidth.array` spreads all the same); `list` (the items of `list()`); `implements` (the interfaces a class implements or an interface extends); `matchCondition` (the conditions of a `match` arm); regular expressions -- The standards: perCs `[imports]`, psr12 `[imports]`, nette `[imports, strings]`, symfony `[imports]` +- The standards: perCs `[imports]`, psr12 `[imports]`, nette `[imports, strings]`, symfony `[imports]`, laravel `[imports]` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing - Read by rule `DressCode\Rules\Arrays\MultilineArrayRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Classes\MultilineImplementsRule`, which this decision does not turn on @@ -141,7 +141,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `keep` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing ### file.openingTag @@ -150,7 +150,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Files\OpeningTagNotationRule`, stage Structure ### file.statementsPerLine @@ -159,7 +159,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - By default `keep` - Takes: exactly 1; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Whitespace\NoStatementsSharingLineRule`, stage Formatting ### file.strictTypes.declaration @@ -168,7 +168,7 @@ Every file of PHP code declares `strict_types=1` as its first statement, a file - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Files\StrictTypesRequiredRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -178,7 +178,7 @@ Where the declaration of `strict_types` stands. - By default `keep` - Takes: `sameLine` (`getTotal()`, never `$order->gettotal()`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `declared`, symfony `declared` +- The standards: perCs `keep`, psr12 `keep`, nette `declared`, symfony `declared`, laravel `keep` - Rule `DressCode\Rules\Functions\MethodCasingRule`, stage Structure ### casing.trueFalseNull @@ -460,7 +460,7 @@ The case of `true`, `false` and `null`, a leading backslash staying. - By default `keep` - Takes: `lowercase` (`true`, `false`, `null`); `uppercase` (`TRUE`, `FALSE`, `NULL`); `keep` -- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase`, symfony `lowercase` +- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase`, symfony `lowercase`, laravel `lowercase` - Rule `DressCode\Rules\Literals\BuiltinCasingRule`, stage Structure ### casing.type @@ -469,7 +469,7 @@ The case of the built-in types of a declaration; `array`, `callable`, `static`, - By default `keep` - Takes: `lowercase` (`int`, `string`, `void`); `keep` -- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase`, symfony `lowercase` +- The standards: perCs `lowercase`, psr12 `lowercase`, nette `lowercase`, symfony `lowercase`, laravel `lowercase` - Rule `DressCode\Rules\Namespaces\DeclaredNameCasingRule`, stage Structure ## qualification @@ -480,7 +480,7 @@ A class, interface, trait or enum of the namespace of the file, `Order` in `name - By default `keep` - Takes: `bare` (bare, `Order` relative to the namespace); `backslashed` (with the leading backslash, `\Acme\Shop\Order`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `bare`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `bare`, symfony `keep`, laravel `bare` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -490,7 +490,7 @@ A class of the global namespace referenced in a namespace, `DateTime`, which a b - By default `keep` - Takes: `imported` (imported, `use DateTime;` and `DateTime`); `backslashed` (with the leading backslash, `\DateTime`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `backslashed` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `backslashed`, laravel `keep` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure ### qualification.class.otherNamespace @@ -501,7 +501,7 @@ A name relative to an import or to the namespace, `Shop\Order`, stays as it is. - By default `keep` - Takes: `imported` (imported, `use Acme\Shop\Order;` and `Order`); `backslashed` (with the leading backslash, `\Acme\Shop\Order`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `imported`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `imported`, symfony `keep`, laravel `imported` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -513,7 +513,7 @@ A name relative to an import or to the namespace, `Shop\Order`, stays as it is, - By default `keep` - Takes: `imported` (imported, `use Acme\Shop\Order;` and `@param Order $order`, a class of the namespace of the file bare, a global one where the file imports it); `backslashed` (with the leading backslash, `@param \Acme\Shop\Order $order`, a name an import reaches); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `imported`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `imported`, symfony `keep`, laravel `imported` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -523,7 +523,7 @@ A constant of the namespace of the file, `STATUS_PAID` in `namespace Acme\Shop`. - By default `keep` - Takes: `bare` (bare, `STATUS_PAID` relative to the namespace); `backslashed` (with the leading backslash, `\Acme\Shop\STATUS_PAID`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -533,7 +533,7 @@ A global constant in a namespace, `PHP_EOL`, but where the compiler computes wit - By default `keep` - Takes: `bare` (bare, `PHP_EOL` without an import or a backslash, reached by the fallback at run time); `imported` (imported, `use const PHP_EOL;` and `PHP_EOL`); `backslashed` (with the leading backslash, `\PHP_EOL`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]`, laravel `keep` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -545,7 +545,7 @@ A constant merely passed to a call is not one the compiler computes with. - By default `keep` - Takes: `imported` (imported, `use const PHP_VERSION_ID;`, so that the compiler computes with it); `backslashed` (with the leading backslash, `\PHP_VERSION_ID`, so that the compiler computes with it); `bare` (bare, `PHP_VERSION_ID`, forgoing the optimization); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]`, laravel `keep` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -555,7 +555,7 @@ A constant of a namespace other than that of the file. - By default `keep` - Takes: `imported` (imported, `use const Acme\Shop\STATUS_PAID;` and `STATUS_PAID`); `backslashed` (with the leading backslash, `\Acme\Shop\STATUS_PAID`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -565,7 +565,7 @@ How a class refers to itself in an expression: a static access, an instantiation - By default `keep` - Takes: `self` (the class named `self` inside itself); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `self`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `self`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\SelfForCurrentClassRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -575,7 +575,7 @@ A function of the namespace of the file, `normalize()` in `namespace Acme\Text`. - By default `keep` - Takes: `bare` (bare, `normalize()` relative to the namespace); `backslashed` (with the leading backslash, `\Acme\Text\normalize()`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -587,7 +587,7 @@ Not a function of the namespace of the file, nor a name in a file without a name - By default `keep` - Takes: `bare` (bare, `ucfirst()` without an import or a backslash, reached by the fallback at run time); `imported` (imported, `use function ucfirst;` and `ucfirst()`); `backslashed` (with the leading backslash, `\ucfirst()`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]`, laravel `keep` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -599,7 +599,7 @@ A function is optimized in a namespace where one of its calls there is, an unpac - By default `keep` - Takes: `imported` (imported, `use function strlen;`, so that the compiler optimizes it); `backslashed` (with the leading backslash, `\strlen()`, so that the compiler optimizes it); `bare` (bare, `strlen()`, forgoing the optimization); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `[backslashed, bare]`, laravel `keep` - Rule `DressCode\Rules\Namespaces\GlobalNameQualificationRule`, stage Structure - Rule `DressCode\Rules\Namespaces\OptimizedCallNotationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -610,7 +610,7 @@ A function of a namespace other than that of the file. - By default `keep` - Takes: `imported` (imported, `use function Acme\Text\normalize;` and `normalize()`); `backslashed` (with the leading backslash, `\Acme\Text\normalize()`); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\ForeignNameQualificationRule`, stage Structure - Modifies comments @@ -620,7 +620,7 @@ The leading backslash of a name where it changes nothing, in a file without a na - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\UselessLeadingBackslashRule`, stage Structure ### qualification.staticInFinalClass @@ -631,7 +631,7 @@ The return type `static` stays, saying what a subclass would return. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `required` - Rule `DressCode\Rules\Classes\SelfForCurrentClassRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -643,7 +643,7 @@ The return type `static` stays, saying what a subclass would return. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Namespaces\UselessAliasRule`, stage Structure ### imports.fromCurrentNamespace @@ -652,7 +652,7 @@ An import of a class of the namespace it stands in, `use Acme\Shop\Order;` insid - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\UselessCurrentNamespaceImportRule`, stage Structure ### imports.grouping.class @@ -661,7 +661,7 @@ How the imports of classes are put into `use` statements, a name of the global n - By default `keep` - Takes: `separated` (a `use` of its own for every name, `use Foo; use Bar;`); `combined` (all names of the kind in one `use`, `use Foo, Bar;`, one per namespace declaration); `grouped` (the names of one namespace in one group use, `use Acme\Shop\{Order, Cart};`, from `imports.groupMinNames` of them on); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[separated, grouped]`, symfony `separated` +- The standards: perCs `keep`, psr12 `keep`, nette `[separated, grouped]`, symfony `separated`, laravel `separated` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.grouping.constant @@ -670,7 +670,7 @@ How the imports of constants are put into `use` statements, a name of the global - By default `keep` - Takes: `separated` (a `use` of its own for every name, `use Foo; use Bar;`); `combined` (all names of the kind in one `use`, `use Foo, Bar;`, one per namespace declaration); `grouped` (the names of one namespace in one group use, `use Acme\Shop\{Order, Cart};`, from `imports.groupMinNames` of them on); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]`, symfony `separated` +- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]`, symfony `separated`, laravel `separated` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.grouping.function @@ -679,7 +679,7 @@ How the imports of functions are put into `use` statements, a name of the global - By default `keep` - Takes: `separated` (a `use` of its own for every name, `use Foo; use Bar;`); `combined` (all names of the kind in one `use`, `use Foo, Bar;`, one per namespace declaration); `grouped` (the names of one namespace in one group use, `use Acme\Shop\{Order, Cart};`, from `imports.groupMinNames` of them on); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]`, symfony `separated` +- The standards: perCs `keep`, psr12 `keep`, nette `[combined, grouped]`, symfony `separated`, laravel `separated` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.groupMinNames @@ -688,7 +688,7 @@ How many names of one namespace make a group use where `grouped` is written, few - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2` +- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2`, laravel `2` - Rule `DressCode\Rules\Namespaces\ImportNotationRule`, stage Structure ### imports.leadingBackslash @@ -697,7 +697,7 @@ The leading backslash of an import, which changes nothing, `use \Foo\Bar;`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Namespaces\UselessLeadingBackslashRule`, stage Structure ### imports.order.caseSensitive @@ -706,7 +706,7 @@ Whether `Acme` sorts before `acme` rather than with it. - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no`, laravel `no` - Rule `DressCode\Rules\Namespaces\ImportOrderRule`, stage Structure ### imports.order.kinds @@ -715,7 +715,7 @@ The order of the kinds among consecutive imports, a kind not listed following in - By default `keep` - Takes: an order of `class` (the imports of classes); `function` (the imports of functions); `constant` (the imports of constants); `keep` -- The standards: perCs `[class, function, constant]`, psr12 `[class, function, constant]`, nette `[class, function, constant]`, symfony `[class, function, constant]` +- The standards: perCs `[class, function, constant]`, psr12 `[class, function, constant]`, nette `[class, function, constant]`, symfony `[class, function, constant]`, laravel `[constant, class, function]` - Rule `DressCode\Rules\Namespaces\ImportOrderRule`, stage Structure ### imports.order.names @@ -724,7 +724,7 @@ The order of the names of one kind among consecutive imports, the names of a sta - By default `keep` - Takes: `alphabetical` (the names of a kind sorted alphabetically); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `alphabetical`, symfony `alphabetical` +- The standards: perCs `keep`, psr12 `keep`, nette `alphabetical`, symfony `alphabetical`, laravel `alphabetical` - Rule `DressCode\Rules\Namespaces\ImportOrderRule`, stage Structure ### imports.unused @@ -733,7 +733,7 @@ An import nothing uses is removed, a class name in a doc comment using the impor - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Namespaces\NoUnusedImportsRule`, stage Structure ## spacing @@ -744,7 +744,7 @@ The whitespace wider than a single space around an operator, aligning a column o - By default `spaces` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `spaces` (spaces aligning a column staying, tabs not); `tabs` (tabs aligning a column staying, spaces not); `optional` (there or not, as the author wrote it) -- The standards: perCs `spaces`, psr12 `spaces`, nette `spaces`, symfony `forbidden` +- The standards: perCs `spaces`, psr12 `spaces`, nette `spaces`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### spacing.alignment.comma @@ -753,7 +753,7 @@ The whitespace wider than a single space after a comma, aligning the columns of - By default `tabs` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `spaces` (spaces aligning a column staying, tabs not); `tabs` (tabs aligning a column staying, spaces not); `optional` (there or not, as the author wrote it) -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `tabs`, symfony `optional` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `tabs`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Whitespace\CommaSpacingRule`, stage Formatting ### spacing.alignment.comment @@ -762,7 +762,7 @@ The whitespace wider than a single space before a comment following code, aligni - By default `optional` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `optional` (there or not, as the author wrote it) -- The standards: perCs `optional`, psr12 `optional`, nette `optional`, symfony `optional` +- The standards: perCs `optional`, psr12 `optional`, nette `optional`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Comments\CommentSpacingRule`, stage Finishing - Modifies comments @@ -772,7 +772,7 @@ The whitespace wider than a single space around `?` and `:`, aligning a column; - By default `optional` - Takes: `forbidden` (never there, more whitespace collapsing to a single space); `optional` (there or not, as the author wrote it) -- The standards: perCs `optional`, psr12 `optional`, nette `forbidden`, symfony `optional` +- The standards: perCs `optional`, psr12 `optional`, nette `forbidden`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting ### spacing.alternativeSyntax @@ -781,7 +781,7 @@ The space before the colon of the alternative syntax and of a label, and the sin - By default `keep` - Takes: `'if ($a):'` (no space before the colon); `'if ($a) :'` (a single space before the colon); `keep` -- The standards: perCs `'if ($a):'`, psr12 `'if ($a):'`, nette `'if ($a):'`, symfony `'if ($a):'` +- The standards: perCs `'if ($a):'`, psr12 `'if ($a):'`, nette `'if ($a):'`, symfony `'if ($a):'`, laravel `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.anonymousClass @@ -790,7 +790,7 @@ The space between `class` and the arguments of an anonymous class. - By default `keep` - Takes: `'new class ($a)'` (a single space before the arguments); `'new class($a)'` (no space before the arguments); `keep` -- The standards: perCs `'new class ($a)'`, psr12 `'new class ($a)'`, nette `'new class ($a)'`, symfony `'new class($a)'` +- The standards: perCs `'new class ($a)'`, psr12 `'new class ($a)'`, nette `'new class ($a)'`, symfony `'new class($a)'`, laravel `'new class($a)'` - Rule `DressCode\Rules\Classes\ClassHeadSpacingRule`, stage Formatting ### spacing.arrowFunction @@ -799,7 +799,7 @@ The space between `fn` and its parenthesis. - By default `keep` - Takes: `'fn($x) => $x'` (no space before the parenthesis); `'fn ($x) => $x'` (a single space); `keep` -- The standards: perCs `'fn($x) => $x'`, psr12 `keep`, nette `'fn($x) => $x'`, symfony `'fn ($x) => $x'` +- The standards: perCs `'fn($x) => $x'`, psr12 `keep`, nette `'fn($x) => $x'`, symfony `'fn ($x) => $x'`, laravel `'fn ($x) => $x'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.attribute @@ -808,7 +808,7 @@ The whitespace inside the brackets of an attribute group and before the parenthe - By default `keep` - Takes: `'#[Foo($a)]'` (none inside the brackets, none before the parenthesis); `keep` -- The standards: perCs `'#[Foo($a)]'`, psr12 `keep`, nette `'#[Foo($a)]'`, symfony `'#[Foo($a)]'` +- The standards: perCs `'#[Foo($a)]'`, psr12 `keep`, nette `'#[Foo($a)]'`, symfony `'#[Foo($a)]'`, laravel `keep` - Rule `DressCode\Rules\Whitespace\AttributeSpacingRule`, stage Formatting ### spacing.binaryOperator @@ -817,7 +817,7 @@ The spaces around a binary operator, an assignment, `instanceof`, `=>` and the ` - By default `keep` - Takes: `'$a + $b'` (a single space around); `keep` -- The standards: perCs `'$a + $b'`, psr12 `'$a + $b'`, nette `'$a + $b'`, symfony `'$a + $b'` +- The standards: perCs `'$a + $b'`, psr12 `'$a + $b'`, nette `'$a + $b'`, symfony `'$a + $b'`, laravel `'$a + $b'` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### spacing.body @@ -826,7 +826,7 @@ The space inside the braces of a body whose code stands on their line, of a func - By default `keep` - Takes: `'function () { return 1; }'` (a single space inside the braces); `keep` -- The standards: perCs `'function () { return 1; }'`, psr12 `'function () { return 1; }'`, nette `'function () { return 1; }'`, symfony `'function () { return 1; }'` +- The standards: perCs `'function () { return 1; }'`, psr12 `'function () { return 1; }'`, nette `'function () { return 1; }'`, symfony `'function () { return 1; }'`, laravel `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.call @@ -835,7 +835,7 @@ The whitespace between the name of a function and its parenthesis, of a call and - By default `keep` - Takes: `'foo($a, $b)'` (no space before the parenthesis); `keep` -- The standards: perCs `'foo($a, $b)'`, psr12 `'foo($a, $b)'`, nette `'foo($a, $b)'`, symfony `'foo($a, $b)'` +- The standards: perCs `'foo($a, $b)'`, psr12 `'foo($a, $b)'`, nette `'foo($a, $b)'`, symfony `'foo($a, $b)'`, laravel `'foo($a, $b)'` - Rule `DressCode\Rules\Functions\FunctionNameSpacingRule`, stage Formatting ### spacing.cast @@ -844,7 +844,7 @@ The space between a cast and its operand, which stays on the line of the cast. - By default `keep` - Takes: `'(int) $x'` (a single space after the cast); `'(int)$x'` (no space after the cast); `keep` -- The standards: perCs `'(int) $x'`, psr12 `'(int) $x'`, nette `'(int) $x'`, symfony `'(int) $x'` +- The standards: perCs `'(int) $x'`, psr12 `'(int) $x'`, nette `'(int) $x'`, symfony `'(int) $x'`, laravel `'(int) $x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.catchType @@ -853,7 +853,7 @@ The space around the bar between the types of a `catch`. - By default `keep` - Takes: `'catch (A | B $e)'` (a single space around the bar); `'catch (A|B $e)'` (no space around the bar); `keep` -- The standards: perCs `'catch (A|B $e)'`, psr12 `'catch (A | B $e)'`, nette `'catch (A|B $e)'`, symfony `'catch (A|B $e)'` +- The standards: perCs `'catch (A|B $e)'`, psr12 `'catch (A | B $e)'`, nette `'catch (A|B $e)'`, symfony `'catch (A|B $e)'`, laravel `'catch (A|B $e)'` - Rule `DressCode\Rules\Types\TypeDeclarationSpacingRule`, stage Formatting ### spacing.comma @@ -862,7 +862,7 @@ The whitespace around a comma, which stays on the line of what is before it, a l - By default `keep` - Takes: `'$a, $b'` (none before, a single space after); `keep` -- The standards: perCs `'$a, $b'`, psr12 `'$a, $b'`, nette `'$a, $b'`, symfony `'$a, $b'` +- The standards: perCs `'$a, $b'`, psr12 `'$a, $b'`, nette `'$a, $b'`, symfony `'$a, $b'`, laravel `'$a, $b'` - Rule `DressCode\Rules\Whitespace\CommaSpacingRule`, stage Formatting ### spacing.comment.afterCode @@ -871,7 +871,7 @@ The whitespace before a comment that follows code on its line. - By default `keep` - Takes: `'$a; // text'` (at least one space); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'$a; // text'`, symfony `'$a; // text'` +- The standards: perCs `keep`, psr12 `keep`, nette `'$a; // text'`, symfony `'$a; // text'`, laravel `keep` - Rule `DressCode\Rules\Comments\CommentSpacingRule`, stage Finishing - Modifies comments @@ -881,7 +881,7 @@ The space after the marker of a comment and before the closing one, unless anoth - By default `keep` - Takes: `'// text'` (a single space after the marker); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'// text'`, symfony `'// text'` +- The standards: perCs `keep`, psr12 `keep`, nette `'// text'`, symfony `'// text'`, laravel `'// text'` - Rule `DressCode\Rules\Comments\CommentSpacingRule`, stage Finishing - Modifies comments @@ -891,7 +891,7 @@ The space around the concatenation operator, unless it sits at a line break. - By default `keep` - Takes: `'$a . $b'` (a single space around); `'$a.$b'` (no space around); `keep` -- The standards: perCs `'$a . $b'`, psr12 `'$a . $b'`, nette `'$a . $b'`, symfony `'$a.$b'` +- The standards: perCs `'$a . $b'`, psr12 `'$a . $b'`, nette `'$a . $b'`, symfony `'$a.$b'`, laravel `'$a.$b'` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### spacing.declare @@ -900,7 +900,7 @@ The whitespace inside a `declare` statement. - By default `keep` - Takes: `'declare(strict_types=1)'` (none inside); `keep` -- The standards: perCs `'declare(strict_types=1)'`, psr12 `'declare(strict_types=1)'`, nette `'declare(strict_types=1)'`, symfony `'declare(strict_types=1)'` +- The standards: perCs `'declare(strict_types=1)'`, psr12 `'declare(strict_types=1)'`, nette `'declare(strict_types=1)'`, symfony `'declare(strict_types=1)'`, laravel `'declare(strict_types=1)'` - Rule `DressCode\Rules\Files\DeclareSpacingRule`, stage Formatting ### spacing.doubleColon @@ -909,7 +909,7 @@ The whitespace around the double colon. - By default `keep` - Takes: `'Foo::bar()'` (none around); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'Foo::bar()'`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `'Foo::bar()'`, symfony `keep`, laravel `'Foo::bar()'` - Rule `DressCode\Rules\Expressions\MemberAccessSpacingRule`, stage Formatting ### spacing.import @@ -918,7 +918,7 @@ The space around the braces of a group use, which hug the names. - By default `keep` - Takes: `'use Acme\{Foo, Bar};'` (no space around the braces); `keep` -- The standards: perCs `'use Acme\{Foo, Bar};'`, psr12 `'use Acme\{Foo, Bar};'`, nette `'use Acme\{Foo, Bar};'`, symfony `'use Acme\{Foo, Bar};'` +- The standards: perCs `'use Acme\{Foo, Bar};'`, psr12 `'use Acme\{Foo, Bar};'`, nette `'use Acme\{Foo, Bar};'`, symfony `'use Acme\{Foo, Bar};'`, laravel `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.keyword @@ -927,7 +927,7 @@ The space after a keyword followed by code on its line, `return`, `new`, `if`, ` - By default `keep` - Takes: `'return $x'` (a single space); `keep` -- The standards: perCs `'return $x'`, psr12 `'return $x'`, nette `'return $x'`, symfony `'return $x'` +- The standards: perCs `'return $x'`, psr12 `'return $x'`, nette `'return $x'`, symfony `'return $x'`, laravel `'return $x'` - Rule `DressCode\Rules\Classes\ClassHeadSpacingRule`, stage Formatting - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting @@ -937,7 +937,7 @@ The whitespace between `!` and its operand, which stay on one line. - By default `keep` - Takes: `'!$x'` (no space after the operator); `'! $x'` (a single space after the operator); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `['!$x', '! $x']`, psr12 `['!$x', '! $x']`, nette `'!$x'`, symfony `'!$x'` +- The standards: perCs `['!$x', '! $x']`, psr12 `['!$x', '! $x']`, nette `'!$x'`, symfony `'!$x'`, laravel `'! $x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.namedArgument @@ -946,7 +946,7 @@ The whitespace around the colon of a named argument. - By default `keep` - Takes: `'foo(name: $x)'` (none before the colon, a single space after); `keep` -- The standards: perCs `'foo(name: $x)'`, psr12 `keep`, nette `'foo(name: $x)'`, symfony `'foo(name: $x)'` +- The standards: perCs `'foo(name: $x)'`, psr12 `keep`, nette `'foo(name: $x)'`, symfony `'foo(name: $x)'`, laravel `keep` - Rule `DressCode\Rules\Functions\NamedArgumentSpacingRule`, stage Formatting ### spacing.objectOperator @@ -955,7 +955,7 @@ The whitespace around the object operator. - By default `keep` - Takes: `'$a->b()'` (none around); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'$a->b()'`, symfony `'$a->b()'` +- The standards: perCs `keep`, psr12 `keep`, nette `'$a->b()'`, symfony `'$a->b()'`, laravel `'$a->b()'` - Rule `DressCode\Rules\Expressions\MemberAccessSpacingRule`, stage Formatting ### spacing.openBrace @@ -964,7 +964,7 @@ The space before the brace or the body a structure opens on the line of its head - By default `keep` - Takes: `'if ($a) {'` (a single space); `keep` -- The standards: perCs `'if ($a) {'`, psr12 `'if ($a) {'`, nette `'if ($a) {'`, symfony `'if ($a) {'` +- The standards: perCs `'if ($a) {'`, psr12 `'if ($a) {'`, nette `'if ($a) {'`, symfony `'if ($a) {'`, laravel `'if ($a) {'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### spacing.padding.argument @@ -973,7 +973,7 @@ The whitespace inside the parentheses of the arguments of a call, of `new` and o - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.array @@ -982,7 +982,7 @@ The whitespace inside the brackets or the parentheses of an array. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.closureUse @@ -991,7 +991,7 @@ The whitespace inside the parentheses of the `use` of a closure. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.controlStructure @@ -1000,7 +1000,7 @@ The whitespace inside the parentheses of `if`, `elseif`, `while`, `for`, `foreac - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.grouping @@ -1009,7 +1009,7 @@ The whitespace inside the parentheses grouping an expression, `($a + $b) * $c`. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.list @@ -1018,7 +1018,7 @@ The whitespace inside the brackets or the parentheses of a destructuring. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.offset @@ -1027,7 +1027,7 @@ The whitespace inside the brackets of an offset access and before them. - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `exceptLiterals` (a single space around an index other than a string or an integer literal, `$a[ $i ]` but `$a[0]`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.padding.parameter @@ -1036,7 +1036,7 @@ The whitespace inside the parentheses of the parameters of a function, a method, - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting ### spacing.reference @@ -1045,7 +1045,7 @@ The whitespace between `&` and its operand. - By default `keep` - Takes: `'&$x'` (none between); `keep` -- The standards: perCs `'&$x'`, psr12 `'&$x'`, nette `'&$x'`, symfony `'&$x'` +- The standards: perCs `'&$x'`, psr12 `'&$x'`, nette `'&$x'`, symfony `'&$x'`, laravel `keep` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.semicolon.after @@ -1054,7 +1054,7 @@ The whitespace after a semicolon that more code follows on its line. - By default `keep` - Takes: `'for ($i = 0; $i < 5; $i++)'` (a single space after a semicolon); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'for ($i = 0; $i < 5; $i++)'`, symfony `'for ($i = 0; $i < 5; $i++)'` +- The standards: perCs `keep`, psr12 `keep`, nette `'for ($i = 0; $i < 5; $i++)'`, symfony `'for ($i = 0; $i < 5; $i++)'`, laravel `'for ($i = 0; $i < 5; $i++)'` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting ### spacing.semicolon.before @@ -1063,7 +1063,7 @@ The whitespace before a semicolon, which stays on the line of what it ends. - By default `keep` - Takes: `'$a;'` (no space before a semicolon); `keep` -- The standards: perCs `'$a;'`, psr12 `keep`, nette `'$a;'`, symfony `'$a;'` +- The standards: perCs `'$a;'`, psr12 `keep`, nette `'$a;'`, symfony `'$a;'`, laravel `'$a;'` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting ### spacing.spread @@ -1072,7 +1072,7 @@ The whitespace between `...` and its operand. - By default `keep` - Takes: `'...$x'` (none between); `keep` -- The standards: perCs `'...$x'`, psr12 `'...$x'`, nette `'...$x'`, symfony `'...$x'` +- The standards: perCs `'...$x'`, psr12 `'...$x'`, nette `'...$x'`, symfony `'...$x'`, laravel `keep` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ### spacing.switchCase @@ -1081,7 +1081,7 @@ The whitespace before the colon of a `case`. - By default `keep` - Takes: `'case 1:'` (none before the colon); `keep` -- The standards: perCs `'case 1:'`, psr12 `'case 1:'`, nette `'case 1:'`, symfony `'case 1:'` +- The standards: perCs `'case 1:'`, psr12 `'case 1:'`, nette `'case 1:'`, symfony `'case 1:'`, laravel `'case 1:'` - Rule `DressCode\Rules\ControlFlow\SwitchCaseSpacingRule`, stage Formatting ### spacing.ternary @@ -1090,7 +1090,7 @@ The whitespace around `?` and `:` of a ternary, and around `?:` as a whole, unle - By default `keep` - Takes: `'$a ? $b : $c'` (whitespace around `?` and `:`); `keep` -- The standards: perCs `'$a ? $b : $c'`, psr12 `'$a ? $b : $c'`, nette `'$a ? $b : $c'`, symfony `'$a ? $b : $c'` +- The standards: perCs `'$a ? $b : $c'`, psr12 `'$a ? $b : $c'`, nette `'$a ? $b : $c'`, symfony `'$a ? $b : $c'`, laravel `'$a ? $b : $c'` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting ### spacing.typeDeclaration @@ -1099,7 +1099,7 @@ The whitespace of a type declaration, which stays on one line with what it descr - By default `keep` - Takes: `'?int $x'` (none inside a type, a single space before the name); `keep` -- The standards: perCs `'?int $x'`, psr12 `'?int $x'`, nette `'?int $x'`, symfony `'?int $x'` +- The standards: perCs `'?int $x'`, psr12 `'?int $x'`, nette `'?int $x'`, symfony `'?int $x'`, laravel `'?int $x'` - Rule `DressCode\Rules\Types\TypeDeclarationSpacingRule`, stage Formatting ### spacing.unaryOperator @@ -1108,7 +1108,7 @@ The whitespace between a unary operator other than `!` and its operand, which st - By default `keep` - Takes: `'-$x'` (no space after the operator); `keep` -- The standards: perCs `'-$x'`, psr12 `'-$x'`, nette `'-$x'`, symfony `'-$x'` +- The standards: perCs `'-$x'`, psr12 `'-$x'`, nette `'-$x'`, symfony `'-$x'`, laravel `'-$x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting ## multiline @@ -1119,7 +1119,7 @@ The attributes of a class, a function, a method, a property, a constant, a case - By default `keep` - Takes: `above` (every group on a line of its own above the declaration); `sameLine` (every group on the line of the declaration); `keep` -- The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `above` +- The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `above`, laravel `keep` - Rule `DressCode\Rules\Whitespace\AttributePositionRule`, stage Formatting ### multiline.attributePosition.parameter @@ -1128,7 +1128,7 @@ Where the attributes of a parameter stand. - By default `keep` - Takes: `above` (every group on a line of its own above the parameter in a list of parameters spread over lines, on the line of the parameter in a list on one line); `sameLine` (every group on the line of the parameter, in a list spread over lines too); `keep` -- The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `keep` +- The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\AttributePositionRule`, stage Formatting ### multiline.edges.argument @@ -1137,7 +1137,7 @@ Where the arguments spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first argument on the line after `(`, `)` on a line of its own); `attached` (the first argument on the line of `(`, `)` right after the last one); `closingApart` (the first argument on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep` +- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting ### multiline.edges.array @@ -1146,7 +1146,7 @@ Where the items spread over lines begin against the opening bracket and where th - By default `keep` - Takes: `apart` (the first item on the line after the opening bracket, the closing one on a line of its own); `attached` (the first item on the line of the opening bracket, the closing one right after the last one); `closingApart` (the first item on the line of the opening bracket, the closing one on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]`, symfony `keep` +- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.edges.attribute @@ -1155,7 +1155,7 @@ Where the attributes spread over lines begin against `#[` and where `]` stands. - By default `keep` - Takes: `apart` (the first attribute on the line after `#[`, `]` on a line of its own); `attached` (the first attribute on the line of `#[`, `]` right after the last one); `closingApart` (the first attribute on the line of `#[`, `]` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\MultilineAttributeRule`, stage Formatting ### multiline.edges.chain @@ -1164,7 +1164,7 @@ Where the links spread over lines begin against the object. - By default `keep` - Takes: `apart` (the first link on the line after the object); `attached` (the links up to the first method call on the line of the object); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `keep`, nette `attached`, symfony `keep` +- The standards: perCs `apart`, psr12 `keep`, nette `attached`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineChainRule`, stage Formatting ### multiline.edges.closureUse @@ -1173,7 +1173,7 @@ Where the variables spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first variable on the line after `(`, `)` on a line of its own); `attached` (the first variable on the line of `(`, `)` right after the last one); `closingApart` (the first variable on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineClosureUseRule`, stage Formatting ### multiline.edges.condition @@ -1182,7 +1182,7 @@ Where the parts spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first part on the line after `(`, `)` on a line of its own); `attached` (the first part on the line of `(`, `)` right after the last one); `closingApart` (the first part on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `[apart, closingApart]`, symfony `keep` +- The standards: perCs `apart`, psr12 `apart`, nette `[apart, closingApart]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting ### multiline.edges.implements @@ -1191,7 +1191,7 @@ Where the interfaces spread over lines begin against the keyword. - By default `keep` - Takes: `apart` (the first interface on the line after the keyword); `attached` (the first interface on the line of the keyword); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep` +- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\MultilineImplementsRule`, stage Formatting ### multiline.edges.import @@ -1200,7 +1200,7 @@ Where the names spread over lines begin against `{` and where `}` stands. - By default `keep` - Takes: `apart` (the first name on the line after `{`, `}` on a line of its own); `attached` (the first name on the line of `{`, `}` right after the last one); `closingApart` (the first name on the line of `{`, `}` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `apart`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `apart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\MultilineImportRule`, stage Formatting ### multiline.edges.list @@ -1209,7 +1209,7 @@ Where the items spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first item on the line after `(`, `)` on a line of its own); `attached` (the first item on the line of `(`, `)` right after the last one); `closingApart` (the first item on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]`, symfony `keep` +- The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.edges.parameter @@ -1218,7 +1218,7 @@ Where the parameters spread over lines begin against `(` and where `)` stands. - By default `keep` - Takes: `apart` (the first parameter on the line after `(`, `)` on a line of its own); `attached` (the first parameter on the line of `(`, `)` right after the last one); `closingApart` (the first parameter on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep` +- The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting ### multiline.expressionStart.arrayItem @@ -1227,7 +1227,7 @@ Where an expression spanning several lines after the `=>` of an array item begin - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### multiline.expressionStart.arrowFunction @@ -1236,7 +1236,7 @@ Where an expression spanning several lines after the `=>` of an arrow function b - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### multiline.expressionStart.assignment @@ -1245,7 +1245,7 @@ Where an expression spanning several lines after the `=` of an assignment, a con - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting ### multiline.expressionStart.echo @@ -1254,7 +1254,7 @@ Where an expression spanning several lines after `echo` and `print` begins. - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.expressionStart.return @@ -1263,7 +1263,7 @@ Where an expression spanning several lines after `return` begins. - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.expressionStart.throw @@ -1272,7 +1272,7 @@ Where an expression spanning several lines after `throw` begins. - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.expressionStart.yield @@ -1281,7 +1281,7 @@ Where an expression spanning several lines after `yield` and `yield from` begins - By default `keep` - Takes: `sameLine` (on the line of what comes before); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting ### multiline.fillMinItems.array @@ -1290,7 +1290,7 @@ From how many items without keys an array written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2` +- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2`, laravel `2` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.fillMinItems.import @@ -1299,7 +1299,7 @@ From how many names a group use written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2` +- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2`, laravel `2` - Rule `DressCode\Rules\Namespaces\MultilineImportRule`, stage Formatting ### multiline.fillMinItems.list @@ -1308,7 +1308,7 @@ From how many items without keys `list()` written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2` +- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2`, laravel `2` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.fillMinItems.matchCondition @@ -1317,7 +1317,7 @@ From how many conditions an arm written in `fill` fills its lines. - By default `2` - Takes: a count from 2 -- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2` +- The standards: perCs `2`, psr12 `2`, nette `6`, symfony `2`, laravel `2` - Rule `DressCode\Rules\Expressions\MultilineMatchArmRule`, stage Formatting ### multiline.items.argument @@ -1328,7 +1328,7 @@ A sole argument that is a string, a message, is not spread for its width - By default `keep` - Takes: `perLine` (every argument on a line of its own); `fill` (the arguments may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting ### multiline.items.arithmetic @@ -1339,7 +1339,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.array @@ -1350,7 +1350,7 @@ Written as `fill`, the items without keys fill the lines evenly from `multiline. - By default `keep` - Takes: `perLine` (every item on a line of its own); `fill` (the items may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `fill`, symfony `keep` +- The standards: perCs `perLine`, psr12 `keep`, nette `fill`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.items.attribute @@ -1359,7 +1359,7 @@ The attributes of one group spread over lines by their author or for their width - By default `keep` - Takes: `perLine` (every attribute on a line of its own); `fill` (the attributes may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\MultilineAttributeRule`, stage Formatting ### multiline.items.bitwise @@ -1370,7 +1370,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.chain @@ -1379,7 +1379,7 @@ The links of a chain of method calls and property accesses spread over lines by - By default `keep` - Takes: `perLine` (every link on a line of its own); `fill` (the links may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `perLine`, symfony `keep` +- The standards: perCs `perLine`, psr12 `keep`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineChainRule`, stage Formatting ### multiline.items.closureUse @@ -1388,7 +1388,7 @@ The variables of the `use` of a closure spread over lines by their author or for - By default `keep` - Takes: `perLine` (every variable on a line of its own); `fill` (the variables may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineClosureUseRule`, stage Formatting ### multiline.items.coalesce @@ -1399,7 +1399,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.concatenation @@ -1410,7 +1410,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.condition @@ -1419,7 +1419,7 @@ A condition of `if`, `elseif`, `while` and `do-while` joined by boolean operator - By default `keep` - Takes: `perLine` (every part on a line of its own); `fill` (the parts may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting ### multiline.items.for @@ -1428,7 +1428,7 @@ The sections of the header of a `for` spread over lines by its author or for the - By default `keep` - Takes: `perLine` (every section on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MultilineForRule`, stage Formatting ### multiline.items.implements @@ -1437,7 +1437,7 @@ The interfaces a class or an enum implements, or an interface extends, spread ov - By default `keep` - Takes: `perLine` (every interface on a line of its own); `fill` (the interfaces may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\MultilineImplementsRule`, stage Formatting ### multiline.items.import @@ -1448,7 +1448,7 @@ Written as `fill`, the names fill the lines evenly from `multiline.fillMinItems. - By default `keep` - Takes: `perLine` (every name on a line of its own); `fill` (the names may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `fill`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `fill`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\MultilineImportRule`, stage Formatting ### multiline.items.list @@ -1459,7 +1459,7 @@ Written as `fill`, the items without keys fill the lines evenly from `multiline. - By default `keep` - Takes: `perLine` (every item on a line of its own); `fill` (the items may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `fill`, symfony `keep` +- The standards: perCs `perLine`, psr12 `keep`, nette `fill`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.items.logical @@ -1470,7 +1470,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep` +- The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.matchArm @@ -1481,7 +1481,7 @@ Not about the conditions of one arm, which `multiline.items.matchCondition` spre - By default `keep` - Takes: `perLine` (every arm on a line of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `perLine`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineMatchRule`, stage Formatting ### multiline.items.matchCondition @@ -1492,7 +1492,7 @@ Written as `fill`, the conditions fill the lines evenly from `multiline.fillMinI - By default `keep` - Takes: `perLine` (every condition on a line of its own); `fill` (the conditions may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `fill`, symfony `keep` +- The standards: perCs `perLine`, psr12 `perLine`, nette `fill`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineMatchArmRule`, stage Formatting ### multiline.items.parameter @@ -1501,7 +1501,7 @@ The parameters of a function or a method spread over lines by their author or fo - By default `keep` - Takes: `perLine` (every parameter on a line of its own); `fill` (the parameters may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep` +- The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting ### multiline.items.pipe @@ -1512,7 +1512,7 @@ Not about the operators of one place joined by another, which begin a family of - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.items.ternary @@ -1521,7 +1521,7 @@ A ternary spread over lines by its author, or for the width of its line. - By default `keep` - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` -- The standards: perCs `perLine`, psr12 `keep`, nette `perLine`, symfony `keep` +- The standards: perCs `perLine`, psr12 `keep`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineTernaryRule`, stage Formatting ### multiline.maxWidth.array @@ -1530,7 +1530,7 @@ The width from bracket to bracket over which an array of several items written o - By default `none` - Takes: a count from 1; `none` (never spread for its width) -- The standards: perCs `none`, psr12 `none`, nette `130`, symfony `none` +- The standards: perCs `none`, psr12 `none`, nette `130`, symfony `none`, laravel `none` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting ### multiline.operatorPosition.arithmetic @@ -1539,7 +1539,7 @@ Where `+`, `-`, `*`, `/`, `%` and `**` stand at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.bitwise @@ -1548,7 +1548,7 @@ Where `|`, `&`, `^`, `<<` and `>>` stand at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.coalesce @@ -1557,7 +1557,7 @@ Where `??` stands at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.comparison @@ -1566,7 +1566,7 @@ Where a comparison stands at a line break: one ending its line moves only where - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting - Read by rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, which this decision does not turn on @@ -1576,7 +1576,7 @@ Where `.` stands at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep` +- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.condition @@ -1585,7 +1585,7 @@ Where a boolean operator chaining a condition of `if`, `elseif`, `while` and `do - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting ### multiline.operatorPosition.logical @@ -1594,7 +1594,7 @@ Where `&&`, `||`, `and`, `or` and `xor` outside a condition stand at a line brea - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.pipe @@ -1603,7 +1603,7 @@ Where `|>` stands at a line break. - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep` +- The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting ### multiline.operatorPosition.ternary @@ -1612,7 +1612,7 @@ Where `?`, `:` and `?:` of a ternary stand at a line break, of a ternary spread - By default `keep` - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` -- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep` +- The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting - Read by rule `DressCode\Rules\Expressions\MultilineTernaryRule`, which this decision does not turn on @@ -1622,7 +1622,7 @@ Where the semicolon of a statement spanning several lines stands. - By default `keep` - Takes: `sameLine` (closing the last line of the statement); `nextLine` (on a line of its own below a chain of method calls spread over lines, closing the last line of any other statement); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting ### multiline.spreadAlways @@ -1631,7 +1631,7 @@ What is spread over lines whatever its width. - By default `[]` - Takes: a list of `promotedProperty` (the parameters of a signature declaring a promoted property) -- The standards: perCs `[]`, psr12 `[]`, nette `[promotedProperty]`, symfony `[]` +- The standards: perCs `[]`, psr12 `[]`, nette `[promotedProperty]`, symfony `[]`, laravel `[]` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting ### multiline.spreadItem @@ -1640,7 +1640,7 @@ What a call too wide spreads where an argument could spread in its place; one so - By default `never` - Takes: `never` (the call spreads its arguments); `sole` (a sole argument that is a call, `new` or an array spreads in the parentheses, `foo(bar(` and `))` together); `last` (as `sole`, and so does the last argument after those before it where the line fits up to its bracket, `foo($a, [` and `])` together) -- The standards: perCs `never`, psr12 `never`, nette `sole`, symfony `never` +- The standards: perCs `never`, psr12 `never`, nette `sole`, symfony `never`, laravel `never` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting ### multiline.trailingComma.argument @@ -1649,7 +1649,7 @@ The trailing comma of the multi-line arguments of a call and the variables of `i - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `optional` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.array @@ -1658,7 +1658,7 @@ The trailing comma of a multi-line array, a destructuring written with `[...]` i - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.closureUse @@ -1667,7 +1667,7 @@ The trailing comma of a multi-line `use` of a closure; any value but `keep` also - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.import @@ -1676,7 +1676,7 @@ The trailing comma of the names of a multi-line group use; any value but `keep` - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional` +- The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.list @@ -1685,7 +1685,7 @@ The trailing comma of a multi-line `list()`; any value but `keep` also removes t - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional` +- The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.matchArm @@ -1694,7 +1694,7 @@ The trailing comma of the arms of a multi-line `match`; any value but `keep` als - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required`, laravel `keep` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ### multiline.trailingComma.parameter @@ -1703,7 +1703,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - By default `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure ## braces @@ -1714,7 +1714,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\NoAlternativeSyntaxRule`, stage Structure ### braces.bareStatementGroup @@ -1723,7 +1723,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\UselessBracesRule`, stage Structure ### braces.bracelessBody @@ -1732,7 +1732,7 @@ The body of a control structure written without braces, `if ($a) foo();`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\NoBracelessBodiesRule`, stage Structure - Modifies comments @@ -1742,7 +1742,7 @@ How the empty body of an anonymous class is written, one written on one line sta - By default `keep` - Takes: `collapsed` (`{}` on the line of the head, one holding a comment staying on its line as written); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `collapsed`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.class @@ -1751,7 +1751,7 @@ How the empty body of a class, an interface, a trait and an enum is written; a c - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.closure @@ -1760,7 +1760,7 @@ How the empty body of a closure is written; a comment inside makes it not empty. - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.controlStructure @@ -1769,7 +1769,7 @@ How the empty body of an `if`, `else`, a loop, `declare`, `try`, `catch` and `fi - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.function @@ -1778,7 +1778,7 @@ How the empty body of a function and a method is written; a comment inside makes - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.emptyBody.propertyHook @@ -1787,7 +1787,7 @@ How the empty body of a property hook is written; a comment inside makes it not - By default `keep` - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` -- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded` +- The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded`, laravel `keep` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.anonymousClass @@ -1796,7 +1796,7 @@ Where the `{` of an anonymous class stands, on the next line always where its in - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.class @@ -1805,7 +1805,7 @@ Where the `{` of a class, an interface, a trait and an enum stands, the members - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine` +- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine`, laravel `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.closure @@ -1814,7 +1814,7 @@ Where the `{` of a closure stands, the body starting a line below it and the `}` - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.continuingKeyword @@ -1823,7 +1823,7 @@ Where the keyword continuing a structure after its `}` stands: `else`, `elseif`, - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.controlStructure @@ -1832,7 +1832,7 @@ Where the `{` of `if`, `else`, a loop, `declare`, `try`, `catch`, `finally`, `sw - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.function @@ -1841,7 +1841,7 @@ Where the `{` of a function and of a method whose parameters stand on one line s - By default `keep` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` -- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine` +- The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine`, laravel `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.multilineSignature @@ -1850,7 +1850,7 @@ Where the `{` of a function and of a method whose parameters are spread over lin - By default `keep` - Takes: `sameLine` (on the line of the `)`); `nextLine` (on the line below the `)`); `keep` -- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine` +- The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.position.typedMultilineSignature @@ -1859,7 +1859,7 @@ Where the `{` of a function and of a method whose parameters are spread over lin - By default `keep` - Takes: `sameLine` (on the line of the return type); `nextLine` (on the line below the return type); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `nextLine`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `nextLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ### braces.singlelineClosure @@ -1868,7 +1868,7 @@ A closure written whole on one line. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `keep`, symfony `keep` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `keep`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting ## blankLines @@ -1879,7 +1879,7 @@ Between the last group of attributes and the declaration, the member, the parame - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `0`, psr12 `keep`, nette `0`, symfony `0` +- The standards: perCs `0`, psr12 `keep`, nette `0`, symfony `0`, laravel `keep` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterDeclare @@ -1888,7 +1888,7 @@ After `declare(...);`, before whatever follows it but a namespace declaration. - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `keep` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterImports @@ -1897,7 +1897,7 @@ After the last import, before the rest of the code. - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterNamespace @@ -1906,7 +1906,7 @@ After an unbraced namespace declaration. - By default `keep` - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` -- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1` +- The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting ### blankLines.afterOpeningTag @@ -1915,7 +1915,7 @@ After the line of `bar - By default `keep` - Takes: `forbidden` (never there); `required` (always there); `keep` -- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\InstantiationWrappingRule`, stage Structure - Needs PHP >=8.4 @@ -2279,7 +2279,7 @@ The annotations and the attributes that keep an internal class as it is, an enti - By default `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]` - Takes: a list of names -- The standards: perCs `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, psr12 `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, nette `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, symfony `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]` +- The standards: perCs `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, psr12 `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, nette `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, symfony `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]`, laravel `[@final, @Entity, @ORM\Entity, @ORM\Mapping\Entity, @Mapping\Entity, @Document, @ODM\Document, Doctrine\ORM\Mapping\Entity, Doctrine\ODM\MongoDB\Mapping\Annotations\Document]` - Rule `DressCode\Rules\Classes\FinalForInternalClassRule`, stage Structure ### classes.internalClass.final @@ -2288,7 +2288,7 @@ The `final` of a class its author marked as internal, which nothing outside the - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\FinalForInternalClassRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2298,7 +2298,7 @@ The annotations, with their `@`, and the attributes, by their fully qualified cl - By default `[@internal]` - Takes: a list of names -- The standards: perCs `[@internal]`, psr12 `[@internal]`, nette `[@internal]`, symfony `[@internal]` +- The standards: perCs `[@internal]`, psr12 `[@internal]`, nette `[@internal]`, symfony `[@internal]`, laravel `[@internal]` - Rule `DressCode\Rules\Classes\FinalForInternalClassRule`, stage Structure ### classes.members.grouping.constant @@ -2307,7 +2307,7 @@ A declaration of several constants of a class, `const A = 1, B = 2;`. - By default `keep` - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` -- The standards: perCs `separated`, psr12 `keep`, nette `keep`, symfony `separated` +- The standards: perCs `separated`, psr12 `keep`, nette `keep`, symfony `separated`, laravel `separated` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure ### classes.members.grouping.property @@ -2316,7 +2316,7 @@ A declaration of several properties, `public $a, $b;`. - By default `keep` - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` -- The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated` +- The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated`, laravel `separated` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure ### classes.members.grouping.traitUse @@ -2325,7 +2325,7 @@ A `use` of several traits, `use A, B;`. - By default `keep` - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` -- The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated` +- The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated`, laravel `keep` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure ### classes.members.order @@ -2334,7 +2334,7 @@ The order of the members of a class by kind, a member taking the most particular - By default `keep` - Takes: an order of `traitUse` (a trait use); `enumCase` (a case of an enum); `constant` (a constant of any visibility); `publicConstant` (a public constant); `protectedConstant` (a protected constant); `privateConstant` (a private constant); `property` (a property of any visibility); `publicProperty` (a public property); `protectedProperty` (a protected property); `privateProperty` (a private property); `publicStaticProperty` (a public static property); `protectedStaticProperty` (a protected static property); `privateStaticProperty` (a private static property); `method` (a method of any visibility); `publicMethod` (a public method); `protectedMethod` (a protected method); `privateMethod` (a private method); `publicStaticMethod` (a public static method); `protectedStaticMethod` (a protected static method); `privateStaticMethod` (a private static method); `constructor` (the constructor); `destructor` (the destructor); `magicMethod` (a method whose name begins with `__`); `keep` -- The standards: perCs `[traitUse]`, psr12 `[traitUse]`, nette `[traitUse, enumCase, publicConstant, protectedConstant, privateConstant, publicProperty, protectedProperty, privateProperty]`, symfony `[traitUse]` +- The standards: perCs `[traitUse]`, psr12 `[traitUse]`, nette `[traitUse, enumCase, publicConstant, protectedConstant, privateConstant, publicProperty, protectedProperty, privateProperty]`, symfony `[traitUse]`, laravel `keep` - Rule `DressCode\Rules\Classes\MemberOrderRule`, stage Structure ### classes.modifiers.implied @@ -2343,7 +2343,7 @@ A modifier PHP implies anyway, `final` in a final class, `readonly` in a readonl - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\UselessModifierRule`, stage Structure ### classes.modifiers.order @@ -2352,7 +2352,7 @@ The order of the modifiers of a property, a promoted one included, a method, a c - By default `keep` - Takes: `canonical` (`abstract` or `final`, visibility, set visibility, `static`, `readonly`); `keep` -- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical` +- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical`, laravel `canonical` - Rule `DressCode\Rules\Classes\ModifierOrderRule`, stage Structure ### classes.overridingMethod.parameterName @@ -2361,7 +2361,7 @@ The names of the parameters of a method overriding one of a parent class or an i - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\OverridingSignatureRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - Modifies comments @@ -2373,7 +2373,7 @@ A method that only forwards to the parent with the same arguments adds nothing. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\UselessOverridingMethodRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2384,7 +2384,7 @@ The attribute where the method declares the return type of what it implements. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\UselessReturnTypeWillChangeRule`, stage Structure ### classes.overridingMethod.signature @@ -2393,7 +2393,7 @@ The return type, the types of the parameters, the parameters added with a defaul - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\OverridingSignatureRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - Modifies comments @@ -2405,7 +2405,7 @@ A `protected` member of an enum, which is `private`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Classes\PrivateForProtectedRule`, stage Structure ### classes.protectedInFinalClass @@ -2414,7 +2414,7 @@ A `protected` member of a final class with no parent and no trait, which is `pri - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Classes\PrivateForProtectedRule`, stage Structure ### classes.staticMethod @@ -2423,7 +2423,7 @@ The `static` keyword of a method that does not use `$this`, of the visibilities - By default `keep` - Takes: `required` (always there, `static function`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\StaticForMethodWithoutThisRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2433,7 +2433,7 @@ The visibilities of the methods made static. - By default `[private]` - Takes: a list of `private` (a private method); `protected` (a protected method, where no child and no ancestor can declare it); `public` (a public method, where no child and no ancestor can declare it) -- The standards: perCs `[private]`, psr12 `[private]`, nette `[private]`, symfony `[private]` +- The standards: perCs `[private]`, psr12 `[private]`, nette `[private]`, symfony `[private]`, laravel `[private]` - Rule `DressCode\Rules\Classes\StaticForMethodWithoutThisRule`, stage Structure ### classes.untypedPropertyNullDefault @@ -2442,7 +2442,7 @@ The visibilities of the methods made static. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Classes\UselessNullInitializationRule`, stage Structure ### classes.visibility.constant @@ -2451,7 +2451,7 @@ The visibility of a constant of a class, an interface, a trait or an enum. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `keep` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.interfaceMethod @@ -2460,7 +2460,7 @@ The `public` of a method of an interface, which is public whatever it says. - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `forbidden`, symfony `required` +- The standards: perCs `required`, psr12 `required`, nette `forbidden`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.method @@ -2469,7 +2469,7 @@ The visibility of a method outside an interface, whose methods take `interfaceMe - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.property @@ -2478,7 +2478,7 @@ The visibility of a property, a promoted one included, `var` becoming `public` a - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required` +- The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure ### classes.visibility.publicWithSet @@ -2487,7 +2487,7 @@ The `public` of a property with a set visibility, `public protected(set) int $x` - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `required`, nette `required`, symfony `required` +- The standards: perCs `keep`, psr12 `required`, nette `required`, symfony `required`, laravel `keep` - Rule `DressCode\Rules\Classes\PublicWithSetVisibilityRule`, stage Structure - Needs PHP >=8.4 @@ -2501,7 +2501,7 @@ Not an arrow function, whose value is its expression. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments @@ -2511,7 +2511,7 @@ A class constant declares the type of its value. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\ConstantTypeRequiredRule`, stage Structure - Needs PHP >=8.3 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2524,7 +2524,7 @@ Not a parameter of a closure, whose types belong to the call it is passed to. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2535,7 +2535,7 @@ A property declares its type, a promoted one included, taken from `@var`, or the - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2546,7 +2546,7 @@ A function or a method declares its return type, taken from `@return`, `void` wh - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2557,7 +2557,7 @@ How a native type of one type and `null` is written. - By default `keep` - Takes: `'?int'` (a single type with `null` written with `?`); `'int|null'` (a union with `null`, which `types.nullPosition` places); `keep` -- The standards: perCs `'?int'`, psr12 `keep`, nette `'?int'`, symfony `'?int'` +- The standards: perCs `'?int'`, psr12 `keep`, nette `'?int'`, symfony `'?int'`, laravel `'?int'` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure ### types.nullPosition @@ -2566,7 +2566,7 @@ Where `null` stands in a native union type. - By default `keep` - Takes: `last` (`int|string|null`); `first` (`null|int|string`); `keep` -- The standards: perCs `last`, psr12 `keep`, nette `last`, symfony `last` +- The standards: perCs `last`, psr12 `keep`, nette `last`, symfony `last`, laravel `keep` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure ### types.traversableClasses @@ -2575,7 +2575,7 @@ Classes treated like `array` and `iterable`, whose annotation says what their it - By default `[Traversable]` - Takes: a list of names -- The standards: perCs `[Traversable]`, psr12 `[Traversable]`, nette `[Traversable]`, symfony `[Traversable]` +- The standards: perCs `[Traversable]`, psr12 `[Traversable]`, nette `[Traversable]`, symfony `[Traversable]`, laravel `[Traversable]` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments @@ -2586,7 +2586,7 @@ The order of the types of a native union. - By default `keep` - Takes: `alphabetical` (the types beside `null` sorted by name, case-insensitively, an intersection by its first name); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure ## functions @@ -2597,7 +2597,7 @@ The order of the types of a native union. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `keep`, symfony `required` +- The standards: perCs `required`, psr12 `keep`, nette `keep`, symfony `required`, laravel `keep` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure ### functions.arrowFunction.oneExpression @@ -2608,7 +2608,7 @@ Not about the `static` of the arrow function, which `functions.staticClosure` de - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure ### functions.arrowFunctionNested @@ -2617,7 +2617,7 @@ Whether a closure holding another closure or arrow function becomes one too. - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `yes`, symfony `no` +- The standards: perCs `no`, psr12 `no`, nette `yes`, symfony `no`, laravel `no` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure ### functions.innerFunction @@ -2626,7 +2626,7 @@ A named function declared inside a function, a method, a closure or a property h - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoInnerFunctionsRule`, stage Structure ### functions.namedBooleanArgument @@ -2635,7 +2635,7 @@ A `true` or `false` passed by position to a parameter declared `bool`, written w - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NamedArgumentForBooleanRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2645,7 +2645,7 @@ The functions and methods whose arguments stay positional, a method written with - By default `[]` - Takes: a list of names -- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]` +- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]`, laravel `[]` - Rule `DressCode\Rules\Functions\NamedArgumentForBooleanRule`, stage Structure ### functions.staticClosure @@ -2654,7 +2654,7 @@ The `static` keyword of a closure or an arrow function that does not use `$this` - By default `keep` - Takes: `required` (always there, `static function`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\StaticForClosureWithoutThisRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2664,7 +2664,7 @@ The `static` keyword of a closure or an arrow function that does not use `$this` - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\UselessReturnRule`, stage Structure ### functions.unreachableParameterDefault @@ -2673,7 +2673,7 @@ A default a required parameter after it makes unreachable. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\Functions\UselessParameterDefaultRule`, stage Structure ## hygiene @@ -2684,7 +2684,7 @@ The sets of functions whose aliases are written by their canonical names. - By default `keep` - Takes: a list of `all` (every set below); `core` (the core functions, `sizeof()` is `count()`, `join()` is `implode()`); `imap` (`imap_*`); `ldap` (`ldap_*`); `mysqli` (`mysqli_*`); `pg` (`pg_exec()` is `pg_query()`); `oci` (`oci_*`); `odbc` (`odbc_*`); `openssl` (`openssl_*`); `sodium` (`sodium_*`); `ftp` (`ftp_quit()` is `ftp_close()`); `posix` (`posix_errno()` is `posix_get_last_error()`); `pcntl` (`pcntl_errno()` is `pcntl_get_last_error()`); `time` (`mktime()` and `gmmktime()` without arguments are `time()`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[core, imap, pg]`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `[core, imap, pg]`, symfony `keep`, laravel `[core, imap, pg]` - Rule `DressCode\Rules\Functions\NoAliasFunctionsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2694,7 +2694,7 @@ The sets of functions whose aliases are written by their canonical names. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoCallUserFuncRule`, stage Structure - Needs PHP >=7.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2705,7 +2705,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no`, laravel `no` - Rule `DressCode\Rules\Functions\NoCallUserFuncRule`, stage Structure - Needs PHP >=7.0 @@ -2715,7 +2715,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ClassNameNotationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2725,7 +2725,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ClassNameNotationRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2737,7 +2737,7 @@ Not about which name of `floatval()` the code writes, `doubleval()` being an ali - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoConversionFunctionsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2747,7 +2747,7 @@ An argument PHP ignores or that repeats the default. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\RedundantArgumentsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2757,7 +2757,7 @@ An argument PHP ignores or that repeats the default. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoDirnameOfFileRule`, stage Structure ### hygiene.is_null @@ -2766,7 +2766,7 @@ An argument PHP ignores or that repeats the default. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoIsNullRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2776,7 +2776,7 @@ A `dirname()` of a `dirname()`, which is one call with the `levels` argument. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoDirnameOfFileRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2786,7 +2786,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\NoRepeatedCatchesRule`, stage Structure - Needs PHP >=7.1 @@ -2796,7 +2796,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoSeparateIssetsRule`, stage Structure ### hygiene.separateUnsets @@ -2805,7 +2805,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoSeparateUnsetsRule`, stage Structure ### hygiene.settype @@ -2814,7 +2814,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoSettypeRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2824,7 +2824,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoManualEmptyStringTestsRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2835,7 +2835,7 @@ The variable of a `catch` that is never read, `catch (E)` written instead. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\UselessCatchVariableRule`, stage Structure - Needs PHP >=8.0 @@ -2845,7 +2845,7 @@ The variable of a `catch` that is never read, `catch (E)` written instead. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoExplicitInvokeCallsRule`, stage Structure ## controlFlow @@ -2858,7 +2858,7 @@ Not about an `if` and an `else` each returning a value, which `controlFlow.ifEls - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\UselessElseRule`, stage Structure ### controlFlow.elseif @@ -2867,7 +2867,7 @@ The keyword of a branch that follows `if`. - By default `keep` - Takes: `'elseif'` (`elseif` in one word, never `else if`); `keep` -- The standards: perCs `'elseif'`, psr12 `'elseif'`, nette `'elseif'`, symfony `'elseif'` +- The standards: perCs `'elseif'`, psr12 `'elseif'`, nette `'elseif'`, symfony `'elseif'`, laravel `'elseif'` - Rule `DressCode\Rules\ControlFlow\ElseifNotationRule`, stage Structure ### controlFlow.elseifAfterReturn @@ -2876,7 +2876,7 @@ An `elseif` after an `if` that always leaves, which becomes an `if` of its own w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\UselessElseRule`, stage Structure ### controlFlow.emptyStatement @@ -2885,7 +2885,7 @@ The second `;` of `foo();;`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\NoEmptyStatementsRule`, stage Structure ### controlFlow.ifElseReturning @@ -2896,7 +2896,7 @@ Not about the ternary of `true` and `false` it may write, which `expressions.ter - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\TernaryForIfRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2908,7 +2908,7 @@ Not about branches returning other values, which `controlFlow.ifElseReturning` w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\ReturnForBooleanIfRule`, stage Structure ### controlFlow.switch.fallThroughComment @@ -2917,7 +2917,7 @@ The text of the comment a non-empty `case` falling through to the next one carri - By default `keep` - Takes: a text, or a list of them, every one passing and the first written; `keep` -- The standards: perCs `no break`, psr12 `no break`, nette `break omitted`, symfony `no break` +- The standards: perCs `no break`, psr12 `no break`, nette `break omitted`, symfony `no break`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\FallThroughCommentRule`, stage Structure - Modifies comments @@ -2927,7 +2927,7 @@ An `if` ending a function or a loop body becomes a guard that leaves early, and - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\EarlyExitForTrailingIfRule`, stage Structure ### controlFlow.trailingIfMinStatements @@ -2936,7 +2936,7 @@ The statements the body of a trailing `if` has at least for it to become a guard - By default `2` - Takes: a count from 1 -- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2` +- The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2`, laravel `2` - Rule `DressCode\Rules\ControlFlow\EarlyExitForTrailingIfRule`, stage Structure ## expressions @@ -2947,7 +2947,7 @@ The statements the body of a trailing `if` has at least for it to become a guard - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Expressions\IncrementRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2957,7 +2957,7 @@ An increment or a decrement whose value is not read, a statement or a step of `f - By default `keep` - Takes: `prefix` (`++$i`, `--$i`); `postfix` (`$i++`, `$i--`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `postfix`, symfony `prefix` +- The standards: perCs `keep`, psr12 `keep`, nette `postfix`, symfony `prefix`, laravel `postfix` - Rule `DressCode\Rules\Expressions\IncrementRule`, stage Structure ### expressions.assignment.targetInValue @@ -2968,7 +2968,7 @@ Not about the `$a += 1` it may write, which `expressions.assignment.addingOne` w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\CombinedAssignmentForRepeatedTargetRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2978,7 +2978,7 @@ The type name of a cast. - By default `keep` - Takes: `canonical` (`(int)`, `(bool)`, `(float)`, `(string)` in lowercase, never `(integer)`, `(Boolean)`, `(double)`, `(binary)`); `keep` -- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical` +- The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical`, laravel `canonical` - Rule `DressCode\Rules\Expressions\CastCanonicalTypeRule`, stage Structure ### expressions.coalesce @@ -2987,7 +2987,7 @@ The type name of a cast. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\NullCoalescingForNullTernaryRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2997,7 +2997,7 @@ A loose comparison, `==`, `!=` or `<>`, is written `===` or `!==`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\NoLooseComparisonsRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3009,7 +3009,7 @@ Not about whether the comparison is loose, which `expressions.comparison.loose` - By default `keep` - Takes: `'!='` (`!=`, never `<>`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'!='`, symfony `'!='` +- The standards: perCs `keep`, psr12 `keep`, nette `'!='`, symfony `'!='`, laravel `'!='` - Rule `DressCode\Rules\Expressions\NotEqualsNotationRule`, stage Structure ### expressions.comparison.yoda @@ -3018,7 +3018,7 @@ A Yoda comparison, the constant on its left side, a comparison of two variables - By default `keep` - Takes: `forbidden` (never there, the variable on the left and the constant on the right, `$a === 1`); `required` (always there, the constant on the left, `1 === $a`, so that an accidental assignment does not compile); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `required` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `required`, laravel `forbidden` - Rule `DressCode\Rules\Expressions\YodaRule`, stage Structure ### expressions.doubleNegation @@ -3027,7 +3027,7 @@ A Yoda comparison, the constant on its left side, a comparison of two variables - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Expressions\NoDoubleNegationsRule`, stage Structure ### expressions.explicitPrecedence @@ -3036,7 +3036,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\ExplicitPrecedenceRequiredRule`, stage Structure ### expressions.parenthesesAfterConstruct @@ -3045,7 +3045,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Expressions\UselessParenthesesAfterConstructRule`, stage Structure ### expressions.ternary.ofTrueFalse @@ -3054,7 +3054,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\UselessTernaryOperatorRule`, stage Structure ### expressions.ternary.short @@ -3063,7 +3063,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\ShortTernaryRule`, stage Structure ### expressions.wordLogicalOperator @@ -3072,7 +3072,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\LogicalOperatorNotationRule`, stage Structure ## literals @@ -3083,7 +3083,7 @@ The syntax of an array. - By default `keep` - Takes: `short` (`[1, 2]`, never `array(1, 2)`); `long` (`array(1, 2)`, never `[1, 2]`); `keep` -- The standards: perCs `short`, psr12 `keep`, nette `short`, symfony `short` +- The standards: perCs `short`, psr12 `keep`, nette `short`, symfony `short`, laravel `short` - Rule `DressCode\Rules\Arrays\ArraySyntaxRule`, stage Structure ### literals.arraySyntax.list @@ -3092,7 +3092,7 @@ The syntax of a destructuring. - By default `keep` - Takes: `short` (`[$a, $b] =`, never `list($a, $b) =`); `long` (`list($a, $b) =`, never `[$a, $b] =`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `short`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `short`, symfony `keep`, laravel `short` - Rule `DressCode\Rules\Arrays\ArraySyntaxRule`, stage Structure ### literals.classNameInString @@ -3101,7 +3101,7 @@ The syntax of a destructuring. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ClassKeywordForStringRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3111,7 +3111,7 @@ Two string literals concatenated on one line, which are one literal, `'ab'` for - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure ### literals.concatenatedLiteralsOverLines @@ -3120,7 +3120,7 @@ Whether two string literals concatenated over a line break are one literal too. - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no`, laravel `no` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure ### literals.digitSeparator.fraction @@ -3129,7 +3129,7 @@ The separator grouping by three the digits of the fraction of a decimal number, - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.digitSeparator.integer @@ -3138,7 +3138,7 @@ The separator grouping by three the digits of a decimal integer, or of the integ - By default `keep` - Takes: `required` (always there); `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.digitSeparatorMinDigits.fraction @@ -3147,7 +3147,7 @@ The number of digits from which `literals.digitSeparator.fraction` groups them. - By default `4` - Takes: a count from 1 -- The standards: perCs `4`, psr12 `4`, nette `20`, symfony `4` +- The standards: perCs `4`, psr12 `4`, nette `20`, symfony `4`, laravel `4` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.digitSeparatorMinDigits.integer @@ -3156,7 +3156,7 @@ The number of digits from which `literals.digitSeparator.integer` groups them. - By default `4` - Takes: a count from 1 -- The standards: perCs `4`, psr12 `4`, nette `7`, symfony `4` +- The standards: perCs `4`, psr12 `4`, nette `7`, symfony `4`, laravel `4` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure ### literals.emptyStringConcatenation @@ -3165,7 +3165,7 @@ The concatenation with an empty string, which is a cast or nothing, `(string) $x - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure ### literals.heredocWithoutInterpolation @@ -3174,7 +3174,7 @@ A heredoc with nothing to interpolate, which is written as a nowdoc. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\Literals\NowdocForHeredocRule`, stage Structure ### literals.implicitBackslash @@ -3183,7 +3183,7 @@ A backslash of a string that escapes nothing, written `\\` instead. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NoImplicitBackslashesRule`, stage Structure ### literals.quotes @@ -3192,7 +3192,7 @@ The quotes of a plain string, one without interpolation, without an escape seque - By default `keep` - Takes: `single` (`'text'` where double quotes gain nothing); `double` (`"text"` for every plain string, the backslashes of a single-quoted one escaped); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `single`, symfony `single` +- The standards: perCs `keep`, psr12 `keep`, nette `single`, symfony `single`, laravel `single` - Rule `DressCode\Rules\Literals\StringQuotesRule`, stage Structure ## comments @@ -3203,7 +3203,7 @@ An empty comment, which is removed. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Comments\NoEmptyCommentsRule`, stage Finishing - Modifies comments @@ -3213,7 +3213,7 @@ The marker of a single-line comment. - By default `keep` - Takes: `'//'` (`//`, never `#`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'//'`, symfony `'//'` +- The standards: perCs `keep`, psr12 `keep`, nette `'//'`, symfony `'//'`, laravel `'//'` - Rule `DressCode\Rules\Comments\NoHashCommentsRule`, stage Finishing - Modifies comments @@ -3225,7 +3225,7 @@ The doc comment stands above the attributes. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocAboveAttributesRule`, stage Structure - Modifies comments @@ -3235,7 +3235,7 @@ The letter case of a known annotation. - By default `keep` - Takes: `canonical` (as it is known, `@inheritDoc`, `@phpstan-var`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\AnnotationCasingRule`, stage Structure - Modifies comments @@ -3245,7 +3245,7 @@ A `@var` on a class constant that says nothing. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\UselessConstantVarAnnotationRule`, stage Finishing - Modifies comments @@ -3255,7 +3255,7 @@ A second `@param` of one parameter. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.duplicateReturn @@ -3264,7 +3264,7 @@ A second `@return` of a function. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.duplicateVar @@ -3273,7 +3273,7 @@ A second `@var` of a property. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.empty @@ -3282,7 +3282,7 @@ A `@param`, `@return`, `@var` or `@see` without content. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.extraParam @@ -3291,7 +3291,7 @@ A `@param` naming a parameter the function does not have. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure ### phpdoc.annotation.inlineVar @@ -3300,7 +3300,7 @@ An inline `@var` says what `assert($x instanceof Foo)` checks. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\AssertForInlineVarRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3311,7 +3311,7 @@ An array, iterable or traversable declaration has an annotation saying what its - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure - Modifies comments @@ -3321,7 +3321,7 @@ A `@param`, `@return` or `@var` without a description that says only the native - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\PhpDoc\UselessTypeAnnotationsRule`, stage Structure - Modifies comments @@ -3331,7 +3331,7 @@ An annotation in a plain comment, `/* @var */` or `/**@var` without the blank, w - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoPlainPhpdocsRule`, stage Finishing - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3342,7 +3342,7 @@ The blank lines inside a doc comment. - By default `keep` - Takes: `trimmed` (none at the start or the end, at most one in a row inside); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `trimmed`, symfony `trimmed` +- The standards: perCs `keep`, psr12 `keep`, nette `trimmed`, symfony `trimmed`, laravel `trimmed` - Rule `DressCode\Rules\PhpDoc\PhpdocBlankLinesRule`, stage Finishing - Modifies comments @@ -3352,7 +3352,7 @@ An empty doc comment, which is removed. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Comments\NoEmptyCommentsRule`, stage Finishing - Modifies comments @@ -3362,7 +3362,7 @@ The annotations removed from doc comments, written with the `@`, such as `@autho - By default `keep` - Takes: a list of annotations, each with its `@`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `[@access, @author, @copyright, @created, @license, @package, @since, @subpackage, @todo, @version]`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `[@access, @author, @copyright, @created, @license, @package, @since, @subpackage, @todo, @version]`, symfony `keep`, laravel `[@access, @package, @subpackage]` - Rule `DressCode\Rules\PhpDoc\ForbiddenAnnotationsRule`, stage Structure - Modifies comments @@ -3372,7 +3372,7 @@ The patterns of the lines of a description removed from doc comments, such as `~ - By default `keep` - Takes: a list of regular expressions; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `['~^(?:(?!private|protected|static)\S+ )?(?:con|de)structor\.\z~i', '~^\S+ [gs]etter\.\z~i']`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `['~^(?:(?!private|protected|static)\S+ )?(?:con|de)structor\.\z~i', '~^\S+ [gs]etter\.\z~i']`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\ForbiddenPhpdocLinesRule`, stage Structure - Modifies comments @@ -3382,7 +3382,7 @@ A doc comment of `@inheritDoc` alone. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\UselessInheritdocRule`, stage Structure - Modifies comments @@ -3392,7 +3392,7 @@ A property doc comment with a single annotation and no description. - By default `keep` - Takes: `collapsed` (on one line, `/** @var int */`); `expanded` (over three lines, the annotation on the one in the middle); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `collapsed`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `collapsed`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PropertyPhpdocLineSpanRule`, stage Formatting - Modifies comments @@ -3402,7 +3402,7 @@ A plain comment describing a property, `//` or `/* */` where `/** */` belongs, r - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoPlainPhpdocsRule`, stage Finishing - Modifies comments @@ -3412,7 +3412,7 @@ The annotation of a promoted property. - By default `keep` - Takes: `'@var'` (a `@var` at the property, not a `@param` of the constructor); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `'@var'`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `'@var'`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PromotedPropertyAnnotationPositionRule`, stage Structure - Modifies comments @@ -3422,7 +3422,7 @@ The `*` opening each line of a doc comment. - By default `keep` - Takes: `aligned` (each `*` below the first `*` of the opening `/**`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `aligned`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `aligned`, symfony `keep`, laravel `aligned` - Rule `DressCode\Rules\PhpDoc\PhpdocAlignmentRule`, stage Formatting - Modifies comments @@ -3432,7 +3432,7 @@ How an array type of a doc comment is written; `list` is a different type and - By default `keep` - Takes: `'array'` (the generic notation); `'T[]'` (the brackets); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3442,7 +3442,7 @@ How a built-in type in a doc comment is written, `int` and never `integer` or `I - By default `keep` - Takes: `canonical` (the short lowercase name of a built-in type, `never` for `no-return`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `canonical` +- The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `canonical`, laravel `canonical` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3452,7 +3452,7 @@ A type a union of a doc comment names twice, `int|integer`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3462,7 +3462,7 @@ How a type of a doc comment of one type and `null` is written. - By default `keep` - Takes: `'?T'` (a single type with `null` written with `?`); `'T|null'` (a union with `null`, which `phpdoc.types.nullPosition` places); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3472,7 +3472,7 @@ Where `null` stands in a union type of a doc comment. - By default `keep` - Takes: `last` (`int|string|null`); `first` (`null|int|string`); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `last`, symfony `last` +- The standards: perCs `keep`, psr12 `keep`, nette `last`, symfony `last`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3482,7 +3482,7 @@ The order of the types of a union type of a doc comment. - By default `keep` - Takes: `alphabetical` (the types beside `null` sorted by name, case-insensitively); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing - Modifies comments @@ -3494,7 +3494,7 @@ A doc comment followed by another one, the first lost to reflection, which is on - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoConsecutivePhpdocsRule`, stage Finishing ### mistakes.continueInSwitch @@ -3503,7 +3503,7 @@ A `continue` that only leaves a `switch`, where `break` is meant, which is only - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\NoContinueInSwitchRule`, stage Structure ### mistakes.debugOutput @@ -3512,7 +3512,7 @@ A statement calling a debugging function to print, changed into a comment, `// v - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoDebugOutputRule`, stage Structure - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3523,7 +3523,7 @@ The debugging functions whose calls print. - By default `[print_r, var_dump, var_export]` - Takes: a list of names -- The standards: perCs `[print_r, var_dump, var_export]`, psr12 `[print_r, var_dump, var_export]`, nette `[print_r, var_dump, var_export]`, symfony `[print_r, var_dump, var_export]` +- The standards: perCs `[print_r, var_dump, var_export]`, psr12 `[print_r, var_dump, var_export]`, nette `[print_r, var_dump, var_export]`, symfony `[print_r, var_dump, var_export]`, laravel `[print_r, var_dump, var_export]` - Rule `DressCode\Rules\Functions\NoDebugOutputRule`, stage Structure - Modifies comments @@ -3533,7 +3533,7 @@ The `@` operator, which silences errors and is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\NoErrorSuppressionRule`, stage Structure ### mistakes.exceptionWhereThrowableBelongs @@ -3542,7 +3542,7 @@ The `@` operator, which silences errors and is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\ThrowableForExceptionRule`, stage Structure ### mistakes.finalParent @@ -3551,7 +3551,7 @@ A class extending a final one, which is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoFinalParentRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3561,7 +3561,7 @@ The `global` statement, which is only reported. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoGlobalStatementsRule`, stage Structure ### mistakes.invisibleCharacter @@ -3570,7 +3570,7 @@ Non-breaking, zero-width and bidi characters, replaced in comments and escaped i - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Files\NoInvisibleCharactersRule`, stage Structure - Modifies comments @@ -3580,7 +3580,7 @@ The return type `never` on a closure that always throws or exits, though its sig - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NeverForThrowingFunctionRule`, stage Structure - Needs PHP >=8.1 @@ -3590,7 +3590,7 @@ The return type `never` of PHP 8.1 on a function that always throws or exits. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NeverForThrowingFunctionRule`, stage Structure - Needs PHP >=8.1 @@ -3600,7 +3600,7 @@ The return type `never` on a method that always throws or exits, only where no d - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NeverForThrowingFunctionRule`, stage Structure - Needs PHP >=8.1 @@ -3610,7 +3610,7 @@ A property written only in the constructor, or not at all. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ReadonlyForUnwrittenPropertyRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3621,7 +3621,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoRepeatedAssignmentsRule`, stage Structure ### mistakes.staticSetState @@ -3630,7 +3630,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\StaticSetStateRequiredRule`, stage Structure ### mistakes.strictArgument @@ -3639,7 +3639,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\StrictComparisonArgumentRequiredRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3649,7 +3649,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoThisOutsideObjectRule`, stage Structure ### mistakes.trailingWhitespaceInString @@ -3658,7 +3658,7 @@ At the end of the lines of a multi-line string or inline HTML, which changes the - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NoTrailingWhitespaceInStringRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3668,7 +3668,7 @@ An inherited abstract method a class that is not abstract leaves unimplemented, - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoUnimplementedAbstractMethodsRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3678,7 +3678,7 @@ A `catch` after one catching `Throwable` or a class it catches, which is only re - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\NoUnreachableCatchesRule`, stage Structure ### mistakes.__debugInfoReturningNull @@ -3687,7 +3687,7 @@ The `null` that `__debugInfo()` returns, which becomes `[]`. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoNullDebugInfoRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3697,7 +3697,7 @@ The `null` that `__debugInfo()` returns, which becomes `[]`. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\SerializeMethodsRequiredRule`, stage Structure ## upgrading @@ -3708,7 +3708,7 @@ An attribute written for an annotation (`@ORM\Entity: ORM\Entity`). - By default `keep` - Takes: a map of names and patterns with `*` to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\AttributeForAnnotationRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on @@ -3720,7 +3720,7 @@ An attribute written for a member the class declares by convention. - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\AttributeForMemberRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on - Modifies comments @@ -3731,7 +3731,7 @@ A class that may not be used, with what to do instead (`Acme\Legacy\Db: "use the - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on @@ -3741,7 +3741,7 @@ A global constant that may not be used, with what to do instead (`E_STRICT: "it - By default `keep` - Takes: a map of names and patterns with `*` to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure ### upgrading.api.forbiddenFunctions @@ -3750,7 +3750,7 @@ A function that may not be called, with what to do instead. - By default `keep` - Takes: a map of names and patterns with `*` to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedFunctionsRule`, which this decision does not turn on @@ -3760,7 +3760,7 @@ A constant, a method or a property that may not be used, with what to do instead - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenMembersRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, which this decision does not turn on @@ -3773,7 +3773,7 @@ A call written as the template says (`Acme\Mail\Mailer::send($to, $body): send(n - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedFunctionCallsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, which this decision does not turn on @@ -3786,7 +3786,7 @@ A class written instead of another one, both fully qualified (`Acme\Old\Mailer: - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, which this decision does not turn on - Modifies comments @@ -3797,7 +3797,7 @@ A function written instead of another one (`acme_send: Acme\Mail\send`). - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ReplacedFunctionsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedFunctionsRule`, which this decision does not turn on - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3808,7 +3808,7 @@ A constant, a method or a property written instead of another one of the class ( - By default `keep` - Takes: a map of names to an entry of the grammar of the map, an entry withdrawn with `keep`; `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedMembersRule`, which this decision does not turn on @@ -3821,7 +3821,7 @@ A constant, a method or a property written instead of another one of the class ( - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\OverrideAttributeRequiredRule`, stage Structure - Needs PHP >=8.3 - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` @@ -3832,7 +3832,7 @@ A constant, a method or a property written instead of another one of the class ( - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\SensitiveParameterRequiredRule`, stage Structure - Needs PHP >=8.2 @@ -3842,7 +3842,7 @@ The names of the parameters holding a secret, whatever their letter case and und - By default `[password, passphrase, secret, apiKey, privateKey]` - Takes: a list of names -- The standards: perCs `[password, passphrase, secret, apiKey, privateKey]`, psr12 `[password, passphrase, secret, apiKey, privateKey]`, nette `[password, passphrase, secret, apiKey, privateKey]`, symfony `[password, passphrase, secret, apiKey, privateKey]` +- The standards: perCs `[password, passphrase, secret, apiKey, privateKey]`, psr12 `[password, passphrase, secret, apiKey, privateKey]`, nette `[password, passphrase, secret, apiKey, privateKey]`, symfony `[password, passphrase, secret, apiKey, privateKey]`, laravel `[password, passphrase, secret, apiKey, privateKey]` - Rule `DressCode\Rules\Functions\SensitiveParameterRequiredRule`, stage Structure - Needs PHP >=8.2 @@ -3852,7 +3852,7 @@ A class with `__toString()` declares it. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\StringableRequiredRule`, stage Structure - Needs PHP >=8.0 @@ -3862,7 +3862,7 @@ A class whose declaration is `@deprecated`, replaced by the class the deprecatio - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedClassesRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - Modifies comments @@ -3874,7 +3874,7 @@ A function whose declaration is `@deprecated`, replaced by the function the depr - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedFunctionsRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3885,7 +3885,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedMembersRule`, stage Structure - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3896,7 +3896,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\ArrayFirstLastNotationRule`, stage Structure - Needs PHP >=8.5 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3907,7 +3907,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\ArraySearchForFilterRule`, stage Structure - Rule `DressCode\Rules\ControlFlow\ArraySearchForForeachRule`, stage Structure - Needs PHP >=8.4 @@ -3919,7 +3919,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\NoManualListTestsRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3930,7 +3930,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\ClampForMinMaxRule`, stage Structure - Needs PHP >=8.6 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3941,7 +3941,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\GetDebugTypeForTernaryRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3952,7 +3952,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\JsonValidateForDecodeRule`, stage Structure - Needs PHP >=8.3 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3963,7 +3963,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\RoundingModeNotationRule`, stage Structure - Needs PHP >=8.4 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3974,7 +3974,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoManualSubstringTestsRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3985,7 +3985,7 @@ Whether the code follows the upgrading files of the installed packages, their ma - By default `no` - Takes: `yes`; `no` -- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no` +- The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no`, laravel `no` - Rule `DressCode\Rules\Upgrading\ReplacedClassesRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedFunctionsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, stage Structure @@ -4003,7 +4003,7 @@ The backtick operator PHP 8.5 deprecated, written as a `shell_exec()` call. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Expressions\NoBacktickOperatorsRule`, stage Structure ### upgrading.php.caseSemicolon @@ -4012,7 +4012,7 @@ The backtick operator PHP 8.5 deprecated, written as a `shell_exec()` call. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\SwitchCaseNotationRule`, stage Structure ### upgrading.php.constructorReturnValue @@ -4021,7 +4021,7 @@ A `return` with a value in a constructor or destructor, deprecated by PHP 8.6. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoConstructorReturnValuesRule`, stage Structure ### upgrading.php.csvEscapeArgument @@ -4030,7 +4030,7 @@ The `escape` argument of `fgetcsv()` and kin written out, since PHP 8.4 deprecat - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\CsvEscapeArgumentRequiredRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4040,7 +4040,7 @@ A function, a constant or a member PHP deprecated, written as what replaces it a - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ReplacedMembersRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedCallsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ReplacedFunctionCallsRule`, stage Structure @@ -4054,7 +4054,7 @@ An argument whose value PHP deprecated is reported, `E_USER_ERROR` given to `tri - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\NoDeprecatedPhpArgumentsRule`, stage Structure ### upgrading.php.dollarBraceInterpolation @@ -4063,7 +4063,7 @@ An argument whose value PHP deprecated is reported, `E_USER_ERROR` given to `tri - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Literals\NoDollarBraceInterpolationsRule`, stage Structure ### upgrading.php.implicitNullable @@ -4072,7 +4072,7 @@ A parameter type made nullable by its default `null` alone, `int $x = null`, dep - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden` +- The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Types\NullableTypeForDefaultNullRule`, stage Structure ### upgrading.php.nullArrayKey @@ -4081,7 +4081,7 @@ A parameter type made nullable by its default `null` alone, `int $x = null`, dep - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\NoNullArrayKeysRule`, stage Structure - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4091,7 +4091,7 @@ Names PHP 8.6 reserved, in declarations and imports. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Namespaces\NoReservedNamesRule`, stage Structure ### upgrading.php.returnInFinally @@ -4100,7 +4100,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\NoReturnsInFinallyRule`, stage Structure ### upgrading.php.sleepAndWakeup @@ -4109,7 +4109,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `forbidden` (never there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoSleepAndWakeupRule`, stage Structure ### upgrading.phpdoc.deprecated @@ -4118,7 +4118,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\DeprecatedAttributeForAnnotationRule`, stage Structure - Needs PHP >=8.4 - Modifies comments @@ -4130,7 +4130,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ReadonlyForAnnotationRule`, stage Structure - Needs PHP >=8.1 - Modifies comments @@ -4142,7 +4142,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\CloneWithNotationRule`, stage Structure - Needs PHP >=8.5 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4153,7 +4153,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ClassConstantForConstantCallRule`, stage Structure - Needs PHP >=8.3 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4164,7 +4164,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\CallableNotationRule`, stage Structure - Rule `DressCode\Rules\Functions\FirstClassCallableForStringRule`, stage Structure - Needs PHP >=8.1 @@ -4176,7 +4176,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MatchForSwitchRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4187,7 +4187,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NewInitializerForNullDefaultRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4198,7 +4198,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\NullsafeForGuardedAccessRule`, stage Structure - Needs PHP >=8.0 @@ -4208,7 +4208,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\OctalNotationRule`, stage Structure - Needs PHP >=8.1 @@ -4218,7 +4218,7 @@ Calls nested one in another, each handing its result to the next, written with t - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\PipeForNestedCallsRule`, stage Structure - Needs PHP >=8.5 @@ -4228,7 +4228,7 @@ The calls a nest has at least for the pipe operator to be written. - By default `3` - Takes: a count from 2 -- The standards: perCs `3`, psr12 `3`, nette `3`, symfony `3` +- The standards: perCs `3`, psr12 `3`, nette `3`, symfony `3`, laravel `3` - Rule `DressCode\Rules\Expressions\PipeForNestedCallsRule`, stage Structure - Needs PHP >=8.5 @@ -4238,7 +4238,7 @@ A constructor parameter only assigned to a property. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\PromotedPropertyForAssignmentRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4249,7 +4249,7 @@ A class whose every property is readonly. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ReadonlyClassForReadonlyPropertiesRule`, stage Structure - Needs PHP >=8.2 @@ -4259,7 +4259,7 @@ A class whose every property is readonly. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\SpreadForArrayMergeRule`, stage Structure - Needs PHP >=8.1 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4270,7 +4270,7 @@ A class whose every property is readonly. - By default `keep` - Takes: `required` (always there); `keep` -- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep` +- The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\ThrowExpressionForNullGuardRule`, stage Structure - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4283,7 +4283,7 @@ The constants the namespaces of the project declare, each of them listed once th - A fact of the project, which its rule guards - Takes: a list of names -- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]` +- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]`, laravel `[]` - Rule `DressCode\Rules\Namespaces\NoUnlistedNamespacedDeclarationsRule`, stage Structure ### namespaces.functions @@ -4292,5 +4292,5 @@ The functions the namespaces of the project declare, each of them listed once th - A fact of the project, which its rule guards - Takes: a list of names -- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]` +- The standards: perCs `[]`, psr12 `[]`, nette `[]`, symfony `[]`, laravel `[]` - Rule `DressCode\Rules\Namespaces\NoUnlistedNamespacedDeclarationsRule`, stage Structure diff --git a/docs/reference/generate.php b/docs/reference/generate.php index 0002d3ec2..797cbd041 100644 --- a/docs/reference/generate.php +++ b/docs/reference/generate.php @@ -6,7 +6,7 @@ */ use DressCode\{Config, DecisionKind, RuleInfo}; -use DressCode\Config\{Catalogue, ConfigResolver, NeonReader, PluginRegistry}; +use DressCode\Config\{Catalogue, ConfigResolver, CorePlugin, NeonReader, PluginRegistry}; use DressCode\Testing\RuleTester; require __DIR__ . '/../../vendor/autoload.php'; @@ -66,7 +66,7 @@ function formatValue(DressCode\Value $value): string $catalogue = $resolver->getCatalogue(); $riskyDecisions = findRiskyDecisions(); $standards = []; -foreach (['perCs', 'psr12', 'nette', 'symfony'] as $standard) { +foreach (CorePlugin::Standards as $standard) { $standards[$standard] = $resolver->resolve(new Config(use: ["dresscode/$standard"]), Config::DefaultPhpVersion)->decisions; } @@ -187,7 +187,7 @@ function formatValue(DressCode\Value $value): string $presets = $registry->presets; $out = "# Presets\n\nGenerated by `composer reference` from the preset files; do not edit. " . count($presets) . " presets.\n\n" - . "A project takes one of the standards (`dresscode/perCs`, `dresscode/psr12`, `dresscode/nette`, `dresscode/symfony`)\n" + . 'A project takes one of the standards (`dresscode/' . implode('`, `dresscode/', CorePlugin::Standards) . "`)\n" . "as its base, and the description of every other preset says what it adds to it.\n\n" . "| Preset | Builds on | |\n|---|---|---|\n"; foreach ($presets as $name => $file) { diff --git a/docs/reference/presets.md b/docs/reference/presets.md index 93338153d..ddb79f56b 100644 --- a/docs/reference/presets.md +++ b/docs/reference/presets.md @@ -1,8 +1,8 @@ # Presets -Generated by `composer reference` from the preset files; do not edit. 10 presets. +Generated by `composer reference` from the preset files; do not edit. 11 presets. -A project takes one of the standards (`dresscode/perCs`, `dresscode/psr12`, `dresscode/nette`, `dresscode/symfony`) +A project takes one of the standards (`dresscode/perCs`, `dresscode/psr12`, `dresscode/nette`, `dresscode/symfony`, `dresscode/laravel`) as its base, and the description of every other preset says what it adds to it. | Preset | Builds on | | @@ -11,6 +11,7 @@ as its base, and the description of every other preset says what it adds to it. | `dresscode/psr12` | | PSR-12 Extended Coding Style | | `dresscode/nette` | `dresscode/perCs`, `dresscode/cleanup`, `dresscode/types`, `dresscode/correctness` | Nette Coding Standard | | `dresscode/symfony` | `dresscode/perCs` | Symfony Coding Standards | +| `dresscode/laravel` | `dresscode/perCs` | Laravel | | `dresscode/cleanup` | | Cleanup | | `dresscode/compilerOptimizations` | | Compiler optimizations | | `dresscode/correctness` | | Correctness | diff --git a/src/Config/ConfigResolver.php b/src/Config/ConfigResolver.php index 3cc5933c4..10e6513d4 100644 --- a/src/Config/ConfigResolver.php +++ b/src/Config/ConfigResolver.php @@ -246,7 +246,7 @@ public function resolve( /** - * The decisions each of the four standards makes, resolved alone. + * The decisions each of the standards makes, resolved alone. * @return array> standard => path => its decision * @throws ConfigurationException */ diff --git a/src/Config/CorePlugin.php b/src/Config/CorePlugin.php index dba3cbd90..1ef1d08b0 100644 --- a/src/Config/CorePlugin.php +++ b/src/Config/CorePlugin.php @@ -26,7 +26,7 @@ final class CorePlugin implements Plugin /** * The presets that are complete standards; the other presets are sets. */ - public const Standards = ['perCs', 'psr12', 'nette', 'symfony']; + public const Standards = ['perCs', 'psr12', 'nette', 'symfony', 'laravel']; private const QualifiedName = '\\\\?\w+(\\\\\w+)*'; @@ -40,6 +40,7 @@ public function getManifest(): PluginManifest 'dresscode/psr12' => dirname(__DIR__) . '/Presets/psr12.neon', 'dresscode/nette' => dirname(__DIR__) . '/Presets/nette.neon', 'dresscode/symfony' => dirname(__DIR__) . '/Presets/symfony.neon', + 'dresscode/laravel' => dirname(__DIR__) . '/Presets/laravel.neon', 'dresscode/cleanup' => dirname(__DIR__) . '/Presets/cleanup.neon', 'dresscode/compilerOptimizations' => dirname(__DIR__) . '/Presets/compilerOptimizations.neon', 'dresscode/correctness' => dirname(__DIR__) . '/Presets/correctness.neon', diff --git a/src/Presets/laravel.neon b/src/Presets/laravel.neon new file mode 100644 index 000000000..635866854 --- /dev/null +++ b/src/Presets/laravel.neon @@ -0,0 +1,214 @@ +# Laravel +# +# The style of Laravel as the preset laravel of Laravel Pint defines it: PER Coding Style with the concatenation, the +# negation and the instantiation Laravel writes, the arrow function spaced, the imports qualified and sorted, a blank +# line before `return` and `continue`, the phpDoc trimmed, and without the rules that break a construct over lines, +# which Pint leaves to the author. + +use: perCs + +file: + lineLength: none + longLines: keep + strictTypes: + position: keep + +indentation: + heredoc: keep + binaryOperator: keep + ternary: keep + +naming: + class: keep + method: keep + constant: keep + enumCase: keep + +casing: + magicConstant: uppercase + hexDigit: uppercase + function: declared # `strlen()`, not `StrLen()` + +qualification: + staticInFinalClass: required # `self::` in a final class + class: + otherNamespace: imported + currentNamespace: bare + phpdoc: imported # `@param Request $request`, not `@param \Illuminate\Http\Request $request` + +imports: + grouping: + class: separated + function: separated + constant: separated + order: + kinds: [constant, class, function] + names: alphabetical + unused: forbidden + aliasEqualToName: forbidden + +spacing: + anonymousClass: 'new class($a)' + comment: + marker: // text + concatenation: $a.$b + alignment: + binaryOperator: forbidden + comma: optional + objectOperator: '$a->b()' + doubleColon: 'Foo::bar()' + logicalNot: ! $x + reference: keep + spread: keep + namedArgument: keep + attribute: keep + alternativeSyntax: keep + body: keep + import: keep + arrowFunction: 'fn ($x) => $x' + padding: + array: forbidden + list: forbidden + offset: forbidden + semicolon: + after: 'for ($i = 0; $i < 5; $i++)' + +# method_argument_space ignores what spreads over lines, and Pint has nothing that would spread a construct +multiline: + items: + array: keep + list: keep + implements: keep + condition: keep + for: keep + logical: keep + concatenation: keep + arithmetic: keep + bitwise: keep + chain: keep + matchCondition: keep + ternary: keep + argument: keep + parameter: keep + edges: + array: keep + list: keep + implements: keep + condition: keep + chain: keep + argument: keep + parameter: keep + trailingComma: + argument: optional # arrays only + parameter: optional + matchArm: keep + closureUse: keep + operatorPosition: + condition: keep + comparison: keep + logical: keep + concatenation: keep + arithmetic: keep + bitwise: keep + coalesce: keep + pipe: keep + ternary: keep + attributePosition: + declaration: keep + parameter: keep + semicolon: sameLine + +braces: + alternativeSyntax: forbidden + bareStatementGroup: forbidden + position: + anonymousClass: nextLine + emptyBody: + propertyHook: keep + +blankLines: + bodyStart: + controlStructure: keep + bodyEnd: + function: keep + closure: keep + controlStructure: keep + branch: keep + class: keep + anonymousClass: keep + aroundMethods: 1 + aroundInterfaceMethods: 1 + beforeOpenBrace: + class: keep + afterTraitUses: keep + betweenMemberKinds: 1 + betweenMembers: # enum cases together + constant: 1 + property: 1 + beforeDocumentedMember: 1 + afterDeclare: keep + afterAttributes: keep + beforeStatement: + continue: '1+' + return: '1+' + +classes: + members: + order: keep + grouping: + traitUse: keep + protectedInEnum: keep + visibility: + constant: keep + emptyParentheses: + instantiation: forbidden # `new Foo` + attribute: keep + instantiationWrapping: keep + +types: + nullPosition: keep + +functions: + trailingBareReturn: forbidden + arrowFunction: + emptyBody: keep + unreachableParameterDefault: forbidden + +hygiene: + aliasFunctions: [core, imap, pg] + +controlFlow: + switch: + fallThroughComment: keep + +expressions: + parenthesesAfterConstruct: forbidden # `return $x;`, not `return ($x);` + assignment: + increment: postfix + doubleNegation: forbidden + comparison: + notEquals: '!=' + yoda: forbidden + +literals: + arraySyntax: + list: short + quotes: single + +comments: + singleline: // + +phpdoc: + empty: forbidden + forbiddenAnnotations: [@access, @package, @subpackage] + aboveAttributes: keep + stars: aligned + blankLines: trimmed + types: + builtin: canonical + annotation: + nativeTypeOnly: forbidden + +upgrading: + php: + implicitNullable: forbidden diff --git a/tests/DressCode/Config/PluginRegistry.phpt b/tests/DressCode/Config/PluginRegistry.phpt index 377c408ff..507310837 100644 --- a/tests/DressCode/Config/PluginRegistry.phpt +++ b/tests/DressCode/Config/PluginRegistry.phpt @@ -192,7 +192,7 @@ test('presets', function () { Assert::same('dresscode/perCs', $registry->resolvePreset('perCs')); Assert::same('test/preset', $registry->resolvePreset('test/preset')); Assert::same([ - 'dresscode/perCs', 'dresscode/psr12', 'dresscode/nette', 'dresscode/symfony', 'dresscode/cleanup', + 'dresscode/perCs', 'dresscode/psr12', 'dresscode/nette', 'dresscode/symfony', 'dresscode/laravel', 'dresscode/cleanup', 'dresscode/compilerOptimizations', 'dresscode/correctness', 'dresscode/deprecations', 'dresscode/modernizations', 'dresscode/types', 'test/preset', ], array_keys($registry->presets)); diff --git a/tests/DressCode/Console/explain.phpt b/tests/DressCode/Console/explain.phpt index d739a9140..0ef7cf26b 100644 --- a/tests/DressCode/Console/explain.phpt +++ b/tests/DressCode/Console/explain.phpt @@ -111,7 +111,7 @@ test('explain of a decision writes what it is, its value here and those of the s Assert::contains('`spacing.call`', $text); Assert::contains("Here `'foo(\$a, \$b)'`, set by dresscode/psr12.", $text); Assert::contains( - "The standards: perCs `'foo(\$a, \$b)'`, psr12 `'foo(\$a, \$b)'`, nette `'foo(\$a, \$b)'`, symfony `'foo(\$a, \$b)'`.", + "The standards: perCs `'foo(\$a, \$b)'`, psr12 `'foo(\$a, \$b)'`, nette `'foo(\$a, \$b)'`, symfony `'foo(\$a, \$b)'`, laravel `'foo(\$a, \$b)'`.", $text, ); Assert::contains('See https://dresscode.run/decisions/spacing.call', $text); diff --git a/tests/DressCode/Presets/sets.phpt b/tests/DressCode/Presets/sets.phpt index b182ccdeb..6c23c21c8 100644 --- a/tests/DressCode/Presets/sets.phpt +++ b/tests/DressCode/Presets/sets.phpt @@ -6,13 +6,13 @@ */ use DressCode\Config; -use DressCode\Config\{ConfigResolver, PluginRegistry}; +use DressCode\Config\{ConfigResolver, CorePlugin, PluginRegistry}; use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; -const Standards = ['dresscode/perCs', 'dresscode/psr12', 'dresscode/nette', 'dresscode/symfony']; +define('Standards', array_map(fn(string $standard) => "dresscode/$standard", CorePlugin::Standards)); $registry = new PluginRegistry; $resolver = new ConfigResolver($registry); @@ -48,7 +48,6 @@ test('a decision is made by a set or a standard, or written by the project on pu 'controlFlow.trailingIf', // the shape of a function body, which no standard prescribes 'controlFlow.elseifAfterReturn', // an `elseif` split into an `if` reads as another question, which a project chooses 'controlFlow.ifElseReturning', // a ternary for two branches is a shape of the code, no construct a newer PHP gave - 'casing.hexDigit', // the case of a hexadecimal digit, which no standard of the core settles 'naming.exceptionAffix', // how an exception is named, which projects settle each their own way 'mistakes.neverReturnType.anonymousFunction', // the signature of a closure usually belongs to the call it is passed to 'mistakes.readonlyProperty', // a guarantee the code takes on, which no older construct gave @@ -62,7 +61,7 @@ test('a decision is made by a set or a standard, or written by the project on pu // the `use` of a closure and a group of attributes, which no standard spreads 'multiline.items.closureUse', 'multiline.edges.closureUse', 'multiline.items.attribute', 'multiline.edges.attribute', 'phpdoc.types.nullable', 'phpdoc.types.unionOrder', 'types.unionOrder', // an order or a notation a project chooses - 'qualification.constant.otherNamespace', 'qualification.staticInFinalClass', 'qualification.function.otherNamespace', // how far a name is written out is the project's + 'qualification.constant.otherNamespace', 'qualification.function.otherNamespace', // how far a name is written out is the project's 'qualification.function.currentNamespace', 'qualification.constant.currentNamespace', 'types.declaration.constant', // a typed constant, which the code before PHP 8.3 cannot have 'upgrading.classes.Override', // a guarantee the code takes on, which no older construct gave From f156cc5151b09d3a2481490b22441e34e4d58f6a Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 13 Oct 2026 07:42:50 +0200 Subject: [PATCH 04/13] cli: the init command dresscode init measures the code of a project and writes the configuration that fits it, each decision with its description. --- docs/internals.md | 4 +- src/Config/ComposerJson.php | 46 +- src/Config/CorePlugin.php | 3 +- src/Config/Loader.php | 16 +- src/Console/Application.php | 1 + src/Console/Commands/InitCommand.php | 75 +++ src/Console/InitPrinter.php | 90 ++++ src/Measuring/FileSample.php | 81 +++ src/Measuring/Measurement.php | 109 ++++ src/Measuring/Proposal.php | 604 +++++++++++++++++++++++ src/Measuring/Survey.php | 152 ++++++ src/Reporters/ConsoleReporter.php | 2 +- tests/DressCode/Config/Loader.phpt | 2 +- tests/DressCode/Console/Application.phpt | 2 +- tests/DressCode/Console/init.phpt | 461 +++++++++++++++++ tests/DressCode/Measuring/Survey.phpt | 90 ++++ tests/DressCode/Reporters/reporters.phpt | 4 +- 17 files changed, 1731 insertions(+), 11 deletions(-) create mode 100644 src/Console/Commands/InitCommand.php create mode 100644 src/Console/InitPrinter.php create mode 100644 src/Measuring/FileSample.php create mode 100644 src/Measuring/Measurement.php create mode 100644 src/Measuring/Proposal.php create mode 100644 src/Measuring/Survey.php create mode 100644 tests/DressCode/Console/init.phpt create mode 100644 tests/DressCode/Measuring/Survey.phpt diff --git a/docs/internals.md b/docs/internals.md index 5c456c1a9..ea8d22913 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -25,7 +25,7 @@ Public: Internal: -- `Engine\*`, `Config\*`, `Console\*`: the engine, the configuration and the console +- `Engine\*`, `Config\*`, `Measuring\*`, `Console\*`: the engine, the configuration, the measuring of `init` and the console - `Reporter`, `Reporters\*`: the formats of the output, whose contract is the output itself, and the interface they implement next to them; they print the `Engine\RunResult` of a run - `Rules\NodeHelpers`, `Rules\ListLayout`, `Rules\Compiler`, `Rules\NativeType`, `Rules\Classes\PropertyWrites`, `Rules\Classes\MemberModifiers`, `Rules\Functions\ForwardingClosure`, `Rules\Namespaces\NameReferences`, `Rules\Namespaces\QualificationPolicy`, `Rules\PhpDoc\AnnotationReplacement`, `Rules\PhpDoc\PhpdocTypeNotationVisitor`, `Rules\Whitespace\BlankLineClaims`, `Rules\Whitespace\Padding`: what the rules share among themselves or a rule does in a class of its own - `Rules\Upgrading\AnnotationArguments`, `Rules\Upgrading\CallableLiteral`, `Rules\Upgrading\CallTemplate`, `Rules\Upgrading\ClassReplacement`, `Rules\Upgrading\MagicCall`, `Rules\Upgrading\MemberTarget`, `Rules\Upgrading\AttributeTarget`, `Rules\Upgrading\Rewrite`, `Rules\Upgrading\AttributeForMemberEntry`, `Rules\Upgrading\AnnotationMap`, `Rules\Upgrading\enums.php`: the helpers of the rules fed by maps that are not the grammar of the maps @@ -245,7 +245,7 @@ What the PHP notation passes as an object or a callable the NEON writes as an en A rule declares what it decides in `Rule::getDecisions()`, and the decisions of every registered rule, with those of a tree its rules name, are the catalogue (`Config\Catalogue`), which is what a file is read against, what `explain` and the reference print and what an editor of the configuration loads. A `Decision` has a path, which is the identity of everything a user ever sees about it, the report and the suppression included, a `Domain` of the values it takes, a description and notes saying what it is not about; it is a requirement, which turns its rule on wherever it is not `keep`, a parameter, which only refines a requirement, turns nothing on and has a default instead of `keep` (`controlFlow.trailingIfMinStatements`, `naming.except`; the flag `upgrading.packages` turns nothing on either, but what it lets in is a layer of requirements, the maps of `upgrading.api` and of plugins and the requirements of the rules of the plugin of a package its upgrading files turn on, which then take effect with nothing of the project), or a fact, a key of the environment the rule guards (below). A rule runs where one of its requirements takes effect, so a rule is never named to be turned on: a project or a preset writes the decisions it wants, and a decision nobody writes requires nothing. A decision of one rule is declared by that rule; one several rules share, or one the engine reads, is declared by a tree, the decisions of the manifest of the plugin bringing the rules (`PluginManifest::$decisions`, the core being `Config\CorePlugin`), and each rule taking it names its path in `RuleInfo::$decisions`, a decision of the core or of its own plugin, never the rule that would otherwise own it, so that a decision over constructs with nothing in common to share is several rules and not one rule of several parts, and its words are written once. The other way round, rules that would repeat the same mechanism over different constructs (a table of claims, a call written another way) are one rule with a decision for each construct, every part reporting under its own and doing nothing where it is `keep`; a part that comes to need logic of its own is split off again. It turns on every rule naming it that can run, and it takes effect while one of them can; a rule that only reads a decision of a tree, to leave to it what it governs, names it in `RuleInfo::$reads`, which puts it in the catalogue and in the values of the run without turning the rule on (`Catalogue::getReadersOf()`, the `readers` of `toArray()`); where two decisions reach one occurrence, the rule of the broader one leaves it to the narrower one whatever its value, `keep` included, knowing what to leave from what the other rule makes public (`StrictComparisonArgumentRequiredRule::StrictArguments`, `NoUnreachableCatchesRule::isUnreachable()`) and never from the values of the run, and where two rules could both take it the order of their registration decides; a decision of a tree no registered rule names is not in the catalogue, and a rule declaring a path a tree declares, or two rules declaring one path, is a `ConfigurationException`. Where no catalogue is at hand, a test running one rule, `Catalogue::collectDecisions()` gives the decisions of the rule and `Catalogue::fromRules()` the catalogue of a few rules, the trees taken from the core and from the plugin the package of each rule names in `extra.dresscode.plugin` (`PackageDiscovery::findPluginOf()`), so that a plugin tests its rules as they run. -The domains are the kinds of value a key takes, each normalizing what a layer wrote into a `Value` and merging two of them by its law: `Words` (`forbidden`, `nextLine`, a tolerance of several where the domain takes one, the first of them the one written: `[perLine, fill]`), `Shapes`, the shapes of a construct each written by its example (`spacing.call: 'foo($a, $b)'`), which shows the whitespace its decision governs and every other whitespace in it as all the standards write it, so that it is read as that one space and nothing else, a shape no such example shows alone being a word, a tolerance of several where the decision declares one, every example in it passing and the first written where none matches (`spacing.logicalNot: ['!$x', '! $x']`), which the tool writes in single quotes and a configuration has to quote, NEON reading `foo($a, $b)` as an entity, the example only ever compared with the examples of the domain and never parsed, and the word naming the shape the rules read never written by a layer, `Count` (`2`, `1-2`, `1+`, a word where the decision names one, `file.lineLength: none`), `Flag`, `Names` (a list of names, regular expressions or an order, an empty set having no item and an empty order or a set a requirement acts on being refused, as is `keep` among the items), `Text` (a text of the project's own the rule writes as it is given), `GrammarEntry` (a value a grammar of its own reads, an entry of a map of the upgrading data) and `Map` (names to values of another domain, the maps of the upgrading data among them). The eight are the whole set: a plugin declares its decisions with them, never with a domain of its own, so `Domain::normalize()` and the constructor of `Value` are `@internal`. `keep` is the one word for "leave this alone" and every requirement takes it, a map entry by entry (`name: keep`) and as a whole, which withdraws what the layers below said, the data of the packages included, an entry a later layer writes standing alone, so `null`, `false` and a turned-off rule never stand side by side for the one idea. `Value` keeps the layer that said it, and a rule reads the values of its decisions from `Values` in `configure()`, never a raw array. A value is written one way in every output, by `Value::toWrittenText()` (`config`, `explain`, the reference, the catalogue): a word bare, an example in single quotes, `1-2` and `1+`, `yes` and `no`; `Value::toData()`, the normalized content, stays the identity of the value the result cache keys on, so a change of the writing invalidates nothing. A map whose entries a grammar reads as a whole, a map of the upgrading data, is a `Map` carrying that grammar (`Map::$grammar`, a schema of `nette/schema` over the merged entries), and its rule reads it with `Values::readMap()`, which hands over what the grammar makes of the entries no layer withdrew, while the grammar takes `keep` still, for a section of an upgrading file read alone; a wrong entry is an error of the configuration naming the path. +The domains are the kinds of value a key takes, each normalizing what a layer wrote into a `Value` and merging two of them by its law: `Words` (`forbidden`, `nextLine`, a tolerance of several where the domain takes one, the first of them the one written: `[perLine, fill]`), `Shapes`, the shapes of a construct each written by its example (`spacing.call: 'foo($a, $b)'`), which shows the whitespace its decision governs and every other whitespace in it as all the standards write it, so that it is read as that one space and nothing else, a shape no such example shows alone being a word, a tolerance of several where the decision declares one, every example in it passing and the first written where none matches (`spacing.logicalNot: ['!$x', '! $x']`), which the tool writes in single quotes and a configuration has to quote, NEON reading `foo($a, $b)` as an entity, the example only ever compared with the examples of the domain and never parsed, and the word naming the shape the rules read never written by a layer, `Count` (`2`, `1-2`, `1+`, a word where the decision names one, `file.lineLength: none`), `Flag`, `Names` (a list of names, regular expressions or an order, an empty set having no item and an empty order or a set a requirement acts on being refused, as is `keep` among the items), `Text` (a text of the project's own the rule writes as it is given), `GrammarEntry` (a value a grammar of its own reads, an entry of a map of the upgrading data) and `Map` (names to values of another domain, the maps of the upgrading data among them). The eight are the whole set: a plugin declares its decisions with them, never with a domain of its own, so `Domain::normalize()` and the constructor of `Value` are `@internal`. `keep` is the one word for "leave this alone" and every requirement takes it, a map entry by entry (`name: keep`) and as a whole, which withdraws what the layers below said, the data of the packages included, an entry a later layer writes standing alone, so `null`, `false` and a turned-off rule never stand side by side for the one idea. `Value` keeps the layer that said it, and a rule reads the values of its decisions from `Values` in `configure()`, never a raw array. A value is written one way in every output, by `Value::toWrittenText()` (`config`, `explain`, the reference, `init`, the catalogue): a word bare, an example in single quotes, `1-2` and `1+`, `yes` and `no`; `Value::toData()`, the normalized content, stays the identity of the value the result cache keys on, so a change of the writing invalidates nothing. A map whose entries a grammar reads as a whole, a map of the upgrading data, is a `Map` carrying that grammar (`Map::$grammar`, a schema of `nette/schema` over the merged entries), and its rule reads it with `Values::readMap()`, which hands over what the grammar makes of the entries no layer withdrew, while the grammar takes `keep` still, for a section of an upgrading file read alone; a wrong entry is an error of the configuration naming the path. The core keeps to its sections, a plugin to the one its manifest names after it (`PluginManifest::$section`), and the rules of the project to `project`; the catalogue refuses a path outside them, a path two rules declare and a section of a plugin that is a reserved key or a section of the core. A rule of a plugin names a tree of the core, of its own plugin and of the plugins it builds on (`PluginManifest::$plugins`, transitively), a rule of the core one of the core alone, so that a decision is shared across plugins only where one depends on the other. A rule of the project names a tree of the core or of any plugin the project loads, the project standing on all of them. The order of the registration is the order the rules of a stage run in, the core in the fixed list of `Config\CorePlugin`, the plugins after it in the order `use` names them, each after the plugins it builds on, the rules of the project last, so that no order of the keys of a file, which an editor may rewrite, changes what a run does. A decision of the core has its page on dresscode.run by its path, and a report in a terminal links to it. diff --git a/src/Config/ComposerJson.php b/src/Config/ComposerJson.php index 473de750b..f3db7676f 100644 --- a/src/Config/ComposerJson.php +++ b/src/Config/ComposerJson.php @@ -8,11 +8,13 @@ namespace DressCode\Config; use DressCode\Engine\Helpers; -use function is_array, is_string; +use Nette\Utils\FileSystem; +use function is_array, is_string, strlen; /** - * What the files of Composer say about a project: where its composer.json is and the PHP it is written for. + * What the files of Composer say about a project: where its composer.json is, the PHP it is written for and where + * its code lies. * @internal */ final class ComposerJson @@ -62,4 +64,44 @@ public static function detectPhpTarget(?string $composerFile): ?string return $constraint !== null && Versions::findLowestVersion($constraint) !== null ? $constraint : null; } + + + /** + * The directories autoload and autoload-dev name, in the order of the file, relative to the root and with + * their dot segments resolved: the only place where a project itself says where its code is. A `files` + * entry is a single file, not a scope, and a classmap may name one too, so only what is a directory + * counts; a directory outside the root belongs to another project, since the file may be the one of a + * directory above. + * @return list + */ + public static function detectAutoloadPaths(?string $composerFile, string $root): array + { + $data = self::read($composerFile); + if ($data === null) { + return []; + } + + $base = Helpers::canonicalizePath(dirname((string) $composerFile)); + $root = Helpers::canonicalizePath($root); + $paths = []; + foreach (['autoload', 'autoload-dev'] as $section) { + foreach (['psr-4', 'psr-0', 'classmap'] as $kind) { + foreach ((array) ($data[$section][$kind] ?? []) as $value) { + foreach ((array) $value as $path) { // a psr-4 prefix takes one path or several + // `./src` and `src` are the same path, and only resolved do they compare as one + $directory = is_string($path) ? Helpers::canonicalizePath(FileSystem::normalizePath("$base/$path")) : null; + if ($directory === null || !is_dir($directory)) { + continue; + } elseif ($directory === $root) { + $paths['.'] = true; + } elseif (str_starts_with($directory, "$root/")) { + $paths[substr($directory, strlen($root) + 1)] = true; + } + } + } + } + } + + return array_keys($paths); + } } diff --git a/src/Config/CorePlugin.php b/src/Config/CorePlugin.php index 1ef1d08b0..0e0b065a0 100644 --- a/src/Config/CorePlugin.php +++ b/src/Config/CorePlugin.php @@ -24,7 +24,8 @@ final class CorePlugin implements Plugin { /** - * The presets that are complete standards; the other presets are sets. + * The presets that are complete standards, the first of them the one written when none is given; the other + * presets are sets. */ public const Standards = ['perCs', 'psr12', 'nette', 'symfony', 'laravel']; private const QualifiedName = '\\\\?\w+(\\\\\w+)*'; diff --git a/src/Config/Loader.php b/src/Config/Loader.php index 5c08dc382..0eba4ddf4 100644 --- a/src/Config/Loader.php +++ b/src/Config/Loader.php @@ -39,7 +39,8 @@ public static function load(?string $file, string $directory, ?Config $default = $file ??= self::find($directory); if ($file === null) { $config = $default ?? throw new ConfigurationException( - "No `dresscode.neon` or `dresscode.php` found in `$directory` or above it, so there is no dress code to check against. Name a standard with `--use`.", + "No `dresscode.neon` or `dresscode.php` found in `$directory` or above it, so there is no dress code to check against. Run `dresscode init` to have one made to measure from the code, or name a standard with `--use`.", + docs: 'cli#init', ); $composer = ComposerJson::findFile($directory); $root = $composer === null ? $directory : dirname($composer); @@ -78,6 +79,19 @@ public static function find(string $directory): ?string } + /** + * The configuration files lying in the directory itself, the local ones and the .dist templates. + * @return list their names + */ + public static function listFiles(string $directory): array + { + return array_values(array_filter( + array_merge(...array_map(fn(string $name) => [$name, $name . self::DistSuffix], self::FileNames)), + fn(string $name) => is_file("$directory/$name"), + )); + } + + /** The format follows the extension of the file, a .dist template that of the file it stands for. */ public static function loadFile(string $file): Config { diff --git a/src/Console/Application.php b/src/Console/Application.php index 70dcbfa91..a35d691fb 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -137,6 +137,7 @@ private function defineCommandLine(): Command new Commands\ConfigCommand($context), new Commands\ExplainCommand($context), new Commands\CatalogueCommand($context), + new Commands\InitCommand($context), ] as $handlers) { $program->addCommandsFrom($handlers); } diff --git a/src/Console/Commands/InitCommand.php b/src/Console/Commands/InitCommand.php new file mode 100644 index 000000000..22b7baa03 --- /dev/null +++ b/src/Console/Commands/InitCommand.php @@ -0,0 +1,75 @@ + $use + */ + #[Cli\Command('Measure the code of the project and write a `dresscode.neon` to fit')] + public function init( + #[Cli\Option( + 'The standard and the sets to write, `perCs` by default', + valueName: 'name', + repeatable: true, + normalizer: [ConfigOptions::class, 'checkPreset'], + )] + array $use = [], + ): int + { + $root = Helpers::canonicalizePath($this->context->workingDirectory); + $presets = $use ?: null; + $registry = new PluginRegistry; + foreach ($presets ?? [] as $preset) { + $registry->resolvePreset($preset); // a misspelled one before the measuring, not after it + } + + $proposal = Proposal::measure($root, $presets); + $proposal->verify($root); + $neon = $proposal->toNeon(); + + $existing = Loader::listFiles($root); + $console = $existing ? $this->context->err : $this->context->out; + $printer = new InitPrinter($proposal); + $console->write(Markup::highlightCode( + $console, + CommandContext::formatName($console) . "\n" . $printer->printMeasurements($console), + )); + $console->write(Markup::highlightCode($console, $printer->printDryRuns($console))); + if ($existing) { + $this->context->writeNote('`' . implode('` and `', $existing) . '`' . (count($existing) > 1 ? ' exist' : ' exists') . ", so the proposal is printed and nothing is written.\n"); + $this->context->out->write($neon); + return 3; + } + + FileSystem::write("$root/dresscode.neon", $neon); + $this->context->write("\n`dresscode.neon` written, made to measure.\n"); + return 0; + } +} diff --git a/src/Console/InitPrinter.php b/src/Console/InitPrinter.php new file mode 100644 index 000000000..11fdc2118 --- /dev/null +++ b/src/Console/InitPrinter.php @@ -0,0 +1,90 @@ +proposal; + $left = array_filter([ + $proposal->sample->oversized ? $proposal->sample->oversized . ' too large' : null, + $proposal->sample->generated ? $proposal->sample->generated . ' generated' : null, + ]); + $out = $console->color('gray', 'Sample ') . sprintf( + "%d of %d files in %s%s\n", + $proposal->countSampled(), + $proposal->total, + implode(', ', $proposal->paths), + $left ? ', ' . implode(' and ', $left) . ' left out' : '', + ); + $out .= $console->color('gray', 'Standard ') . implode(', ', $proposal->presets) . match (true) { + $proposal->presetsGiven => ", as given\n", + $proposal->formatter !== null + => ", the style of {$proposal->formatter}, which the project formats with; `--use` writes another\n", + default => ", not chosen by measure; the dry runs below count what each would change, not which is nearest, and `--use` writes another\n", + }; + $out .= $console->color('gray', 'Indent ') . $proposal->indent->describe() . "\n"; + $out .= $console->color('gray', 'Quotes ') . $proposal->quotes->describe() . "\n"; + $out .= $console->color('gray', 'Conditions ') . $proposal->conditions->describe() . "\n"; + return $out . $console->color('gray', 'Namespaces ') . $proposal->describeNamespaces() . "\n"; + } + + + /** What a dry run of the standard given, or of every standard over a part of the sample, would change. */ + public function printDryRuns(Console $console): string + { + $proposal = $this->proposal; + if ($proposal->presetsGiven) { + [$changed, $failed] = $proposal->countChanged(); + return $console->color('gray', 'Dry run ') . sprintf( + "%d of %d sampled files would change%s\n", + $changed, + $proposal->countSampled(), + $failed ? ", $failed failing" : '', + ); + } + + $out = ''; + $first = true; + foreach ($proposal->countChangedByStandard() as $standard => [$changed, $failed]) { + $out .= $console->color('gray', $first ? 'Dry run ' : ' ') . sprintf( + "%-8s %4d of %d%s%s%s\n", + $standard, + $changed, + $proposal->countPriced(), + $first ? ' sampled files would change' : '', + in_array($standard, $proposal->presets, true) ? ', the one written' : '', + $failed ? ", $failed failing" : '', + ); + $first = false; + } + + return $out; + } +} diff --git a/src/Measuring/FileSample.php b/src/Measuring/FileSample.php new file mode 100644 index 000000000..7ccfb3e48 --- /dev/null +++ b/src/Measuring/FileSample.php @@ -0,0 +1,81 @@ + the sample, relative to the root */ + public array $files, + /** files of the scope left out because they are too large or the sample had no room left */ + public int $oversized, + /** files of the scope left out because their head says a generator wrote them */ + public int $generated, + ) { + } + + + /** @param list $files the scope, sorted, relative to the root */ + public static function pick(string $root, array $files): self + { + $step = max(1, (int) ceil(count($files) / self::MaxFiles)); + $picked = []; + $bytes = $oversized = $generated = 0; + foreach ($files as $index => $file) { + if ($index % $step !== 0) { + continue; + } + + // a file that says it was generated says it whatever its size, so that is the reason it is left out for + $path = "$root/$file"; + $size = (int) @filesize($path); // @ file may have been deleted meanwhile + if (self::isGenerated($path)) { + $generated++; + } elseif ($size > self::MaxFileSize || $bytes + $size > self::MaxBytes) { + $oversized++; + } else { + $picked[] = $file; + $bytes += $size; + } + } + + return new self($picked, $oversized, $generated); + } + + + /** Whether the head of the file says a generator wrote it. */ + public static function isGenerated(string $file): bool + { + $head = @file_get_contents($file, length: self::HeadLength); // @ file may have been deleted meanwhile + return $head !== false && preg_match(self::GeneratedPattern, $head) === 1; + } +} diff --git a/src/Measuring/Measurement.php b/src/Measuring/Measurement.php new file mode 100644 index 000000000..80854cd48 --- /dev/null +++ b/src/Measuring/Measurement.php @@ -0,0 +1,109 @@ + value => places that agree with it */ + public array $agreeing, + /** places the decision appears in at all */ + public int $opportunities, + /** what a place is, in the plural: files, strings */ + public string $unit, + /** places among the opportunities that agree with none of the values */ + public int $neither = 0, + /** files the rule failed in under some value, left out of the places */ + public int $failed = 0, + ) { + } + + + public function getShare(string $value): float + { + return $this->opportunities === 0 ? 0.0 : $this->agreeing[$value] / $this->opportunities; + } + + + /** The value the code agrees with in most places; null when the decision appears nowhere. */ + public function findCommonest(): ?string + { + if ($this->opportunities === 0) { + return null; + } + + $agreeing = $this->agreeing; + arsort($agreeing); + return (string) array_key_first($agreeing); + } + + + /** The value at least the threshold of places agrees with, which a proposal may write as the value. */ + public function findPrevailing(): ?string + { + $value = $this->findCommonest(); + return $value !== null && $this->getShare($value) >= self::Threshold ? $value : null; + } + + + /** Every value some place agrees with, and its share, the commonest first: "tab 55%, 4 45% of 290 files". */ + public function describe(): string + { + $parts = []; + foreach ($this->sortAgreeing() as $value) { + $parts[] = sprintf('%s %d%%', $value, round(100 * $this->getShare((string) $value))); + } + + return match (true) { + $this->opportunities === 0 => "no $this->unit", + $parts === [] => "none of the $this->opportunities $this->unit agrees with any value", + default => implode(', ', $parts) . " of $this->opportunities $this->unit" + . ($this->neither ? ", $this->neither in none of them" : ''), + } . ($this->failed ? sprintf(', %d failing %s left out', $this->failed, $this->failed === 1 ? 'file' : 'files') : ''); + } + + + /** + * The values some place agrees with, the commonest first, when together they hold at least the threshold of + * the places: as a list, the tolerance of what the code already has. + * @return ?list + */ + public function findTolerated(): ?array + { + if ( + $this->opportunities === 0 + || ($this->opportunities - $this->neither) / $this->opportunities < self::Threshold + ) { + return null; + } + + return array_map(strval(...), $this->sortAgreeing()); + } + + + /** @return list */ + private function sortAgreeing(): array + { + $agreeing = array_filter($this->agreeing); + arsort($agreeing); + return array_keys($agreeing); + } +} diff --git a/src/Measuring/Proposal.php b/src/Measuring/Proposal.php new file mode 100644 index 000000000..363f0f123 --- /dev/null +++ b/src/Measuring/Proposal.php @@ -0,0 +1,604 @@ + */ + public array $presets, + /** the presets were given, not the default */ + public bool $presetsGiven, + /** the formatter of the project whose style the standard is, null where the standard is the default */ + public ?string $formatter, + /** @var list */ + public array $paths, + /** @var list */ + public array $fileExtensions, + /** @var list */ + public array $excludePaths, + /** the files of the scope */ + public int $total, + public Measurement $indent, + public Measurement $quotes, + /** the shape of the conditions on several lines */ + public Measurement $conditions, + /** @var list fully qualified names of the functions the namespaces of the scope declare */ + public array $namespacedFunctions, + /** @var list fully qualified names of the constants the namespaces of the scope declare */ + public array $namespacedConstants, + /** @var list the files of the scope that do not parse, whose declarations are therefore unknown */ + public array $unparsed, + /** the project has phpstan/phpstan, so the rules can get the types of the code */ + public bool $phpstanInstalled, + /** the files everything above was measured on */ + public FileSample $sample, + private Survey $survey, + ) { + } + + + /** + * @param ?list $presets the standard, named as the configuration names it; null for the default + * @throws ConfigurationException + */ + public static function measure(string $root, ?array $presets = null): self + { + $paths = self::findPaths($root); + $factory = new RunnerFactory; + $scope = $factory->createRunner($factory->resolve(new Config(fileExtensions: ['php', 'phpt']), $root), cache: false); + $files = $generated = []; + foreach ($scope->findFiles($paths) as $file) { + $dirs = array_intersect(self::GeneratedDirs, explode('/', $file)); + if ($dirs) { + $generated += array_flip($dirs); + } else { + $files[] = $file; + } + } + + $extensions = ['php', ...(array_filter($files, fn(string $file) => str_ends_with($file, '.phpt')) ? ['phpt'] : [])]; + $excludePaths = array_values(array_intersect(self::GeneratedDirs, array_keys($generated))); + $files = array_values(array_filter($files, fn(string $file) => in_array(pathinfo($file, PATHINFO_EXTENSION), $extensions, true))); + $sample = FileSample::pick($root, $files); + $survey = new Survey($root, $sample->files, new Config(excludePaths: $excludePaths, fileExtensions: $extensions)); + + $indents = []; + foreach (self::Indents as $indent) { + $indents[$indent] = new Profile(decisions: ['indentation' => ['unit' => self::toUnit($indent)]]); + } + + [$functions, $constants, $unparsed] = self::collectNamespacedDeclarations($root, $files); + $installed = self::findInstalledPackages($root); + $pint = $presets === null ? self::findPintStandard($root, $installed) : null; + return new self( + $presets ?? [$pint ?? CorePlugin::Standards[0]], + $presets !== null, + $pint === null ? null : 'Laravel Pint', + $paths, + $extensions, + $excludePaths, + count($files), + $survey->measureFiles(IndentationRule::class, $indents), + $survey->measurePlaces(StringQuotesRule::class, [ + 'single' => new Profile(decisions: ['literals' => ['quotes' => 'single']]), + 'double' => new Profile(decisions: ['literals' => ['quotes' => 'double']]), + ], 'strings'), + $survey->measurePlaces( + MultilineConditionRule::class, + [ + 'apart' => new Profile(decisions: ['multiline' => ['edges' => ['condition' => 'apart']]]), + 'closingApart' => new Profile(decisions: ['multiline' => ['edges' => ['condition' => 'closingApart']]]), + ], + 'conditions', + new Profile(decisions: ['multiline' => ['edges' => ['condition' => ['apart', 'closingApart']]]]), + ), + $functions, + $constants, + $unparsed, + isset($installed['phpstan/phpstan']), + $sample, + $survey, + ); + } + + + /** + * The scope of a project: the directories its autoload names, the conventional ones the root has besides + * them, and the root itself when neither says anything. A directory that lies in another one is already + * in the scope, and one that does not exist would only make the run throw. + * @return list + */ + public static function findPaths(string $root): array + { + $paths = ComposerJson::detectAutoloadPaths(ComposerJson::findFile($root), $root); + foreach (self::Paths as $name) { + // a pattern is matched against the names of the root, never against a path of its own, or a root + // holding a bracket or a star would be part of the pattern and quietly match nothing + $found = str_contains($name, '*') + ? array_values(array_filter(scandir($root) ?: [], fn(string $entry) => fnmatch($name, $entry) && is_dir("$root/$entry"))) + : (is_dir("$root/$name") ? [$name] : []); + $paths = [...$paths, ...$found]; + } + + $scope = []; + foreach (array_unique($paths) as $path) { + foreach ($paths as $other) { + if ($other !== $path && ($other === '.' || str_starts_with("$path/", "$other/"))) { + continue 2; + } + } + + $scope[] = $path; + } + + sort($scope, SORT_STRING); + return $scope ?: ['.']; + } + + + /** + * The functions and constants the namespaces of the scope declare, read from every file and not from the sample, + * because a declaration left out would be taken as global where nothing reports it. Every file naming a namespace + * or `define()` is parsed, since a declaration can stand anywhere a statement can; one that does not parse is listed, + * its declarations unknown. + * @param list $files + * @return array{list, list, list} functions, constants and the files that do not parse + */ + private static function collectNamespacedDeclarations(string $root, array $files): array + { + $parser = new Parser; + $names = [SymbolKind::Function->name => [], SymbolKind::Constant->name => []]; + $unparsed = []; + foreach ($files as $file) { + $code = @file_get_contents(Helpers::toAbsolutePath($file, $root)); // @ file may have been deleted meanwhile + if ($code === false || (stripos($code, 'namespace') === false && stripos($code, 'define') === false)) { + continue; + } + + try { + $node = $parser->parse($code); + } catch (ParseException) { + $unparsed[] = $file; + continue; + } + + foreach (new NameResolver($node)->findNamespacedDeclarations($node) as $declaration) { + $key = ConfigResolver::toSymbolKey($declaration->kind, $declaration->name); + $names[$declaration->kind->name][$key] ??= $declaration->name; + } + } + + $lists = []; + foreach ([SymbolKind::Function, SymbolKind::Constant] as $kind) { + $list = array_values($names[$kind->name]); + sort($list, SORT_STRING | SORT_FLAG_CASE); + $lists[] = $list; + } + + return [$lists[0], $lists[1], $unparsed]; + } + + + /** + * The packages the project has, told by the lock file beside its composer.json or, without one, by what Composer + * installed. + * @return array + */ + private static function findInstalledPackages(string $root): array + { + $composer = ComposerJson::findFile($root); + $installed = []; + foreach ([$composer === null ? null : dirname($composer) . '/composer.lock', "$root/vendor/composer/installed.json"] as $file) { + $data = ComposerJson::read($file); + if ($data === null) { + continue; + } + + $packages = array_is_list($data) ? $data : [...(array) ($data['packages'] ?? []), ...(array) ($data['packages-dev'] ?? [])]; + foreach ($packages as $package) { + if (is_array($package) && isset($package['name'])) { + $installed[(string) $package['name']] = true; + } + } + + break; + } + + return $installed; + } + + + /** + * The standard of the preset Laravel Pint formats the project with, `laravel` where `pint.json` names none; null + * for a project without Pint and for a preset no standard is, `empty` among them. + * @param array $installed + */ + private static function findPintStandard(string $root, array $installed): ?string + { + $config = ComposerJson::read("$root/pint.json"); + if ($config === null && !isset($installed['laravel/pint'])) { + return null; + } + + return match ($config['preset'] ?? 'laravel') { + 'laravel' => 'laravel', + 'psr12' => 'psr12', + 'per' => 'perCs', + 'symfony' => 'symfony', + default => null, + }; + } + + + /** + * The configuration as data, the same the text says; with a standard of its own what that standard would + * come to over the same decisions, which is what a price is measured on. + * @param ?list $presets + */ + public function toConfig(?array $presets = null): Config + { + $indent = $this->indent->findPrevailing(); + $quotes = $this->findQuotes(); + $shape = $this->findConditionShape(); + return new Config( + use: $presets ?? $this->presets, + namespaces: ['functions' => $this->namespacedFunctions, 'constants' => $this->namespacedConstants], + nameResolution: 'certain', + typeAnalysis: $this->phpstanInstalled ? 'phpstan' : null, + paths: $this->paths, + excludePaths: $this->excludePaths, + fileExtensions: $this->fileExtensions, + decisions: array_filter([ + 'indentation' => $indent === null ? null : ['unit' => self::toUnit($indent)], + 'multiline' => $shape === null ? null : ['edges' => ['condition' => $shape]], + 'literals' => $quotes === null ? null : ['quotes' => $quotes], + ]), + ); + } + + + /** + * The configuration as dresscode.neon, every measured value with the share it has in the code. A decision + * no value of which reaches the threshold is not written as a value. + */ + public function toNeon(): string + { + $sections = [ + sprintf( + "# Written by dresscode init from %d of the %d files. A number is the share of the places a decision\n" + . "# appears in that already agree with its value; what the file does not name, the standard decides.\n", + $this->countSampled(), + $this->total, + ), + self::renderUse( + $this->presets, + match (true) { + $this->presetsGiven => '', + $this->formatter !== null => " # the style of {$this->formatter}, which the project formats with; the other complete standards are " + . $this->describeOthers(), + default => ' # not chosen by measure; the other complete standards are ' . $this->describeOthers(), + }, + ), + ]; + + $lists = ''; + foreach (['functions' => $this->namespacedFunctions, 'constants' => $this->namespacedConstants] as $key => $names) { + $lists .= $names ? "\t$key:\n" . implode('', array_map(fn(string $name) => "\t\t- $name\n", $names)) : ''; + } + + $sections[] = match (true) { + $this->unparsed !== [] => "# {$this->describeUnparsed()}, so the namespaces may declare more than is listed here\n" + . "# and an unqualified name in a namespace is not resolved for certain; once every file parses, init writes\n" + . "# nameResolution: certain\n", + $lists === '' => "# the namespaces of the scope declare no function and no constant, so an unqualified name in a namespace\n" + . "# is resolved for certain; one declared later is reported until it is listed\nnameResolution: certain\n", + default => "# the namespaces of the scope declare these functions and constants and no others, so an unqualified name\n" + . "# in a namespace is resolved for certain; one declared later is reported until it is listed\nnameResolution: certain\n", + } . ($lists === '' ? '' : "namespaces:\n$lists"); + + if ($this->phpstanInstalled) { + $sections[] = "# phpstan/phpstan is installed, so the rules get the types of the code: a fix the type of a value decides\n" + . "# is made where the types tell it is safe, and the rules that need the types run\ntypeAnalysis: phpstan\n"; + } + + $sections[] = "paths:\n" . implode('', array_map(fn(string $path) => "\t- $path\n", $this->paths)); + if ($this->excludePaths) { + $sections[] = "excludePaths:\n" . implode('', array_map(fn(string $path) => "\t- $path\n", $this->excludePaths)); + } + + if ($this->fileExtensions !== ['php']) { + $sections[] = "fileExtensions:\n" . implode('', array_map(fn(string $ext) => "\t- $ext\n", $this->fileExtensions)); + } + + $indent = $this->indent->findPrevailing(); + if ($indent !== null) { + $sections[] = self::renderDecision( + IndentationRule::class, + 'indentation.unit', + $indent === 'tab' ? 'tab' : self::toUnit($indent), + $this->indent->describe(), + ); + } elseif ($this->indent->opportunities) { + $sections[] = sprintf( + "# indentation.unit: %s; no value reaches %d%%, so the standard decides\n", + $this->indent->describe(), + 100 * Measurement::Threshold, + ); + } + + $shape = $this->findConditionShape(); + if ($shape !== null) { + $sections[] = self::renderDecision( + MultilineConditionRule::class, + 'multiline.edges.condition', + $shape, + $this->conditions->describe(), + ); + } + + $quotes = $this->findQuotes(); + if ($quotes !== null) { + $sections[] = self::renderDecision(StringQuotesRule::class, 'literals.quotes', $quotes, $this->quotes->describe()); + } + + return implode("\n", $sections); + } + + + /** + * The key of a decision under its section, what the catalogue says of it above and what was measured beside it. + * @param class-string $rule + */ + private static function renderDecision(string $rule, string $path, mixed $value, string $measured): string + { + $decision = array_find(Config\Catalogue::collectDecisions($rule), fn(Decision $decision) => $decision->path === $path) + ?? throw new \LogicException("Rule `$rule` does not declare `$path`."); + $links = explode('.', $path); + $key = array_pop($links); + $out = ''; + foreach ($links as $depth => $link) { + $out .= str_repeat("\t", $depth) . "$link:\n"; + } + + // a line never breaks inside code + $comment = $decision->description . '. Values: ' . $decision->describeValues(); + $comment = preg_replace_callback('~`[^`]*`~', fn(array $m) => str_replace(' ', "\0", $m[0]), $comment); + $comment = str_replace("\0", ' ', wordwrap($comment, 110, "\n")); + $indent = str_repeat("\t", count($links)); + $written = $decision->domain->accept($value, $path, keep: true)->toWrittenText(); + return $out . preg_replace('~^~m', "$indent# ", $comment) . "\n$indent$key: $written # $measured\n"; + } + + + /** + * The key `use`, on one line for a single entry, the comment following the standard. + * @param list $entries + */ + private static function renderUse(array $entries, string $comment): string + { + return count($entries) === 1 + ? "use: $entries[0]$comment\n" + : "use:\n" + . implode( + '', + array_map( + fn(string $entry, int $i) => "\t- $entry" . ($i === 0 ? $comment : '') . "\n", + $entries, + array_keys($entries), + ), + ); + } + + + /** What the namespaces of the scope declare, for the report of init. */ + public function describeNamespaces(): string + { + $counts = fn(int $count, string $noun) => match ($count) { + 0 => "no $noun", + 1 => "1 $noun", + default => "$count {$noun}s", + }; + return $counts(count($this->namespacedFunctions), 'function') . ' and ' . $counts(count($this->namespacedConstants), 'constant') + . ' declared, ' . ($this->unparsed !== [] ? 'not resolved for certain, ' . $this->describeUnparsed() : 'resolved for certain'); + } + + + /** The files that do not parse, the first of them by name. */ + private function describeUnparsed(): string + { + $others = count($this->unparsed) - 1; + return $this->unparsed[0] . match ($others) { + 0 => ' does not parse', + 1 => ' and 1 other file do not parse', + default => " and $others other files do not parse", + }; + } + + + /** + * How many files of the sample the proposal would change, and how many a rule fails in. + * @return array{int, int} + */ + public function countChanged(): array + { + return $this->survey->countChanged($this->toConfig()); + } + + + /** + * What each complete standard would cost over the same decisions, the cheapest first: the one number about + * the standards that measures a consequence and not a likeness, which is why it is told and no standard is + * chosen by it. A price comes with the files a rule of the standard fails in, and is counted over the files + * `countPriced()` says. + * @return array + * @throws ConfigurationException + */ + public function countChangedByStandard(): array + { + $survey = $this->survey->narrow(self::PricedFiles); + $prices = []; + foreach (CorePlugin::Standards as $standard) { + $prices[$standard] = $survey->countChanged($this->toConfig([$standard])); + } + + asort($prices); + return $prices; + } + + + /** The files of the sample the prices of the standards are counted over. */ + public function countPriced(): int + { + return $this->survey->narrow(self::PricedFiles)->countFiles(); + } + + + public function countSampled(): int + { + return count($this->sample->files); + } + + + /** + * Resolves the configuration the proposal writes the way a run reads it and checks it against what was measured, + * before it is anywhere a run could read it. + * @throws \LogicException + * @throws ConfigurationException + */ + public function verify(string $root): void + { + $temp = sys_get_temp_dir() . '/dresscode-init-' . uniqid() . '.neon'; + FileSystem::write($temp, $this->toNeon()); + try { + $this->checkResolution((new RunnerFactory)->resolve(Loader::loadFile($temp), $root)->resolvedConfig); + } finally { + @unlink($temp); // @ file may have been deleted meanwhile + } + } + + + /** + * What the run makes of the written configuration must be what was measured; anything else is a proposal + * that says one thing and does another. + * @throws \LogicException + */ + public function checkResolution(ResolvedConfig $resolved): void + { + $indent = $this->indent->findPrevailing(); + $quotes = $this->findQuotes(); + $shape = $this->findConditionShape(); + $quotesValue = ($resolved->decisions['literals.quotes'] ?? null)?->value; + $conditionValue = ($resolved->decisions['multiline.edges.condition'] ?? null)?->value; + $problem = match (true) { + $indent !== null + && $resolved->indent !== ($indent === 'tab' ? "\t" : str_repeat(' ', (int) $indent)) => "the indentation is not `$indent`", + $quotes !== null + && ($quotesValue === null + || ($quotesValue->isKept() ? 'keep' : $quotesValue->getWord()) !== $quotes) => "`literals.quotes` is not `$quotes`", + $shape !== null + && $conditionValue?->toData() !== (is_string($shape) && $shape !== 'keep' + ? [$shape] + : $shape) => '`multiline.edges.condition` has another shape', + ($resolved->nameResolution === 'certain') !== ($this->unparsed === []) + => 'the name resolution is not what the files that parse allow', + array_diff( + $this->namespacedFunctions, + array_keys($resolved->namespacedFunctions), + ) !== [] => 'a function the namespaces declare is not listed', + array_diff( + $this->namespacedConstants, + array_keys($resolved->namespacedConstants), + ) !== [] => 'a constant the namespaces declare is not listed', + // a DressCode without PHPStan beside it goes without the types the project can give + $this->phpstanInstalled !== ($resolved->typeAnalysis === 'phpstan') + && Analyses\PhpStan::isAvailable() => 'the types are not what the installed packages allow', + default => null, + }; + if ($problem !== null) { + throw new \LogicException("The configuration `dresscode init` wrote does not say what it measured: $problem."); + } + } + + + /** What the file says of the complete standards beside the one it writes by itself: "psr12, nette and symfony". */ + private function describeOthers(): string + { + $others = array_values(array_diff(CorePlugin::Standards, $this->presets)); + $last = array_pop($others); + return implode(', ', $others) . " and $last"; + } + + + /** + * The shape the conditions on several lines are written in: the one that reaches the threshold, else the + * shapes the code has, the commonest first, when together they do, else keep, since the code writes its + * conditions in no shape the rule knows; null when the sample has no such condition. + * @return string|list|null + */ + private function findConditionShape(): string|array|null + { + return $this->conditions->opportunities + ? $this->conditions->findPrevailing() ?? $this->conditions->findTolerated() ?? 'keep' + : null; + } + + + /** + * The quotes the strings are written with: the ones that reach the threshold, else keep, since quotes have no + * tolerance and a value half of the strings disagree with would rewrite them; null when the sample has no string + * that could take either. + */ + private function findQuotes(): ?string + { + return $this->quotes->opportunities ? $this->quotes->findPrevailing() ?? 'keep' : null; + } + + + /** The unit of the indentation as the decision writes it, `tab` or `4 spaces`. */ + private static function toUnit(string $indent): string + { + return $indent === 'tab' ? 'tab' : "$indent spaces"; + } +} diff --git a/src/Measuring/Survey.php b/src/Measuring/Survey.php new file mode 100644 index 000000000..5c831d148 --- /dev/null +++ b/src/Measuring/Survey.php @@ -0,0 +1,152 @@ + the sample, relative to the root */ + private array $files, + /** what the configuration leaves the run to, the scope of the files above all */ + private Config $base, + ) { + } + + + /** + * A decision that is a property of the file: a file agrees with a value when the rule of that value reports + * nothing in it, and a file no value reports has none of the decision in it and does not count. Neither does a + * file the rule fails in under any of the values, which would otherwise agree with that one. + * @param class-string $rule + * @param array $values value => the profile that runs the rule with it; PHP keeps a numeric value as an int key + * @throws ConfigurationException + */ + public function measureFiles(string $rule, array $values): Measurement + { + $probes = array_map(fn(Profile $layer) => $this->probe($rule, $layer), $values); + $failed = array_merge(...array_column($probes, 1)); + $reported = array_map(fn(array $probe) => array_diff_key($probe[0], $failed), $probes); + $opportunities = count(array_replace([], ...array_values($reported))); + return new Measurement( + array_map(fn(array $files) => $opportunities - count($files), $reported), + $opportunities, + 'files', + failed: count($failed), + ); + } + + + /** + * A decision every place makes anew, where the rule of each value reports every place written another way, + * once: the places are what the values report together, and what one reports disagrees with it. A place in + * none of the values is reported by every one of them; the profile that lets all of them pass counts those, + * so that each is one place and disagrees with every value. The places of a file the rule fails in under any + * of the profiles are left out of all of them. + * @param class-string $rule + * @param array $values value => the profile that runs the rule with it; PHP keeps a numeric value as an int key + * @param ?Profile $any the profile that runs the rule with every value allowed + * @throws ConfigurationException + */ + public function measurePlaces(string $rule, array $values, string $unit, ?Profile $any = null): Measurement + { + $probes = array_map(fn(Profile $layer) => $this->probe($rule, $layer), $values); + $neither = $any === null ? [[], []] : $this->probe($rule, $any); + $failed = array_merge($neither[1], ...array_column($probes, 1)); + $count = fn(array $probe) => array_sum(array_diff_key($probe[0], $failed)); + $reported = array_map($count, $probes); + $opportunities = array_sum($reported) - (count($values) - 1) * $count($neither); + return new Measurement( + array_map(fn(int $count) => $opportunities - $count, $reported), + $opportunities, + $unit, + $count($neither), + count($failed), + ); + } + + + /** + * How many files of the sample the configuration would change, which is the number a user weighs, and how many + * a rule fails in, whose change is not known. + * @return array{int, int} + * @throws ConfigurationException + */ + public function countChanged(Config $config): array + { + $factory = new RunnerFactory; + $runner = $factory->createRunner($factory->resolve($config, $this->root), cache: false); + $run = $runner->run($this->files, fix: false, reporter: new NullReporter); + return [$run->countChangedFiles(), $run->countFailures()]; + } + + + /** The survey over at most the given number of files of the sample, spread evenly over it. */ + public function narrow(int $count): self + { + $total = count($this->files); + if ($total <= $count) { + return $this; + } + + $files = []; + for ($i = 0; $i < $count; $i++) { + $files[] = $this->files[intdiv($i * $total, $count)]; + } + + return new self($this->root, $files, $this->base); + } + + + public function countFiles(): int + { + return count($this->files); + } + + + /** + * What the rule reports with the profile laid over the base, counted by file, and the files it fails in; a violation + * that follows from another one is not a place of its own. + * @param class-string $rule + * @return array{array, array} + * @throws ConfigurationException + */ + private function probe(string $rule, Profile $layer): array + { + $factory = new RunnerFactory; + $runner = $factory->createRunner($factory->resolve($this->base, $this->root, $layer), cache: false); + $decisions = array_fill_keys(array_map(fn(Decision $decision) => $decision->path, Config\Catalogue::collectDecisions($rule)), true); + $reported = $failed = []; + foreach ($runner->run($this->files, fix: false, reporter: new NullReporter)->files as $result) { + if ($result->failure !== null) { + $failed[$result->path] = true; + continue; + } + + foreach ($result->violations as $violation) { + if (isset($decisions[$violation->decision]) && $violation->derivedFrom === null) { + $reported[$result->path] = ($reported[$result->path] ?? 0) + 1; + } + } + } + + return [$reported, $failed]; + } +} diff --git a/src/Reporters/ConsoleReporter.php b/src/Reporters/ConsoleReporter.php index 16473d6bf..257dd1727 100644 --- a/src/Reporters/ConsoleReporter.php +++ b/src/Reporters/ConsoleReporter.php @@ -272,7 +272,7 @@ private function formatRefused(Risk $risk, RunResult $result): string $text = match ($risk) { Risk::NameUncertain => "$wait, a name may reach a function of the namespace: " . ($this->namespacesListed ? 'set `nameResolution: certain`' - : 'list what the namespaces declare under `namespaces`, or set `nameResolution: certain` if they declare nothing'), + : 'run `dresscode init`, which lists what the namespaces declare, or set `nameResolution: certain` if they declare nothing'), Risk::TypeUnknown => match ($this->typeAnalysis) { TypeAnalysisStatus::Available => "$wait, the type is unknown: set `typeAnalysis: phpstan`", TypeAnalysisStatus::Enabled => "$wait, not even the types tell: check $them with `fix --ask-risky`", diff --git a/tests/DressCode/Config/Loader.phpt b/tests/DressCode/Config/Loader.phpt index 7b4d58b6d..ae6e3c9bc 100644 --- a/tests/DressCode/Config/Loader.phpt +++ b/tests/DressCode/Config/Loader.phpt @@ -132,7 +132,7 @@ test('load: without a file and without a default there is no style to run', func Assert::exception( fn() => Loader::load(null, sys_get_temp_dir()), ConfigurationException::class, - 'No `dresscode.neon` or `dresscode.php` found in `%a%` or above it, so there is no dress code to check against. Name a standard with `--use`.', + 'No `dresscode.neon` or `dresscode.php` found in `%a%` or above it, so there is no dress code to check against. Run `dresscode init` to have one made to measure from the code, or name a standard with `--use`.', ); }); diff --git a/tests/DressCode/Console/Application.phpt b/tests/DressCode/Console/Application.phpt index a3810c77e..7ad66ef59 100644 --- a/tests/DressCode/Console/Application.phpt +++ b/tests/DressCode/Console/Application.phpt @@ -568,7 +568,7 @@ test('without a configuration file neither check nor fix runs, unless a preset i [$code, , $err] = runApp($dir, [$command]); Assert::same(3, $code); Assert::match( - "Error: No `dresscode.neon` or `dresscode.php` found in `%a%` or above it, so there is no dress code to check against. Name a standard with `--use`.\n", + "Error: No `dresscode.neon` or `dresscode.php` found in `%a%` or above it, so there is no dress code to check against. Run `dresscode init`%A%.\nSee https://dresscode.run/cli#init\n", $err, ); } diff --git a/tests/DressCode/Console/init.phpt b/tests/DressCode/Console/init.phpt new file mode 100644 index 000000000..b30bb4db4 --- /dev/null +++ b/tests/DressCode/Console/init.phpt @@ -0,0 +1,461 @@ + $files + */ +function createProject(string $name, array $files): string +{ + $root = createTempDir($name); + foreach ($files as $path => $content) { + @mkdir(dirname("$root/$path"), recursive: true); // @ directory may already exist + file_put_contents("$root/$path", $content); + } + + return $root; +} + + +/** + * @param list $args + * @return array{int, string, string} + */ +function runInit(string $root, array $args = []): array +{ + $out = fopen('php://memory', 'w+') ?: throw new RuntimeException; + $err = fopen('php://memory', 'w+') ?: throw new RuntimeException; + $code = new Application($out, $err, cwd: $root)->run(['dresscode', 'init', ...$args]); + rewind($out); + rewind($err); + return [$code, (string) stream_get_contents($out), (string) stream_get_contents($err)]; +} + + +$tabbed = " $tabbed, + 'src/B.php' => str_replace('A', 'B', $tabbed), + 'src/C.php' => " " " '{}', // the project of its own, not the one the temporary directory lies in + ]); + [$code, $out] = runInit($root); + Assert::same(0, $code); + Assert::match(<<<'XX' + DRESS|CODE %a% + Sample 3 of 4 files in src, tests, 1 generated left out + Standard perCs, not chosen by measure; the dry runs below count what each would change, not which is nearest, and `--use` writes another + Indent tab 100% of 3 files + Quotes single 86%, double 14% of 7 strings + Conditions no conditions + Namespaces no function and no constant declared, resolved for certain + Dry run perCs 1 of 3 sampled files would change, the one written + psr12 1 of 3 + nette 3 of 3 + symfony 3 of 3 + laravel 3 of 3 + + `dresscode.neon` written, made to measure. + + XX, $out); + + $neon = (string) file_get_contents("$root/dresscode.neon"); + Assert::match(<<<'XX' + # Written by dresscode init from 3 of the 4 files. %A% + + use: perCs # not chosen by measure; the other complete standards are psr12, nette, symfony and laravel + + # the namespaces of the scope declare no function and no constant, so an unqualified name in a namespace + # is resolved for certain; one declared later is reported until it is listed + nameResolution: certain + + paths: + - src + - tests + + excludePaths: + - fixtures + + fileExtensions: + - php + - phpt + + indentation: + # Every line indented by the construct it continues, %A% + # `2 spaces` (two spaces per level); `keep` + unit: tab # tab 100% of 3 files + + literals: + # The quotes of a plain string, %A% + quotes: single # single 86%, double 14% of 7 strings + + XX, $neon); + Assert::notContains('fixRisky', $neon); + + // the round trip: what the run resolves the written file to is what was measured + $factory = new RunnerFactory; + $resolved = $factory->resolve(Loader::loadFile("$root/dresscode.neon"), $root)->resolvedConfig; + Assert::same("\t", $resolved->indent); + Assert::same('single', $resolved->decisions['literals.quotes']->value->getWord()); + Assert::noError(fn() => Proposal::measure($root)->checkResolution($resolved)); + + // and the configuration is one a run takes: the generated file is out, the one double quote is found + $out = fopen('php://memory', 'w+') ?: throw new RuntimeException; + $check = new Application($out, $out, cwd: $root)->run(['dresscode', 'check', '--no-cache', '--jobs', '1']); + rewind($out); + Assert::same(1, $check); + Assert::match('%A%tests%a%c.phpt%A%literals.quotes%A%FOUND 1 violation%A%', (string) stream_get_contents($out)); +}); + + +test( + 'a configuration that exists is not overwritten; the proposal goes to the output and the exit code says so', + function () use ($tabbed) { + $root = createProject('existing', ['src/A.php' => $tabbed, 'dresscode.php.dist' => " " " " ' '{"autoload": {"psr-4": {"App\\\\": "src"}}}', + 'composer.lock' => '{"packages": [{"name": "symfony/dependency-injection"}], "packages-dev": []}', + 'vendor/composer/installed.json' => '{"packages": [{"name": "symfony/dependency-injection", "version": "7.3.0", "install-path": "../symfony/dependency-injection"}]}', + ]); + [$code, $out] = runInit($root, ['--use', 'perCs']); + Assert::same(0, $code); + Assert::contains('Namespaces 3 functions and 3 constants declared, resolved for certain', $out); + Assert::match(<<<'XX' + %A%use: perCs + %A?% + # the namespaces of the scope declare these functions and constants and no others, so an unqualified name + # in a namespace is resolved for certain; one declared later is reported until it is listed + nameResolution: certain + namespaces: + functions: + - App\Compat\first + - App\helper + - App\One\one + constants: + - App\BOOTED + - App\LIMIT + - App\One\DEFINED + %A% + XX, (string) file_get_contents("$root/dresscode.neon")); + + $factory = new RunnerFactory; + $resolved = $factory->resolve(Loader::loadFile("$root/dresscode.neon"), $root)->resolvedConfig; + Assert::same('certain', $resolved->nameResolution); + Assert::same(['dresscode/psr12', 'dresscode/perCs'], $resolved->use); + Assert::same('the configuration', $resolved->namespacedFunctions['App\Compat\first']); + Assert::same( + 'DressCode for symfony/dependency-injection', + $resolved->namespacedFunctions['Symfony\Component\DependencyInjection\Loader\Configurator\service'], + ); + Assert::true($resolved->findRule(DressCode\Rules\Namespaces\NoUnlistedNamespacedDeclarationsRule::class)?->isActive()); +}); + + +test('init turns the types on where the project has PHPStan, and says why', function () { + $root = createProject('typed', [ + 'src/a.php' => " '{"autoload": {"psr-4": {"App\\\\": "src"}}}', + 'composer.lock' => '{"packages": [], "packages-dev": [{"name": "phpstan/phpstan"}]}', + ]); + [$code] = runInit($root, ['--use', 'perCs']); + Assert::same(0, $code); + Assert::match(<<<'XX' + %A% + # phpstan/phpstan is installed, so the rules get the types of the code: a fix the type of a value decides + # is made where the types tell it is safe, and the rules that need the types run + typeAnalysis: phpstan + %A% + XX, (string) file_get_contents("$root/dresscode.neon")); + Assert::same('phpstan', Loader::loadFile("$root/dresscode.neon")->typeAnalysis); + + $root = createProject('untyped', ['src/a.php' => " '{"autoload": {"psr-4": {"App\\\\": "src"}}}']); + runInit($root, ['--use', 'perCs']); + Assert::notContains('types:', (string) file_get_contents("$root/dresscode.neon")); +}); + + +test('init proposes the standard of the preset Laravel Pint formats with, as the package or its configuration says', function () { + foreach ([ + 'package' => ['composer.lock' => '{"packages": [], "packages-dev": [{"name": "laravel/pint"}]}'], + 'configuration' => ['pint.json' => '{"preset": "laravel"}'], + ] as $name => $files) { + $root = createProject( + "pint-$name", + ['src/a.php' => " '{"autoload": {"psr-4": {"App\\\\": "src"}}}', ...$files], + ); + [$code, $out] = runInit($root); + Assert::same(0, $code); + Assert::contains('Standard laravel, the style of Laravel Pint, which the project formats with', $out); + Assert::contains( + 'use: laravel # the style of Laravel Pint, which the project formats with; the other complete standards are perCs, psr12, nette and symfony', + (string) file_get_contents("$root/dresscode.neon"), + ); + } + + // another preset of Pint is its standard, and one no standard is leaves the default + foreach (['per' => 'perCs', 'psr12' => 'psr12', 'symfony' => 'symfony', 'empty' => null] as $preset => $standard) { + $root = createProject("pint-$preset", ['src/a.php' => " "{\"preset\": \"$preset\"}"]); + [, $out] = runInit($root); + Assert::contains( + $standard === null ? 'Standard perCs, not chosen by measure' : "Standard $standard, the style of Laravel Pint", + $out, + ); + } + + // a standard given wins + $root = createProject('pint-given', ['src/a.php' => " '{}']); + runInit($root, ['--use', 'nette']); + Assert::contains("use: nette\n", (string) file_get_contents("$root/dresscode.neon")); +}); + + +test('a file that does not parse leaves what the namespaces declare unknown, so init writes no certain resolution', function () { + $root = createProject('unparsed', [ + 'src/helpers.php' => " " $tabbed]); + [$code] = runInit($root, ['--use', 'psr12', '--use', 'perCs']); + Assert::same(0, $code); + Assert::contains("use:\n\t- psr12\n\t- perCs\n\n", (string) file_get_contents("$root/dresscode.neon")); +}); + + +test('a decision the code does not make clearly is not written as a value', function () { + $root = createProject('undecided', [ + 'src/a.php' => " "resolve(Loader::loadFile("$root/dresscode.neon"), $root)->resolvedConfig; + Assert::false($resolved->findRule(DressCode\Rules\Literals\StringQuotesRule::class)?->isActive()); + Assert::same("\t", $resolved->indent); // the tab of dresscode/nette +}); + + +test('the unit of the indentation is measured alone, whatever levels the other constructs step in by', function () { + $switch = " $switch, 'src/b.php' => $switch]); + [$code, $out] = runInit($root); + Assert::same(0, $code); + Assert::contains("Indent tab 100% of 2 files\n", $out); +}); + + +test('the shape of the conditions is counted by condition, and both shapes pass where neither prevails', function () { + $perLine = "if (\n\t\$a\n\t&& \$b\n) {\n}\n"; + $compact = "if (\$a\n\t&& \$b\n) {\n}\n"; + $neither = "if (\$a\n\t&& \$b) {\n}\n"; + $root = createProject('shapes', [ + 'src/a.php' => " "resolve(Loader::loadFile("$root/dresscode.neon"), $root)->resolvedConfig; + Assert::same(['apart', 'closingApart'], $resolved->decisions['multiline.edges.condition']->value->getWords()); + + // a shape at least 70 % of the conditions have is written as the shape + $root = createProject('shape', ['src/a.php' => " " json_encode($autoload, JSON_THROW_ON_ERROR); + + // psr-4 with a prefix on several paths and one that is not there, a test suite only the convention knows + $root = createProject('scope-psr4', [ + 'composer.json' => $composer([ + 'autoload' => ['psr-4' => ['App\\' => 'ModuleFront', 'App\\Api\\' => ['ModuleApi/', 'gone']]], + 'autoload-dev' => ['psr-4' => ['App\\Tests\\' => 'test/']], + ]), + 'ModuleFront/a.php' => $php, + 'ModuleApi/a.php' => $php, + 'test/a.php' => $php, + 'www.admin/index.php' => $php, + 'cron/a.php' => $php, + 'assets/a.php' => $php, + ]); + Assert::same(['ModuleApi', 'ModuleFront', 'cron', 'test', 'www.admin'], Proposal::findPaths($root)); + + // a classmap names directories and single files, and the files section names no scope at all + $root = createProject('scope-classmap', [ + 'composer.json' => $composer(['autoload' => ['classmap' => ['src/', 'src/helpers.php'], 'files' => ['bootstrap.php']]]), + 'src/a.php' => $php, + 'src/helpers.php' => $php, + 'bootstrap.php' => $php, + 'tests/a.php' => $php, + ]); + Assert::same(['src', 'tests'], Proposal::findPaths($root)); + + // a path inside another one is already in the scope, whichever of the two named it + $root = createProject('scope-nested', [ + 'composer.json' => $composer(['autoload' => ['psr-4' => ['App\\' => 'ModuleFront/', 'App\\Sub\\' => 'ModuleFront/Sub']]]), + 'ModuleFront/a.php' => $php, + 'ModuleFront/Sub/a.php' => $php, + ]); + Assert::same(['ModuleFront'], Proposal::findPaths($root)); + + $root = createProject('scope-app', [ + 'composer.json' => $composer(['autoload' => ['psr-0' => ['' => 'app/Model']]]), + 'app/Model/a.php' => $php, + 'app/bootstrap.php' => $php, + ]); + Assert::same(['app'], Proposal::findPaths($root)); + + // a dot segment names the same directory as the path without it, and only resolved do the two compare as one + $root = createProject('scope-dots', [ + 'composer.json' => $composer(['autoload' => ['psr-4' => ['App\\' => './src', 'App\\Api\\' => 'app/../ModuleApi']]]), + 'src/a.php' => $php, + 'ModuleApi/a.php' => $php, + ]); + Assert::same(['ModuleApi', 'src'], Proposal::findPaths($root)); + + // an autoload pointing at the root itself is the whole scope + $root = createProject('scope-root', [ + 'composer.json' => $composer(['autoload' => ['psr-4' => ['App\\' => '']]]), + 'src/a.php' => $php, + ]); + Assert::same(['.'], Proposal::findPaths($root)); + + // the composer.json of a directory above names paths of another project, which are none of this scope + $root = createProject('scope-above/package', [ + '../composer.json' => $composer(['autoload' => ['psr-4' => ['App\\' => 'src']]]), + '../src/a.php' => $php, // the src of the project above, which exists and is still none of this scope + 'src/a.php' => $php, + ]); + Assert::same(['src'], Proposal::findPaths($root)); + + // without a composer.json the convention is the whole answer + $root = createProject('scope-conventional', ['src/a.php' => $php, 'tests/a.php' => $php, 'storage/a.php' => $php]); + Assert::same(['src', 'tests'], Proposal::findPaths($root)); +}); + + +test('without the usual directories the root itself is the scope', function () use ($tabbed) { + $root = createProject('flat', ['A.php' => $tabbed]); + [$code] = runInit($root); + Assert::same(0, $code); + Assert::contains("paths:\n\t- .\n", (string) file_get_contents("$root/dresscode.neon")); +}); + + +test('the sample is every k-th file, the same for the same tree, without a file too large or generated', function () { + $files = []; + foreach (range(1, 400) as $i) { + $files[sprintf('src/f%03d.php', $i)] = "files)); // every second of the 400, less the two + Assert::same(1, $sample->oversized); + Assert::same(1, $sample->generated); + Assert::same('src/f001.php', $sample->files[0]); + Assert::same('src/f003.php', $sample->files[1]); + Assert::notContains('src/f007.php', $sample->files); + Assert::notContains('src/f013.php', $sample->files); + Assert::same($sample->files, FileSample::pick($root, $scope)->files); + + // the budget of bytes is the cost of the measure, and it holds whatever the count allows + $files = []; + foreach (range(1, 30) as $i) { + $files[sprintf('src/g%02d.php', $i)] = "files)); // 22 * 90 kB is the last that fits in 2 MB + Assert::same(8, $sample->oversized); +}); diff --git a/tests/DressCode/Measuring/Survey.phpt b/tests/DressCode/Measuring/Survey.phpt new file mode 100644 index 000000000..e2eceeb45 --- /dev/null +++ b/tests/DressCode/Measuring/Survey.phpt @@ -0,0 +1,90 @@ +value = $values->get('project.variable')->getText(); + } + + + public function getVisitedNodes(): array + { + return [VariableNode::class]; + } + + + public function enter(Node|Token $node, RuleContext $context): void + { + if (!$node instanceof VariableNode || !$node->name instanceof Token) { + return; + } elseif ($node->name->text === '$boom' && $this->value === 'b') { + throw new RuntimeException('boom'); + } elseif ($node->name->text !== '$' . $this->value) { + $context->report($node, "Not \$$this->value."); + } + } +} + + +$root = createTempDir('survey'); +file_put_contents("$root/a.php", " new Profile(decisions: ['project' => ['variable' => 'a']]), + 'b' => new Profile(decisions: ['project' => ['variable' => 'b']]), +]; + + +test('a file the rule fails in under one value agrees with none of them', function () use ($survey, $values) { + $measurement = $survey->measureFiles(SurveyedRule::class, $values); + Assert::same(['a' => 1, 'b' => 1], $measurement->agreeing); + Assert::same(2, $measurement->opportunities); + Assert::same('a 50%, b 50% of 2 files, 1 failing file left out', $measurement->describe()); +}); + + +test('the price of a configuration tells the files a rule fails in apart from the unchanged ones', function () use ($survey) { + Assert::same([0, 1], $survey->countChanged(new Config(rules: [SurveyedRule::class], decisions: ['project' => ['variable' => 'b']]))); +}); + + +test('nor are its places counted for any of them', function () use ($survey, $values) { + $measurement = $survey->measurePlaces(SurveyedRule::class, $values, 'variables'); + Assert::same(['a' => 1, 'b' => 1], $measurement->agreeing); + Assert::same(2, $measurement->opportunities); + Assert::same(1, $measurement->failed); +}); + + +test('a survey narrowed to a count takes that many files spread over the sample, a smaller sample whole', function () { + $files = array_map(fn(int $i) => "f$i.php", range(0, 249)); + $survey = new Survey('/project', $files, new Config); + Assert::same(100, $survey->narrow(100)->countFiles()); + Assert::same(250, $survey->narrow(300)->countFiles()); + Assert::same(3, new Survey('/project', ['a.php', 'b.php', 'c.php'], new Config)->narrow(100)->countFiles()); +}); diff --git a/tests/DressCode/Reporters/reporters.phpt b/tests/DressCode/Reporters/reporters.phpt index df797246e..e36936e6d 100644 --- a/tests/DressCode/Reporters/reporters.phpt +++ b/tests/DressCode/Reporters/reporters.phpt @@ -151,7 +151,7 @@ test('console: the refused risky fixes are counted by their risk, with the advic TypeAnalysisStatus::Available, false, '2 risky fixes wait, the type is unknown: set `typeAnalysis: phpstan`.', - 'list what the namespaces declare under `namespaces`, or set `nameResolution: certain` if they declare nothing', + 'run `dresscode init`, which lists what the namespaces declare, or set `nameResolution: certain` if they declare nothing', ], [ TypeAnalysisStatus::Enabled, @@ -163,7 +163,7 @@ test('console: the refused risky fixes are counted by their risk, with the advic TypeAnalysisStatus::Unavailable, false, '2 risky fixes wait, the type is unknown: check them with `fix --ask-risky`.', - 'list what the namespaces declare under `namespaces`, or set `nameResolution: certain` if they declare nothing', + 'run `dresscode init`, which lists what the namespaces declare, or set `nameResolution: certain` if they declare nothing', ], ] as [$typeAnalysis, $listed, $advice, $names]) { $stream = memory(); From 3b57c989b887764f7f91928beb53197de0a9f0a6 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 13 Oct 2026 14:00:13 +0200 Subject: [PATCH 05/13] cli: upgrade, the code upgraded to the versions the project requires, in the style the project keeps dresscode upgrade lays the set deprecations, with --modernize also modernizations, over the presets the configuration uses and beneath its own decisions and overrides, so that what the project keeps stays kept, lets in the plugins of the packages of upgrading data, adds the types of the code and fixes. Without a configuration file it takes the directory as a whole and no style. With one it keeps the scope, the target, the types, fixRisky, the baseline and the maps, and of the style only the decisions the code keeps: a check without the types and the sets runs first, every decision it finds violated anywhere is left out, and the run is narrowed by only to the rest, the sets and the packages whose data reach the project, which a worker gets through --only. It takes --only, --fix-risky and --ask-risky, and the summary names upgrade --ask-risky for the risky fixes left. The upgrade goes to the lowest versions of PHP and of the libraries the project allows; the console says which libraries it takes, which packages are installed newer than that, whose declarations then say nothing of what it deprecates, advising composer update --prefer-lowest, and, where it rewrote a file, to run it again. --- docs/internals.md | 4 + readme.md | 7 +- src/Config/ConfigResolver.php | 14 +- src/Config/PackageDiscovery.php | 17 ++ src/Config/ProjectPackages.php | 44 +++- src/Config/ResolvedProject.php | 2 + src/Config/RunnerFactory.php | 21 +- src/Console/CommandContext.php | 6 +- src/Console/Commands/CheckCommand.php | 207 +++++++++++++++++- src/Reporters/ConsoleReporter.php | 8 +- tests/DressCode/Console/Application.phpt | 126 ++++++++++- .../Console/fixtures/upgrade-plugin.php | 39 ++++ 12 files changed, 465 insertions(+), 30 deletions(-) create mode 100644 tests/DressCode/Console/fixtures/upgrade-plugin.php diff --git a/docs/internals.md b/docs/internals.md index ea8d22913..837649d44 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -305,6 +305,10 @@ What the namespaces of the project declare is a property of the project too, and A certain resolution rests on lists that must stay complete, so the key turns on their guard, `NoUnlistedNamespacedDeclarationsRule`, whose decisions are the facts `namespaces.functions` and `namespaces.constants`: it reports a declaration missing from them under the path of the list, `--only` never narrows it away, since a fix resting on the lists must not lose what keeps them true, and a file whose resolution ends up uncertain has no guard; a declaration the lists do not name is decidable in its file, a declaration inside a condition and a `define()` with its name written as a string among them (`NameResolver::findNamespacedDeclarations()`), and the lists can then go stale only through a path left out of the run or a package of the same namespace. What a package declares for the files a project writes into its namespace is said by the data of the package (`namespaces` of its upgrading file), which lies under the lists of every layer; `dresscode config` names the source of each. +### upgrade + +`dresscode upgrade` is `fix` for a project that only moves to a newer PHP or newer libraries, which it says by raising the lower bounds of its constraints: the set `deprecations` (with `--modernize` also `modernizations`) is laid over the presets the configuration uses and beneath its own decisions and overrides (`ConfigResolver::$laid`), so that what the project keeps stays kept in the upgrade too, and the command line lays the plugins of the packages of upgrading data, which no `use` has to name, and `typeAnalysis: phpstan` over it all, a run asking for the types refusing to go without PHPStan beside DressCode (`RunnerFactory::resolve()`), and the versions are those the constraints of the project allow, as for any run, PHP among them (`RunnerFactory::readProjectPackages()`). The preface says which packages are installed newer than the lowest version allowed, whose declarations then say nothing of what that one deprecates, advising `composer update --prefer-lowest`, and which packages of upgrading data are not required with a lower bound, so that their data are reported and never written (`ProjectPackages::findNewerInstalled()`, `isUnbounded()`). A project without a configuration file is the directory as a whole, `new Config(paths: ['.'])`, with no style. A project with one keeps its scope, target, types, `fixRisky`, baseline and maps, and of its style only the decisions its code keeps: a check without the types and without the sets runs first over the code as it is, every decision it finds violated anywhere is left out whatever its severity, and the run is narrowed by `only` to the decisions left, the sets and the packages whose data reach the project, for the maps of their plugins no set covers (`CheckCommand::findKeptDecisions()`), which a worker gets through `--only` instead of checking again; `--only` of the command line narrows the run instead, and `--fix-risky` and `--ask-risky` work as with `fix`. One run makes no fixed point over the files: where it rewrote a file, it advises running it again. + ## Writing a rule ### Messages diff --git a/readme.md b/readme.md index b472f1c3c..0f3c3bd49 100644 --- a/readme.md +++ b/readme.md @@ -424,9 +424,10 @@ None of this is a search and replace: an annotation became an attribute along wi of a parameter moved behind `??` and a flag became a named argument. And DressCode knows that `$form` is a form and `$this` a presenter, because it asks PHPStan about the types. -The set `deprecations` lets the data of the installed packages in, and naming the package in `use` brings -its rules too. Most rewrites ask PHPStan about the [types](#types-from-phpstan); without them only classes, -functions and annotations are fixed: +`vendor/bin/dresscode upgrade` makes the upgrade with no configuration: it takes the installed packages with +their data and their rules, asks PHPStan about the [types](#types-from-phpstan) and leaves the style of the +code as it is. To have every check watch for the old API too, name the package in `use` next to the set +`deprecations`; without the types only classes, functions and annotations are fixed: ```neon typeAnalysis: phpstan diff --git a/src/Config/ConfigResolver.php b/src/Config/ConfigResolver.php index 10e6513d4..2ab6e01df 100644 --- a/src/Config/ConfigResolver.php +++ b/src/Config/ConfigResolver.php @@ -51,6 +51,11 @@ public function __construct( private readonly string $root = '.', /** @var list the packages whose plugins the project does not let in, so the maps of their rules are unknown */ private readonly array $pluginsSkipped = [], + /** + * @var list presets laid over those the configuration uses and beneath its own decisions and its overrides, + * so that the project keeps the last word, as `upgrade` lays its sets + */ + private readonly array $laid = [], ) { $this->rulesOfOverrides = new \WeakMap; } @@ -634,6 +639,10 @@ private function collectLayers(array $profiles): array $layers = $visited = $repeated = []; foreach ($profiles as [$layer, $profile]) { $this->collectUsed($profile, $layer, $layers, $visited, $repeated); + if ($layer->kind === LayerKind::Configuration && $this->laid !== []) { + $this->collectUsed(new Profile(use: $this->laid), new Layer(LayerKind::Caller), $layers, $visited, $repeated); + } + $layers[] = [$layer, $profile]; } @@ -699,9 +708,12 @@ private function collectUsed(Profile $profile, Layer $source, array &$layers, ar private function warnRepeated(array $repeated): void { foreach ($repeated as [$layer, $laidBy, $namedBy]) { - // what a preset uses is not the project's to change, and the command line reaches what an override does not + // what a preset or the caller lays is not the project's to change, and the command line reaches what an override + // does not if ( $namedBy->kind !== LayerKind::Preset + && $namedBy->kind !== LayerKind::Caller + && $laidBy->kind !== LayerKind::Caller && !($laidBy->kind === LayerKind::Override && $namedBy->kind === LayerKind::CommandLine) ) { $this->warnings['repeated ' . $layer->describe() . ' ' . $namedBy->describe()] = ucfirst($namedBy->formatReference()) diff --git a/src/Config/PackageDiscovery.php b/src/Config/PackageDiscovery.php index 7e5328fb2..070f1700e 100644 --- a/src/Config/PackageDiscovery.php +++ b/src/Config/PackageDiscovery.php @@ -135,6 +135,23 @@ public static function readPhpData(ProjectPackages $project): ?UpgradingData } + /** + * The packages shipping upgrading files, with the data of each. + * @return array> + */ + public function getDataPackages(): array + { + $res = []; + foreach ($this->upgradingData as $data) { + if ($data->layer->name !== null && $data->layer->package !== null) { + $res[$data->layer->package][] = $data; + } + } + + return $res; + } + + /** * The upgrading files of a package, relative to where it lies: every NEON file of the directory * `extra.dresscode.upgrading-dir` names, in the order of their names. diff --git a/src/Config/ProjectPackages.php b/src/Config/ProjectPackages.php index 720669bda..ff8fdd4bf 100644 --- a/src/Config/ProjectPackages.php +++ b/src/Config/ProjectPackages.php @@ -196,26 +196,36 @@ public function findVersion(string $package): ?string /** - * The directories of the installed packages the project supports from an older minor version than the installed one, - * where a declaration may say what the oldest version does not, a deprecation and its replacement above all. - * @return list + * The installed packages the project supports from an older minor version than the installed one, where a declaration + * may say what the oldest version does not, a deprecation and its replacement above all. + * @return array package => the version installed and the lowest one supported */ - public function findNewerInstalledPaths(): array + public function findNewerInstalled(): array { - $paths = []; - foreach ($this->installed as $package => ['version' => $installed, 'path' => $path]) { + $newer = []; + foreach ($this->installed as $package => ['version' => $installed]) { $lowest = $this->findVersion($package); if ( - $path !== null - && $installed !== null + $installed !== null && $lowest !== null && version_compare(self::toMinorVersion($lowest), self::toMinorVersion($installed), '<') ) { - $paths[] = $path; + $newer[$package] = [$installed, $lowest]; } } - return $paths; + return $newer; + } + + + /** + * The directories of those packages. + * @return list + */ + public function findNewerInstalledPaths(): array + { + $paths = array_map(fn(string $package) => $this->installed[$package]['path'], array_keys($this->findNewerInstalled())); + return array_values(array_filter($paths, fn(?string $path) => $path !== null)); } @@ -228,8 +238,7 @@ public function findUnboundedInstalledPaths(): array { $paths = []; foreach ($this->installed as $package => ['path' => $path]) { - $constraint = $this->findConstraint($package); - if ($path !== null && $constraint !== null && Versions::findLowestVersion($constraint) === null) { + if ($path !== null && $this->isUnbounded($package)) { $paths[] = $path; } } @@ -238,6 +247,17 @@ public function findUnboundedInstalledPaths(): array } + /** + * Whether the project supports the package from no particular version, not requiring it or requiring it without + * a lower bound. + */ + public function isUnbounded(string $package): bool + { + $constraint = $this->findConstraint($package); + return $constraint !== null && Versions::findLowestVersion($constraint) === null; + } + + /** * The first of the required packages the project does not meet; null where it meets every one. * @param array $required package => Composer constraint diff --git a/src/Config/ResolvedProject.php b/src/Config/ResolvedProject.php index 0dbfcb131..3d8bcb42a 100644 --- a/src/Config/ResolvedProject.php +++ b/src/Config/ResolvedProject.php @@ -45,6 +45,8 @@ public function __construct( private ConfigResolver $resolver, /** the versions the code is written for unless an override says another */ private string $target, + /** @var list presets laid over those of the configuration and beneath its decisions and overrides */ + public array $laid = [], ) { } diff --git a/src/Config/RunnerFactory.php b/src/Config/RunnerFactory.php index 24e7e5544..463fa2409 100644 --- a/src/Config/RunnerFactory.php +++ b/src/Config/RunnerFactory.php @@ -38,15 +38,23 @@ public function __construct( * override included. * @param ?Profile $commandLine laid over the configuration and its overrides, as --use is * @param ?list $only decisions, sections, presets and classes of rules the run is narrowed to + * @param list $laid presets laid over those of the configuration and beneath its decisions and overrides * @throws ConfigurationException */ - public function resolve(Config $config, string $root, ?Profile $commandLine = null, ?array $only = null): ResolvedProject + public function resolve( + Config $config, + string $root, + ?Profile $commandLine = null, + ?array $only = null, + array $laid = [], + ): ResolvedProject { [$target, $source] = $this->getPhpTarget($config, $root); $project = $this->readProjectPackages($config, $root, $target); $packages = PackageDiscovery::discover($project); [$packagePlugins, $skipped, $unnamed] = $this->admitPackagePlugins( $packages->plugins, + array_keys($packages->getDataPackages()), $config, $commandLine, $project->rootName, @@ -68,7 +76,7 @@ public function resolve(Config $config, string $root, ?Profile $commandLine = nu ); } - $resolver = new ConfigResolver($this->registry, $packages->upgradingData, $project, $typesAvailable, $root, $skipped); + $resolver = new ConfigResolver($this->registry, $packages->upgradingData, $project, $typesAvailable, $root, $skipped, $laid); $resolved = $resolver->resolve($config, $target, [], $commandLine, $only); $missing = array_map( fn(string $package) => $package === $project->rootName @@ -97,6 +105,7 @@ public function resolve(Config $config, string $root, ?Profile $commandLine = nu $only, $resolver, $target, + $laid, ); } @@ -243,9 +252,10 @@ private function isPhpStanInstalled(): bool * without it, or the plugin in `use`, its own package needing no name, the others skipped with a warning; a package * named makes its name no preset. * @param array> $plugins package => its plugin + * @param list $carriers the packages shipping upgrading data, which `upgrade` lets in by itself * @return array{list>, list, list} the plugins, the packages skipped, the warnings */ - private function admitPackagePlugins(array $plugins, Config $config, ?Profile $commandLine, ?string $rootName): array + private function admitPackagePlugins(array $plugins, array $carriers, Config $config, ?Profile $commandLine, ?string $rootName): array { $named = [ ...$config->use, @@ -259,6 +269,11 @@ private function admitPackagePlugins(array $plugins, Config $config, ?Profile $c if ($package === $rootName || array_any([$package, $short, $plugin], fn(string $name) => in_array($name, $named, true))) { $admitted[] = $plugin; $this->registry->registerPluginPackage($package, $plugin); + } elseif (in_array($package, $carriers, true)) { + $skipped[] = $package; + $warnings[] = "Package `$package` brings rules that run only where `use` names it" + . ($short === $package ? '' : ", as `$short`") + . ', and in `dresscode upgrade`; skipped, with what its upgrading files set for them.'; } else { $skipped[] = $package; $warnings[] = "Package `$package` brings the plugin `$plugin`, which loads only where `use` names the package or the plugin; skipped."; diff --git a/src/Console/CommandContext.php b/src/Console/CommandContext.php index a2722c899..618fcbc0b 100644 --- a/src/Console/CommandContext.php +++ b/src/Console/CommandContext.php @@ -64,16 +64,17 @@ public function resolveProject(ParseResult $args, ConfigOptions $options, array /** * The configuration, the root directory, the file it came from, and the layer the command line lays over them. + * @param ?Config $default the configuration of a command that runs without a file * @return array{config: Config, root: string, file: ?string, commandLine: ?Config} * @throws UsageException */ - public function loadConfig(ParseResult $args, ConfigOptions $options): array + public function loadConfig(ParseResult $args, ConfigOptions $options, ?Config $default = null): array { [$config, $root, $file] = Loader::load( $options->config === null ? null : Helpers::toAbsolutePath($options->config, $this->workingDirectory), $this->workingDirectory, // without a configuration file the run has only what the command line uses - $this->defaultConfig ?? ($options->use ? new Config : null), + $this->defaultConfig ?? $default ?? ($options->use ? new Config : null), ); return ['config' => $config, 'root' => $root, 'file' => $file, 'commandLine' => $this->readCommandLine($args, $options)]; } @@ -139,6 +140,7 @@ public function writeHeader(?string $configFile, ResolvedProject $resolution): v [...$config->plugins, ...$commandLine instanceof Config ? $commandLine->plugins : []], ), ...$config->use, + ...$resolution->laid, ...$commandLine->use ?? [], ]; $this->out->writeLine($this->out->color('gray', 'Config ') . ($configFile === null diff --git a/src/Console/Commands/CheckCommand.php b/src/Console/Commands/CheckCommand.php index f36f00bb1..ba4399b4a 100644 --- a/src/Console/Commands/CheckCommand.php +++ b/src/Console/Commands/CheckCommand.php @@ -7,8 +7,11 @@ namespace DressCode\Console\Commands; -use DressCode\{Config, Reporter, Reporters}; -use DressCode\Config\{Loader, PluginRegistry, ResolvedProject}; +use DressCode\{Config, ConfigurationException, Reporter, Reporters}; +use DressCode\Config\{ + Loader, PackageDiscovery, PluginRegistry, ResolvedProject, RunnerFactory, UpgradingData, + Versions, +}; use DressCode\Console\{CommandContext, Markup, ProgressBar, RiskReview}; use DressCode\Engine\{Baseline, FileSummary, Helpers, LineRanges, Runner, RunResult, Worker, WorkerPool}; use Nette\CommandLine\{Attributes as Cli, Console, ParseResult, UsageException}; @@ -49,6 +52,47 @@ public function fix( } + /** + * @param list $fixRisky + * @param list $only + */ + #[Cli\Command('Upgrade the code to the lowest versions of PHP and of the libraries composer.json allows, after raising them: the set `deprecations` and the packages of upgrading data with the types of the code, in the style the project keeps')] + public function upgrade( + ParseResult $args, + RunOptions $run, + #[Cli\Flag('Also write what newer PHP offers, the set `modernizations`')] + ?bool $modernize, + #[Cli\Flag('Upgrade as usual, then ask about every risky fix left, one at a time with its diff, and make those accepted; needs an interactive terminal')] + ?bool $askRisky, + ConfigOptions $configuration, + #[Cli\Option( + 'Also make the fixes that may change what the code does, of every rule, or with `=name` of that decision, rule, preset or package; they are reported either way', + valueName: 'name', + valueOptional: true, + repeatable: true, + )] + array $fixRisky = [], + #[Cli\Option( + 'Only these: a package of upgrading data for its maps and the rules reading them, or a decision, a section, a rule or a preset', + valueName: 'name', + repeatable: true, + )] + array $only = [], + ): int + { + return $this->run( + $args, + $configuration, + $run, + new CheckOptions(fixRisky: $fixRisky, only: $only), + fix: true, + askRisky: (bool) $askRisky, + upgrade: $only, + modernize: (bool) $modernize, + ); + } + + #[Cli\Command('Write the violations a fix leaves into the baseline file of the configuration, once a fix changes nothing')] public function baseline(ParseResult $args, RunOptions $run, ConfigOptions $configuration): int { @@ -59,6 +103,7 @@ public function baseline(ParseResult $args, RunOptions $run, ConfigOptions $conf /** * @param ?CheckOptions $check what check and fix take beyond the run, null for the baseline * @param bool $generate the baseline command writes the configured baseline, it never reads it + * @param ?list $upgrade the run of `upgrade`, with the decisions a worker runs; null for another command */ private function run( ParseResult $args, @@ -68,6 +113,8 @@ private function run( bool $fix = false, bool $generate = false, bool $askRisky = false, + ?array $upgrade = null, + bool $modernize = false, ): int { // `fix --stdin` is the formatter of an editor, which reads the text and the exit code alone @@ -88,7 +135,9 @@ private function run( } } - [$factory, $resolution, $configFile] = $this->context->resolveProject($args, $configuration, $check->only ?? []); + [$factory, $resolution, $configFile] = $upgrade === null + ? $this->context->resolveProject($args, $configuration, $check->only ?? []) + : $this->resolveUpgrade($args, $configuration, $modernize, $upgrade); [$config, $root] = [$resolution->config, $resolution->root]; $fixRisky = in_array(true, $check->fixRisky ?? [], true); // a bare `--fix-risky`, the names given go to the configuration if ($run->worker !== null) { // the parent keeps the cache; the baseline decides what is reported @@ -188,6 +237,10 @@ private function run( } $this->context->writeHeader($configFile, $resolution); + if ($upgrade !== null) { + self::writeUpgradedVersions($out, $resolution); + } + $out->setStatus($out->color('gray', $fix ? 'Fixing ' : 'Checking ') . 'looking for files…'); } @@ -220,7 +273,13 @@ private function run( // a worker costs about the processing of a few files to start, so by default one for every four files at most $jobs = $run->jobs ?? max(1, min(WorkerPool::detectCpuCount(), intdiv(count($files), 4))); - $command = $jobs > 1 && $files ? $this->buildWorkerCommand($args, $configuration, $run, $check) : null; + $command = $jobs > 1 && $files + ? $this->buildWorkerCommand($args, $configuration, $run, $check, $upgrade === null ? [] : [ + ...$modernize ? ['--modernize'] : [], + // what the parent narrowed the run to, unless `--only` did, which goes over as it is + ...$upgrade ? [] : array_map(fn(string $name) => "--only=$name", $resolution->only ?? []), + ]) + : null; $workers = $command ? new WorkerPool($command, $jobs, $this->context->workingDirectory, warmFirst: $runner->warmUp !== null) : null; @@ -244,17 +303,143 @@ private function run( $runner, $resolution, $run->paths, + $upgrade === null ? 'fix' : 'upgrade', ); $result = $runner->run($files, $fix, $reporter, $workers, $onProgress, $maxWarnings, $lines); + if ($upgrade !== null && $preface && $result->countChangedFiles()) { + $out->writeLine('Run `dresscode upgrade` again: the code it rewrote may hold more to upgrade.'); + } + return $askRisky ? $this->review($runner, $result, $files, $root, $maxWarnings, $factory->registry) : $result->getExitCode(); + } finally { $progress?->clear(); // an error must not be written into the bar } } + /** + * What `upgrade` upgrades by: the packages of upgrading data, those installed newer than the lowest version the + * project allows, whose declarations then say nothing of what that one deprecates, and those it allows in any version, + * whose data are reported and never written. + */ + private static function writeUpgradedVersions(Console $out, ResolvedProject $resolution): void + { + $project = $resolution->projectPackages; + $packages = array_diff( + array_unique(array_map(fn(array $data) => $data[0]->package, $resolution->upgradingData)), + [PackageDiscovery::Php], + ); + $out->writeLine( + $out->color('gray', 'Upgrading ') . (implode(', ', $packages) ?: 'PHP alone, no installed library has upgrading data'), + ); + + $newer = []; + foreach ($project->findNewerInstalled() as $package => [$installed, $lowest]) { + $newer[] = "$package " . Versions::formatVersion($installed) . " installed, $lowest required"; + } + + if ($newer !== []) { + $out->writeLine( + $out->color('gray', 'Lowest ') + . implode(', ', $newer) + . '; `composer update --prefer-lowest` lets the upgrade see what the lowest versions deprecate', + ); + } + + $unbounded = array_filter($packages, $project->isUnbounded(...)); + if ($unbounded !== []) { + $out->writeLine( + $out->color('gray', 'Reported ') + . implode(', ', $unbounded) + . ': not required with a lower bound, so the upgrading data are reported, never written', + ); + } + } + + + /** + * The project of `upgrade`: its configuration, or the directory as a whole where it has none, with the sets of the + * upgrade laid over the presets it uses and beneath its own decisions and overrides, and the plugins of the packages + * of upgrading data with the types of the code over it all, narrowed by `--only`, or where it has a configuration to + * the decisions its style keeps and to the packages whose data reach the project. + * @param list $only those of `--only`, which the parent gives a worker too + * @return array{RunnerFactory, ResolvedProject, ?string} + */ + private function resolveUpgrade(ParseResult $args, ConfigOptions $configuration, bool $modernize, array $only): array + { + ['config' => $config, 'root' => $root, 'file' => $file, 'commandLine' => $commandLine] + = $this->context->loadConfig($args, $configuration, new Config(paths: ['.'])); + $factory = new RunnerFactory; + $discovered = PackageDiscovery::discover($factory->readProjectPackages($config, $root)); + $packages = $discovered->getDataPackages(); + $sets = $modernize ? ['deprecations', 'modernizations'] : ['deprecations']; + $layer = new Config( + use: [ + ...$commandLine->plugins ?? [], + ...$commandLine->use ?? [], + ...array_keys(array_intersect_key($discovered->plugins, $packages)), + ], + fixRisky: $commandLine->fixRisky ?? [], + severity: $commandLine->severity ?? [], + typeAnalysis: 'phpstan', + decisions: $commandLine->decisions ?? [], + ); + if ($file !== null && !$only) { + // a package for the maps and the rules of its plugin, which no set covers + $reached = array_filter( + $packages, + fn(array $files) => array_any($files, fn(UpgradingData $data) => $data->maps !== [] || $data->requirements !== []), + ); + $only = [...$this->findKeptDecisions($config, $root, $file, $commandLine), ...$sets, ...array_keys($reached)]; + } + + return [$factory, $factory->resolve($config, $root, $layer, $only ?: null, laid: $sets), $file]; + } + + + /** + * The decisions of the configuration the code keeps everywhere: those it makes, the types left out, less each one + * the code violates anywhere, whatever its severity. + * @return list + */ + private function findKeptDecisions(Config $config, string $root, string $file, ?Config $commandLine): array + { + try { + $factory = new RunnerFactory(phpstanInstalled: false); + $resolution = $factory->resolve($config, $root, $commandLine); + } catch (ConfigurationException) { // a decision of the project needs the types, so the check takes them + $factory = new RunnerFactory; + $resolution = $factory->resolve($config, $root, $commandLine); + } + + $runner = $factory->createRunner($resolution, configFile: $file); + $result = $runner->run($runner->findFiles($config->paths, configured: $config->paths), false, new Reporters\NullReporter); + $violated = []; + foreach ($result->files as $summary) { + foreach ($summary->violations as $violation) { + $violated[$violation->decision] = true; + } + } + + $kept = []; + foreach ($resolution->resolvedConfig->decisions as $path => $decision) { + if ( + $decision->inactive === null + && $decision->decision->isRequirement() + && !$decision->value->isKept() + && !isset($violated[$path]) + ) { + $kept[] = $path; + } + } + + return $kept; + } + + /** * The decisions the file as it was before an edit broke, which the run then leaves as the file writes them. * @return list @@ -423,9 +608,16 @@ private static function findCommonDirectory(array $files): string * The command line of a worker: the same PHP, the same command and configuration; the paths come over the * connection. Null where there is no PHP to start one with, and the run stays in the process. * @param ?CheckOptions $check null for the baseline + * @param list $extra what the parent decided for the run beyond its command line * @return ?list */ - private function buildWorkerCommand(ParseResult $args, ConfigOptions $configuration, RunOptions $run, ?CheckOptions $check): ?array + private function buildWorkerCommand( + ParseResult $args, + ConfigOptions $configuration, + RunOptions $run, + ?CheckOptions $check, + array $extra = [], + ): ?array { $php = WorkerPool::buildPhpCommand(); if ($php === null) { @@ -437,6 +629,7 @@ private function buildWorkerCommand(ParseResult $args, ConfigOptions $configurat $this->context->scriptFile, (string) $args->command->name, '--no-color', + ...$extra, ]; foreach ([ '--config' => (array) $configuration->config, @@ -526,9 +719,11 @@ private static function getDefaultBaselineName(?string $configFile): string /** + * @param bool $diff the console shows the fixes as a unified diff * @param resource $stream the same place as the console; a machine-readable format is no text of a console * @param string $root the paths of the results are relative to it * @param list $paths the paths as the command line gave them + * @param string $command the command whose `--ask-risky` decides the risky fixes left */ private function createReporter( bool $diff, @@ -540,6 +735,7 @@ private function createReporter( Runner $runner, ResolvedProject $resolution, array $paths, + string $command = 'fix', ): Reporter { return match ($format) { @@ -573,6 +769,7 @@ private function createReporter( root: $root, cwd: Helpers::canonicalizePath($this->context->workingDirectory), findDecisionUrl: $registry->findUrl(...), + command: $command, ), }; } diff --git a/src/Reporters/ConsoleReporter.php b/src/Reporters/ConsoleReporter.php index 257dd1727..229810ab5 100644 --- a/src/Reporters/ConsoleReporter.php +++ b/src/Reporters/ConsoleReporter.php @@ -54,6 +54,8 @@ public function __construct( private readonly string $cwd = '', /** @var ?\Closure(string): ?string the path of a decision => the address of its page */ private readonly ?\Closure $findDecisionUrl = null, + /** the command whose `--ask-risky` decides the risky fixes left */ + private readonly string $command = 'fix', ) { } @@ -275,11 +277,11 @@ private function formatRefused(Risk $risk, RunResult $result): string : 'run `dresscode init`, which lists what the namespaces declare, or set `nameResolution: certain` if they declare nothing'), Risk::TypeUnknown => match ($this->typeAnalysis) { TypeAnalysisStatus::Available => "$wait, the type is unknown: set `typeAnalysis: phpstan`", - TypeAnalysisStatus::Enabled => "$wait, not even the types tell: check $them with `fix --ask-risky`", - TypeAnalysisStatus::Unavailable => "$wait, the type is unknown: check $them with `fix --ask-risky`", + TypeAnalysisStatus::Enabled => "$wait, not even the types tell: check $them with `$this->command --ask-risky`", + TypeAnalysisStatus::Unavailable => "$wait, the type is unknown: check $them with `$this->command --ask-risky`", }, Risk::BehaviorChanges => ($count === 1 ? '1 risky fix changes' : "$count risky fixes change") - . " what the code does: decide $them with `fix --ask-risky`, or accept $decisions in `fixRisky`", + . " what the code does: decide $them with `$this->command --ask-risky`, or accept $decisions in `fixRisky`", }; $docs = match ($risk) { Risk::TypeUnknown => 'types#enable', diff --git a/tests/DressCode/Console/Application.phpt b/tests/DressCode/Console/Application.phpt index 7ad66ef59..be0cbf829 100644 --- a/tests/DressCode/Console/Application.phpt +++ b/tests/DressCode/Console/Application.phpt @@ -4,6 +4,7 @@ use DressCode\Console\Application; use DressCode\{Decision, Domain, NodeRule, Plugin, PluginManifest, Risk, RuleContext, RuleInfo, Stage}; use DressCode\Rules\Classes\FinalForInternalClassRule; use DressCode\Rules\Functions\StaticForClosureWithoutThisRule; +use Nette\Utils\FileSystem; use PhpSyntax\{Node, Token}; use PhpSyntax\Nodes\Expression\VariableNode; use Tester\Assert; @@ -150,7 +151,7 @@ function runApp(string $root, array $args, string $stdin = '', bool $xdebug = fa $in = fopen('php://memory', 'w+') ?: throw new RuntimeException; fwrite($in, $stdin); rewind($in); - if (in_array($args[0] ?? null, ['check', 'fix', 'baseline'], strict: true) && !in_array('--jobs', $args, strict: true)) { + if (in_array($args[0] ?? null, ['check', 'fix', 'baseline', 'upgrade'], strict: true) && !in_array('--jobs', $args, strict: true)) { $args = [...$args, '--jobs', '1']; // the rules of this file do not exist in a worker process } @@ -422,6 +423,129 @@ test('--lines takes the lines of one file in the agent format, anything else is }); +test('upgrade rewrites the code in no style without a configuration, and with one in the style its code keeps', function () { + $code = "=8.2"}}'); + file_put_contents("$root/src/a.php", $code); + [$exit, $out] = runApp($root, ['upgrade', '--no-color']); + Assert::same(0, $exit); + Assert::same($upgraded, file_get_contents("$root/src/a.php")); + Assert::match( + "%A%Config none, using deprecations\n%A%Upgrading PHP alone, no installed library has upgrading data\n%A%Run `dresscode upgrade` again: %a%\n", + $out, + ); + FileSystem::delete($root); + + // the code writes double quotes, so the quotes of the standard are left out, in the workers too + $root = createTempDir('upgrade-configured'); + mkdir("$root/src"); + file_put_contents("$root/composer.json", '{"require": {"php": ">=8.2"}}'); + file_put_contents("$root/dresscode.neon", "use: [nette]\npaths: [src]\n"); + foreach (['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] as $name) { + file_put_contents("$root/src/$name.php", $code); + } + + [$exit] = runApp($root, ['upgrade', '--no-color', '--jobs', '2']); + Assert::same(0, $exit); + Assert::same($upgraded, file_get_contents("$root/src/a.php")); + Assert::same($upgraded, file_get_contents("$root/src/h.php")); + Assert::same(1, runApp($root, ['check', '--no-color'])[0]); +}); + + +test('upgrade keeps what the project keeps, in an override too, the sets lying beneath its decisions', function () { + $code = "=8.2"}}'); + file_put_contents( + "$root/dresscode.neon", + "paths: [src]\noverrides:\n\t-\n\t\tpaths: [src/kept.php]\n\t\tupgrading: {api: {replacedCalls: {'curl_close(\$handle)': keep}}}\n", + ); + file_put_contents("$root/src/kept.php", $code); + file_put_contents("$root/src/upgraded.php", $code); + + [$exit, , $err] = runApp($root, ['upgrade', '--no-color']); + Assert::same(0, $exit); + Assert::same('', $err); + Assert::same($code, file_get_contents("$root/src/kept.php")); + Assert::same("=8.2", "acme/lib": "^3.0"}}'); + file_put_contents("$root/dresscode.neon", "paths: [src]\n"); + file_put_contents("$root/src/a.php", " [ + ['name' => 'acme/lib', 'version' => '3.2.0', 'version_normalized' => '3.2.0.0', 'install-path' => '../acme/lib'], + [ + 'name' => 'acme/tool', + 'version' => '1.0.0', + 'version_normalized' => '1.0.0.0', + 'install-path' => '../acme/tool', + 'extra' => ['dresscode' => ['upgrading-dir' => 'upgrading']], + ], + ]], JSON_THROW_ON_ERROR), + ); + + [, $out] = runApp($root, ['upgrade', '--no-color']); + Assert::match( + "%A%Upgrading acme/tool\nLowest acme/lib 3.2 installed, 3.0 required; `composer update --prefer-lowest` %a%\nReported acme/tool: not required with a lower bound, %a%\n%A%", + $out, + ); +}); + + +test('upgrade runs the rules of a package of upgrading data its maps turn on, which check runs only where use names it', function () { + require_once __DIR__ . '/fixtures/upgrade-plugin.php'; + $root = createTempDir('upgrade-plugin'); + mkdir("$root/src"); + file_put_contents("$root/composer.json", '{"require": {"php": ">=8.2", "acme/upgrade": "^1.0"}}'); + file_put_contents("$root/dresscode.neon", "paths: [src]\n"); + file_put_contents("$root/src/a.php", " [[ + 'name' => 'acme/upgrade', + 'version' => '1.0.0', + 'version_normalized' => '1.0.0.0', + 'install-path' => '../acme/upgrade', + 'extra' => ['dresscode' => ['plugin' => 'UpgradePlugin', 'upgrading-dir' => 'upgrading']], + ]]], JSON_THROW_ON_ERROR), + ); + + // one warning for the plugin, none for the map of its rule + Assert::same( + "Warning: Package `acme/upgrade` brings rules that run only where `use` names it, and in `dresscode upgrade`; skipped, with what its upgrading files set for them.\n", + runApp($root, ['check', '--no-color'])[2], + ); + foreach ([[], ['--only', 'acme/upgrade']] as $only) { + [, $out, $err] = runApp($root, ['upgrade', '--no-color', ...$only]); + Assert::same('', $err); + Assert::contains('A variable of the upgrade.', $out); + } +}); + + test('fix --stdin is a formatter: the whole text on stdout, nothing else, and exit 0 whatever is left', function () use ($root) { file_put_contents( "$root/formatter.php", diff --git a/tests/DressCode/Console/fixtures/upgrade-plugin.php b/tests/DressCode/Console/fixtures/upgrade-plugin.php new file mode 100644 index 000000000..bb03ab4be --- /dev/null +++ b/tests/DressCode/Console/fixtures/upgrade-plugin.php @@ -0,0 +1,39 @@ +report($node, 'A variable of the upgrade.', fixable: false); + } +} + + +final class UpgradePlugin implements Plugin +{ + public function getManifest(): PluginManifest + { + return new PluginManifest(rules: [UpgradeVariableRule::class], section: 'acmeUpgrade'); + } +} From cfa53196e58ae9d126c3f8eefacf7660b0d05001 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 14 Oct 2026 06:15:04 +0200 Subject: [PATCH 06/13] upgrade: the upgrading data a library of the project needs installed beside DressCode A package of upgrading data says which packages it is about under extra.dresscode.packages, and UpgradingTester::collectPackageProblems() refuses a list that is not exactly the packages its upgrading files are about. upgrade learns of the packages from Packagist, asked once a day and remembered in the cache directory, for now of the vendor dresscode alone, and PackageDiscovery finds those installed beside DressCode by their type dresscode-plugin, not only those of the project. A project requiring one of their libraries itself, not one where it comes with another library, gets a missing package installed first: composer require into the installation of DressCode, or require --dev into the project where its lock holds DressCode, in a terminal after asking and without one only with --yes, otherwise it refuses with the command to run. The command then runs anew in a new process, whose autoloader knows the installed packages and the plugin dresscode/upgrade-nette brings. --- docs/internals.md | 4 +- docs/reference/api.md | 1 + src/Config/DataPackageCatalogue.php | 88 +++++++++++++++++++ src/Config/PackageDiscovery.php | 83 +++++++++++++++-- src/Config/ProjectPackages.php | 2 +- src/Config/RunnerFactory.php | 2 +- src/Console/Commands/CheckCommand.php | 64 +++++++++++++- src/Testing/UpgradingTester.php | 46 ++++++++++ .../Config/DataPackageCatalogue.phpt | 14 +++ tests/DressCode/Config/PackageDiscovery.phpt | 46 ++++++++++ tests/DressCode/Console/Application.phpt | 28 ++++++ tests/DressCode/Testing/UpgradingTester.phpt | 31 ++++++- 12 files changed, 395 insertions(+), 14 deletions(-) create mode 100644 src/Config/DataPackageCatalogue.php create mode 100644 tests/DressCode/Config/DataPackageCatalogue.phpt diff --git a/docs/internals.md b/docs/internals.md index 837649d44..2bb4f2a01 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -307,7 +307,7 @@ A certain resolution rests on lists that must stay complete, so the key turns on ### upgrade -`dresscode upgrade` is `fix` for a project that only moves to a newer PHP or newer libraries, which it says by raising the lower bounds of its constraints: the set `deprecations` (with `--modernize` also `modernizations`) is laid over the presets the configuration uses and beneath its own decisions and overrides (`ConfigResolver::$laid`), so that what the project keeps stays kept in the upgrade too, and the command line lays the plugins of the packages of upgrading data, which no `use` has to name, and `typeAnalysis: phpstan` over it all, a run asking for the types refusing to go without PHPStan beside DressCode (`RunnerFactory::resolve()`), and the versions are those the constraints of the project allow, as for any run, PHP among them (`RunnerFactory::readProjectPackages()`). The preface says which packages are installed newer than the lowest version allowed, whose declarations then say nothing of what that one deprecates, advising `composer update --prefer-lowest`, and which packages of upgrading data are not required with a lower bound, so that their data are reported and never written (`ProjectPackages::findNewerInstalled()`, `isUnbounded()`). A project without a configuration file is the directory as a whole, `new Config(paths: ['.'])`, with no style. A project with one keeps its scope, target, types, `fixRisky`, baseline and maps, and of its style only the decisions its code keeps: a check without the types and without the sets runs first over the code as it is, every decision it finds violated anywhere is left out whatever its severity, and the run is narrowed by `only` to the decisions left, the sets and the packages whose data reach the project, for the maps of their plugins no set covers (`CheckCommand::findKeptDecisions()`), which a worker gets through `--only` instead of checking again; `--only` of the command line narrows the run instead, and `--fix-risky` and `--ask-risky` work as with `fix`. One run makes no fixed point over the files: where it rewrote a file, it advises running it again. +`dresscode upgrade` is `fix` for a project that only moves to a newer PHP or newer libraries, which it says by raising the lower bounds of its constraints: the set `deprecations` (with `--modernize` also `modernizations`) is laid over the presets the configuration uses and beneath its own decisions and overrides (`ConfigResolver::$laid`), so that what the project keeps stays kept in the upgrade too, and the command line lays the plugins of the packages of upgrading data, which no `use` has to name, and `typeAnalysis: phpstan` over it all, a run asking for the types refusing to go without PHPStan beside DressCode (`RunnerFactory::resolve()`), and the versions are those the constraints of the project allow, as for any run, PHP among them (`RunnerFactory::readProjectPackages()`). The preface says which packages are installed newer than the lowest version allowed, whose declarations then say nothing of what that one deprecates, advising `composer update --prefer-lowest`, and which packages of upgrading data are not required with a lower bound, so that their data are reported and never written (`ProjectPackages::findNewerInstalled()`, `isUnbounded()`). A project without a configuration file is the directory as a whole, `new Config(paths: ['.'])`, with no style. A project with one keeps its scope, target, types, `fixRisky`, baseline and maps, and of its style only the decisions its code keeps: a check without the types and without the sets runs first over the code as it is, every decision it finds violated anywhere is left out whatever its severity, and the run is narrowed by `only` to the decisions left, the sets and the packages whose data reach the project, for the maps of their plugins no set covers (`CheckCommand::findKeptDecisions()`), which a worker gets through `--only` instead of checking again; `--only` of the command line narrows the run instead, and `--fix-risky` and `--ask-risky` work as with `fix`. One run makes no fixed point over the files: where it rewrote a file, it advises running it again. The upgrading data of a library come from a package that says which libraries it is about under `extra.dresscode.packages`, as `dresscode/upgrade-nette` says `nette/`; discovery also finds one installed beside DressCode rather than in the project, a package of the type `dresscode-plugin` with upgrading files. DressCode knows no such package itself: what Packagist offers, for now of the vendor `dresscode` alone until a package of another vendor can be trusted, is asked once a day and remembered in the cache directory (`DataPackageCatalogue`), and `UpgradingTester::collectPackageProblems()` refuses a list that is not exactly the packages the upgrading files are about. A project requiring one of those libraries itself, in `require` or `require-dev`, a library coming with another one not counting, gets the package installed first, `composer require` into the installation of DressCode, or `require --dev` into the project where its lock holds DressCode: in a terminal after asking, without one only with `--yes`, otherwise the run is refused with the command to run. The command then runs anew in a new process, whose autoloader knows the installed packages and the plugins they bring (`dresscode/upgrade-nette` brings one). ## Writing a rule @@ -341,6 +341,6 @@ Everything in such a message that is code stands in backticks, as in Markdown: a `Testing\RuleTester` runs a rule over `*.code` fixtures (`.expected` output beside them, `.violations` as `line: message` with the risk of a risky one on an indented line below it, `risky ` with `: ` where the rule gives one, the values of its decisions in a `// {"json"}` comment, a key without its section taking the one section of the rule, over those of a `values.neon` beside the fixtures, a run that allows risky fixes in a `// risky` one, the code and the fixed code run under the CLI of PHP in a `// run` one, which must print the same and exit alike, the targeted version in a `// php 8.4` one, the types turned off in a `// types off` one, the widest line in a `// lineLength 80` one (120 without it; `check()` takes a whole `Style` instead, whose line ending stays that of the code) and what the namespaces declare outside the fixture in `// namespacedFunctions`, `// namespacedConstants` and `// nameResolution certain`, all in the comments that open the fixture; without a version the rule is tested at the version of PHP it requires, else at `Config::DefaultPhpVersion`, never at the version of the interpreter running the tests) and, given `analyses:` in the shape of `Config::$analyses`, registers those of a plugin for the rule, then checks the contract: parent invariant, idempotence over the output, comments preserved unless `modifiesComments`, `dresscode:ignoreFile` honored, no silent mutation. -`Testing\UpgradingTester` is for a package that ships upgrading files: `collectProblems($file, $root, $packageRules)` reads a file the way a run would, in the project whose vendor holds the library, the rules the package ships itself known besides those of DressCode, and gives back sentences. A key under `extra.dresscode` of the nearest `composer.json` at or above the file that `PackageDiscovery` does not read is a problem too (a run only warns of it, the package may be newer than the tool). Every section, reached or not, has to be what the grammars of its maps accept, and what the sections come to at the installed version has to replace, in `replacedClasses` and `replacedMembers` and by a static method in `replacedFunctions`, by classes, members and methods that exist, asked of the loaded code (a function of a library is declared by a file loaded with its first use, so it is not looked up); the other maps are held to their grammar alone. A replacement the file replaces in its turn (`REALPATH`, then `Realpath`, then `RealPath`) is followed to its end, the way the passes of a run follow it, and one that leads back to where it started is a problem; an entry withdrawn with `keep` is thereby not checked, and one whose withdrawal was forgotten shows as a replacement that does not exist. What is replaced is not looked up, a library having usually removed it. The sentence of a `forbidden…` map is read as the end of the message it completes: in lower case unless it begins with a name, with its code in backticks, with no period, double quote or "should", and at most 160 characters, the backticks not counted; an entry of a `forbidden…` map that gives none (a project may leave it out) is a problem in the data of a package. `runSample($code, $root, $decisions)` is the other half: code written for the old API as the decisions given fix and report it, every section the installed version reaches applying, whatever the constraint of the project allows, the types taken from the PHPStan of the project, which reads the code from `temp/` of the root, and no result cache; it gives the `FileResult`, so that a package tests its data without reaching for the `@internal` factory of the run. +`Testing\UpgradingTester` is for a package that ships upgrading files: `collectProblems($file, $root, $packageRules)` reads a file the way a run would, in the project whose vendor holds the library, the rules the package ships itself known besides those of DressCode, and gives back sentences. A key under `extra.dresscode` of the nearest `composer.json` at or above the file that `PackageDiscovery` does not read is a problem too (a run only warns of it, the package may be newer than the tool). `collectPackageProblems($root)` holds `extra.dresscode.packages` of the package to exactly the packages its upgrading files are about, by which `upgrade` offers it to a project lacking it. Every section, reached or not, has to be what the grammars of its maps accept, and what the sections come to at the installed version has to replace, in `replacedClasses` and `replacedMembers` and by a static method in `replacedFunctions`, by classes, members and methods that exist, asked of the loaded code (a function of a library is declared by a file loaded with its first use, so it is not looked up); the other maps are held to their grammar alone. A replacement the file replaces in its turn (`REALPATH`, then `Realpath`, then `RealPath`) is followed to its end, the way the passes of a run follow it, and one that leads back to where it started is a problem; an entry withdrawn with `keep` is thereby not checked, and one whose withdrawal was forgotten shows as a replacement that does not exist. What is replaced is not looked up, a library having usually removed it. The sentence of a `forbidden…` map is read as the end of the message it completes: in lower case unless it begins with a name, with its code in backticks, with no period, double quote or "should", and at most 160 characters, the backticks not counted; an entry of a `forbidden…` map that gives none (a project may leave it out) is a problem in the data of a package. `runSample($code, $root, $decisions)` is the other half: code written for the old API as the decisions given fix and report it, every section the installed version reaches applying, whatever the constraint of the project allows, the types taken from the PHPStan of the project, which reads the code from `temp/` of the root, and no result cache; it gives the `FileResult`, so that a package tests its data without reaching for the `@internal` factory of the run. The fixtures of every rule run again under each standard with the sets beside it (`robustness.phpt`), which no fixture of one rule tries: the run must not fail, write code that does not parse or fail to settle, whatever the other rules make of the code. What no fixture catches, the runs over the corpus do: the whitespace fuzz damages the whitespace of a fixed file and requires the fixer to come back to it, and `name-meaning.phpt` runs the rules that write names and imports over the corpus and requires every name outside the imports to resolve to what it resolved to before, because an import moved into a statement writing its items differently, or dropped while something still uses it, leaves code that parses and round-trips and says nothing else. diff --git a/docs/reference/api.md b/docs/reference/api.md index 747291964..ce0b28482 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -774,6 +774,7 @@ final class TestFailure extends Exception implements Stringable, Throwable ```php final class UpgradingTester public static function collectProblems(string $file, string $root, list> $packageRules = []): list + public static function collectPackageProblems(string $root): list public static function runSample(string $code, string $root, array $decisions = [], string $name = 'sample'): DressCode\FileResult ``` diff --git a/src/Config/DataPackageCatalogue.php b/src/Config/DataPackageCatalogue.php new file mode 100644 index 000000000..94b3c76f7 --- /dev/null +++ b/src/Config/DataPackageCatalogue.php @@ -0,0 +1,88 @@ +> package => the libraries it is about, a whole vendor written `vendor/`; + * empty where Packagist cannot be reached and nothing is remembered + */ + public static function load(string $cacheDir): array + { + $file = "$cacheDir/data-packages.json"; + $cached = is_file($file) ? json_decode((string) file_get_contents($file), associative: true) : null; + $cached = is_array($cached) ? $cached : null; + if ($cached !== null && filemtime($file) > time() - self::MaxAge) { + return $cached; + } + + $fetched = self::fetch(); + try { + FileSystem::write($file, json_encode($fetched ?? $cached ?? [], JSON_THROW_ON_ERROR)); + if ($fetched === null) { + touch($file, time() - self::MaxAge + self::FailureAge); + } + } catch (\Throwable) { // a cache that cannot be written only means asking again + } + + return $fetched ?? $cached ?? []; + } + + + /** @return ?array> null where Packagist does not answer */ + private static function fetch(): ?array + { + $names = self::request(self::ListUrl)['packageNames'] ?? null; + if (!is_array($names)) { + return null; + } + + $res = []; + foreach (array_filter($names, is_string(...)) as $name) { + // the newest release, or the development version of a package with none + $metadata = self::request(sprintf(self::MetadataUrl, $name))['packages'][$name][0] + ?? self::request(sprintf(self::MetadataUrl, "$name~dev"))['packages'][$name][0] + ?? null; + $libraries = $metadata['extra']['dresscode']['packages'] ?? null; + if (is_array($libraries) && array_all($libraries, fn($library) => is_string($library))) { + $res[$name] = array_values($libraries); + } + } + + return $res; + } + + + /** @return ?array */ + private static function request(string $url): ?array + { + $context = stream_context_create(['http' => ['timeout' => 5, 'user_agent' => 'DressCode']]); + $json = @file_get_contents($url, context: $context); // @ a failure is no answer + $data = $json === false ? null : json_decode($json, associative: true); + return is_array($data) ? $data : null; + } +} diff --git a/src/Config/PackageDiscovery.php b/src/Config/PackageDiscovery.php index 070f1700e..ba27582c9 100644 --- a/src/Config/PackageDiscovery.php +++ b/src/Config/PackageDiscovery.php @@ -18,16 +18,20 @@ * the directory `extra.dresscode.upgrading-dir` names, read for the versions of their package the code must work with * (`UpgradingFile::selectFor()`), with those of PHP for its target (`readPhpData()`), and the plugin under * `extra.dresscode.plugin`, which the project lets in by naming the package or the plugin in `use`, as Composer lets in - * its own plugins. The root package takes part too, and a file about the root itself applies whole. Such a file never - * turns a rule on itself; its maps, and the requirements of the rules of the plugin of its package, are laid under those - * of the project where the project says `upgrading.packages: yes`, those of PHP where it says + * its own plugins. The root package takes part too, and a file about the root itself applies whole, and so do the + * packages of upgrading data installed beside DressCode, which `upgrade` installs for a project lacking them. Such a + * file never turns a rule on itself; its maps, and the requirements of the rules of the plugin of its package, are + * laid under those of the project where the project says `upgrading.packages: yes`, those of PHP where it says * `upgrading.php.deprecatedApi: forbidden`. * @internal */ final readonly class PackageDiscovery { /** the keys of `extra.dresscode` a package may use */ - public const Keys = ['upgrading-dir', 'plugin']; + public const Keys = ['upgrading-dir', 'packages', 'plugin']; + + /** the type of the packages that bring DressCode upgrading data or a plugin */ + public const PackageType = 'dresscode-plugin'; /** the platform package of Composer the upgrading data of PHP are about, which DressCode ships itself */ public const Php = 'php'; @@ -63,10 +67,14 @@ private function __construct( } - /** @throws ConfigurationException for a file a package ships that cannot be read */ - public static function discover(ProjectPackages $project): self + /** + * @param ?array $installation the packages of upgrading data installed beside DressCode => where + * they lie, by default those of the running process + * @throws ConfigurationException for a file a package ships that cannot be read + */ + public static function discover(ProjectPackages $project, ?array $installation = null): self { - // the root package first, then the installed ones as Composer lists them + // the root package first, then the installed ones as Composer lists them, then the data beside DressCode $packages = $project->rootPath === null ? [] : [[$project->rootName ?? 'the root package', $project->rootPath, $project->rootExtra['dresscode'] ?? null]]; @@ -74,6 +82,12 @@ public static function discover(ProjectPackages $project): self $packages[] = [$name, $package['path'], $package['extra']['dresscode'] ?? null]; } + foreach ($installation ?? self::findInstalledDataPackages() as $name => $path) { + if (!$project->has($name)) { + $packages[] = [$name, $path, ComposerJson::read("$path/composer.json")['extra']['dresscode'] ?? null]; + } + } + $upgradingData = $plugins = $warnings = []; foreach ($packages as [$name, $path, $extra]) { if (!is_array($extra)) { @@ -175,6 +189,61 @@ public static function findUpgradingFiles(string $path, array $extra, string $pa } + /** + * The packages of upgrading data about a library the project requires itself, which neither the project nor the + * installation of DressCode holds. + * @param array> $catalogue package => the libraries it says it is about (`DataPackageCatalogue`) + * @param ?array $installation as `discover()` takes it + * @return list + */ + public static function findMissingDataPackages(ProjectPackages $project, array $catalogue, ?array $installation = null): array + { + $installation ??= self::findInstalledDataPackages(); + $missing = []; + foreach ($catalogue as $package => $libraries) { + if ($project->has($package) || isset($installation[$package])) { + continue; + } + + if (array_any(array_keys($project->required), fn(string $name) => self::isAbout($libraries, $name))) { + $missing[] = $package; + } + } + + return $missing; + } + + + /** + * Whether the library is one of those `extra.dresscode.packages` lists, a whole vendor written `vendor/`. + * @param list $libraries + */ + public static function isAbout(array $libraries, string $library): bool + { + return array_any($libraries, fn(string $name) => str_ends_with($name, '/') ? str_starts_with($library, $name) : $library === $name); + } + + + /** + * The packages of upgrading data installed beside DressCode, as Composer says of the running process. + * @return array package => where it lies + */ + public static function findInstalledDataPackages(): array + { + $found = []; + $packages = class_exists(InstalledVersions::class) ? InstalledVersions::getInstalledPackagesByType(self::PackageType) : []; + foreach (array_unique($packages) as $package) { + $path = InstalledVersions::getInstallPath($package); + $extra = $path === null ? null : (ComposerJson::read("$path/composer.json")['extra']['dresscode'] ?? null); + if (isset($extra['upgrading-dir'])) { + $found[$package] = Helpers::canonicalizePath(realpath($path) ?: $path); + } + } + + return $found; + } + + /** * The directory of the `composer.json` DressCode is installed by, globally, beside a project or as the project; * null for a phar and for a process Composer did not load. diff --git a/src/Config/ProjectPackages.php b/src/Config/ProjectPackages.php index ff8fdd4bf..7dc516e2b 100644 --- a/src/Config/ProjectPackages.php +++ b/src/Config/ProjectPackages.php @@ -36,7 +36,7 @@ public function __construct( /** @var array what the composer.json of the root package says under extra */ public array $rootExtra = [], /** @var array package the project requires itself => its constraint */ - private array $required = [], + public array $required = [], /** * @var array}> installed * package => the version it stands for (null for any), the source it came from, where it lies and its extra diff --git a/src/Config/RunnerFactory.php b/src/Config/RunnerFactory.php index 463fa2409..2af5fa990 100644 --- a/src/Config/RunnerFactory.php +++ b/src/Config/RunnerFactory.php @@ -380,7 +380,7 @@ private static function resolveCacheFile(Config $config, string $root): string } - private static function resolveCacheDir(Config $config, string $root): string + public static function resolveCacheDir(Config $config, string $root): string { $root = Helpers::canonicalizePath($root); $dir = $config->cacheDir === null ? sys_get_temp_dir() . '/dresscode' : Helpers::toAbsolutePath($config->cacheDir, $root); diff --git a/src/Console/Commands/CheckCommand.php b/src/Console/Commands/CheckCommand.php index ba4399b4a..2bda75b14 100644 --- a/src/Console/Commands/CheckCommand.php +++ b/src/Console/Commands/CheckCommand.php @@ -9,7 +9,7 @@ use DressCode\{Config, ConfigurationException, Reporter, Reporters}; use DressCode\Config\{ - Loader, PackageDiscovery, PluginRegistry, ResolvedProject, RunnerFactory, UpgradingData, + DataPackageCatalogue, Loader, PackageDiscovery, PluginRegistry, ProjectPackages, ResolvedProject, RunnerFactory, UpgradingData, Versions, }; use DressCode\Console\{CommandContext, Markup, ProgressBar, RiskReview}; @@ -64,6 +64,8 @@ public function upgrade( ?bool $modernize, #[Cli\Flag('Upgrade as usual, then ask about every risky fix left, one at a time with its diff, and make those accepted; needs an interactive terminal')] ?bool $askRisky, + #[Cli\Flag('Install the upgrading data the libraries of the project need without asking, outside a terminal too')] + ?bool $yes, ConfigOptions $configuration, #[Cli\Option( 'Also make the fixes that may change what the code does, of every rule, or with `=name` of that decision, rule, preset or package; they are reported either way', @@ -80,6 +82,15 @@ public function upgrade( array $only = [], ): int { + // a worker runs with what its parent installed, and what is installed, plugins among it, loads in a new process + if ($run->worker === null && $this->installDataPackages($args, $configuration, (bool) $yes)) { + passthru( + implode(' ', array_map(escapeshellarg(...), [...WorkerPool::buildPhpCommand() ?? [PHP_BINARY], ...$_SERVER['argv']])), + $exit, + ); + return $exit; + } + return $this->run( $args, $configuration, @@ -440,6 +451,57 @@ private function findKeptDecisions(Config $config, string $root, string $file, ? } + /** + * Installs the packages of upgrading data the libraries of the project need and DressCode lacks: beside DressCode, + * or into the project with `require --dev` where its lock holds DressCode; in a terminal after asking, elsewhere + * only with `--yes`. + * @return bool whether it installed them + * @throws ConfigurationException where it may not install them, or Composer failed + */ + private function installDataPackages(ParseResult $args, ConfigOptions $configuration, bool $yes): bool + { + ['config' => $config, 'root' => $root] = $this->context->loadConfig($args, $configuration, new Config(paths: ['.'])); + $project = ProjectPackages::read($root); + $missing = PackageDiscovery::findMissingDataPackages( + $project, + DataPackageCatalogue::load(RunnerFactory::resolveCacheDir($config, $root)), + ); + if (!$missing) { + return false; + } + + $intoProject = $project->has('dresscode/dresscode'); + $directory = $intoProject ? $root : PackageDiscovery::findInstallationRoot(); + $names = implode(', ', array_map(fn(string $package) => "`$package`", $missing)); + $what = 'The libraries of the project have upgrading data in ' . $names . ', which DressCode does not have'; + if ($directory === null) { + throw new ConfigurationException("$what, and its installation is not one Composer can add to; install the package beside DressCode."); + } + + $command = ['composer', 'require', ...$intoProject ? ['--dev'] : [], '--working-dir=' . $directory, ...$missing]; + $shown = implode(' ', array_map(fn(string $arg) => str_contains($arg, ' ') ? "\"$arg\"" : $arg, $command)); + if (!$yes) { + $prompt = $this->context->prompt; + if (!$prompt->isInteractive()) { + throw new ConfigurationException("$what; install it with `$shown`, or let `dresscode upgrade --yes` do it."); + } + + $this->context->out->writeLine("$what."); + $where = $intoProject ? 'into the project, which changes its `composer.json`' : "beside DressCode in `$directory`"; + if ($prompt->choose("Install it $where?", ['y', 'n'], 'y') !== 'y') { + return false; + } + } + + passthru(implode(' ', array_map(escapeshellarg(...), $command)), $exit); + if ($exit !== 0) { + throw new ConfigurationException("Composer did not install the upgrading data: `$shown` exited with code $exit."); + } + + return true; + } + + /** * The decisions the file as it was before an edit broke, which the run then leaves as the file writes them. * @return list diff --git a/src/Testing/UpgradingTester.php b/src/Testing/UpgradingTester.php index 4dff3e7bc..8c6169a99 100644 --- a/src/Testing/UpgradingTester.php +++ b/src/Testing/UpgradingTester.php @@ -12,6 +12,7 @@ use DressCode\Domains\Map; use DressCode\Rules\Upgrading\MemberTarget; use DressCode\Upgrading\{MemberMapGrammar, MemberPattern, MemberPatternKind}; +use Nette\Neon\{Exception, Neon}; use Nette\Schema\ValidationException; use Nette\Utils\FileSystem; use function count, is_array, is_string, strlen; @@ -78,6 +79,51 @@ public static function collectProblems(string $file, string $root, array $packag } + /** + * Checks that `extra.dresscode.packages` of the package at the root says exactly which packages its upgrading files + * are about, by which `upgrade` offers it to a project lacking it: none of them left out, and none listed that no + * file is about. + * @return list + */ + public static function collectPackageProblems(string $root): array + { + $composer = ComposerJson::read("$root/composer.json") ?? []; + $name = is_string($composer['name'] ?? null) ? $composer['name'] : 'the root package'; + $extra = $composer['extra']['dresscode'] ?? null; + if (!is_array($extra)) { + return []; + } + + try { + $about = []; + foreach (PackageDiscovery::findUpgradingFiles($root, $extra, $name) as $file) { + $package = Neon::decodeFile("$root/$file")['package'] ?? null; + if (is_string($package)) { + $about[$package] = $file; + } + } + } catch (ConfigurationException|Exception $e) { + return [$e->getMessage()]; + } + + $listed = is_array($extra['packages'] ?? null) ? array_values(array_filter($extra['packages'], is_string(...))) : []; + $problems = []; + foreach ($about as $package => $file) { + if (!PackageDiscovery::isAbout($listed, $package)) { + $problems[] = "Package `$name`: `extra.dresscode.packages` in its `composer.json` does not list `$package`, which `$file` is about."; + } + } + + foreach ($listed as $package) { + if (!array_any(array_keys($about), fn(string $library) => PackageDiscovery::isAbout([$package], $library))) { + $problems[] = "Package `$name`: `extra.dresscode.packages` in its `composer.json` lists `$package`, which no upgrading file is about."; + } + } + + return $problems; + } + + /** * Runs code written for the old API of the libraries through the rules as the project fixes it: every section * the installed version of a library reaches applies, whatever the constraint of the project allows, and the diff --git a/tests/DressCode/Config/DataPackageCatalogue.phpt b/tests/DressCode/Config/DataPackageCatalogue.phpt new file mode 100644 index 000000000..d80e6a674 --- /dev/null +++ b/tests/DressCode/Config/DataPackageCatalogue.phpt @@ -0,0 +1,14 @@ + ['nette/', 'latte/latte']], DataPackageCatalogue::load($dir)); +}); diff --git a/tests/DressCode/Config/PackageDiscovery.phpt b/tests/DressCode/Config/PackageDiscovery.phpt index cdbcdf5f0..02a2aedeb 100644 --- a/tests/DressCode/Config/PackageDiscovery.phpt +++ b/tests/DressCode/Config/PackageDiscovery.phpt @@ -668,3 +668,49 @@ test( ); }, ); + + +test('the upgrading data installed beside DressCode serves a project without it, and what its libraries miss is named', function () { + $root = project( + 'data-beside', + ['nette/utils' => ['4.0.0.0', []], 'dibi/dibi' => ['5.0.0.0', []], 'symfony/console' => ['7.0.0.0', []]], + [], + ['require' => ['nette/utils' => '^4.0', 'dibi/dibi' => '^5.0']], + ); + $data = createTempDir('data-package'); + FileSystem::write( + "$data/composer.json", + json_encode(['name' => 'dresscode/upgrade-nette', 'extra' => ['dresscode' => ['upgrading-dir' => 'upgrading']]]), + ); + FileSystem::write( + "$data/upgrading/utils.neon", + "package: nette/utils\n\nsince 4.0:\n\treplacedClasses:\n\t\tNette\\Old: Nette\\Renamed\n", + ); + $project = ProjectPackages::read($root); + + // a library the project requires itself, as the package says it is about, whereas symfony/console comes with another + $catalogue = [ + 'dresscode/upgrade-nette' => ['nette/', 'latte/latte'], + 'dresscode/upgrade-deegee' => ['dibi/dibi'], + 'dresscode/upgrade-symfony' => ['symfony/console'], + ]; + Assert::same( + ['dresscode/upgrade-nette', 'dresscode/upgrade-deegee'], + PackageDiscovery::findMissingDataPackages($project, $catalogue, []), + ); + Assert::same( + ['dresscode/upgrade-deegee'], + PackageDiscovery::findMissingDataPackages($project, $catalogue, ['dresscode/upgrade-nette' => $data]), + ); + Assert::same( + ['upgrading/utils.neon of dresscode/upgrade-nette'], + array_map( + fn($data) => $data->layer->describe(), + PackageDiscovery::discover($project, ['dresscode/upgrade-nette' => $data])->upgradingData, + ), + ); + + // a project with none of their libraries misses nothing + $other = ProjectPackages::read(project('data-none', ['acme/lib' => ['1.0.0.0', []]], [], ['require' => ['acme/lib' => '^1.0']])); + Assert::same([], PackageDiscovery::findMissingDataPackages($other, $catalogue, [])); +}); diff --git a/tests/DressCode/Console/Application.phpt b/tests/DressCode/Console/Application.phpt index be0cbf829..0eeee0626 100644 --- a/tests/DressCode/Console/Application.phpt +++ b/tests/DressCode/Console/Application.phpt @@ -511,6 +511,34 @@ test('upgrade says which packages are installed newer than required, and whose d }); +test('upgrade installs the upgrading data a library of the project needs only when asked, or with --yes', function () { + $root = createTempDir('upgrade-data'); + mkdir("$root/src"); + file_put_contents("$root/composer.json", '{"require": {"php": ">=8.2", "nette/utils": "^4.0"}}'); + file_put_contents("$root/dresscode.neon", "paths: [src]\ncacheDir: cache\n"); + file_put_contents("$root/src/a.php", " Date: Wed, 14 Oct 2026 14:07:21 +0200 Subject: [PATCH 07/13] internals: init and the standards symfony and laravel --- AGENTS.md | 1 + docs/internals.md | 20 +++++++++++++++++--- readme.md | 21 +++++++++++++-------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 55b619f56..8ff451201 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ Rules use only the public API of `PhpSyntax`; whatever a rule in DressCode needs - `composer reference`: regenerates `docs/reference/decisions.md` (every decision with its values, the values of the standards and the rules declaring it) from the catalogue. Commit the output; CI diffs it. - `composer api`: regenerates `docs/reference/api.md`, the public classes and members as reflection sees them. Commit the output and read its diff, which is what a change does to the public surface; CI diffs it. - `php bin/dresscode check`: DressCode over its own sources with `dresscode.neon`; CI runs it too. +- `php bin/dresscode init` in the root of another project: writes the `dresscode.neon` measured from its code, or prints it when a configuration exists; how it measures is in `docs/internals.md`. ## Conventions diff --git a/docs/internals.md b/docs/internals.md index 2bb4f2a01..02753cf96 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -263,7 +263,7 @@ Such a file sets maps, the requirements of the rules of the plugin of its own pa A preset is a file of the shape of the configuration under a name, `src/Presets/.neon` for those of the core, a file a plugin names in its manifest, or any file `use` gives by its path. It carries decisions, the presets it uses, `suppressionComments` and `severity`, nothing else: what is the project's, the facts, the scope, `fixRisky`, `reportOnly`, a plugin and `rules`, `NeonReader::readPreset()` refuses, so a file laid by `use` never gains what the file of the project can do. A name without a vendor is the one of the core, so `perCs` and `dresscode/perCs` are the same preset. A preset opens with a comment, its title and what it is, and says what a decision does, where its key and value do not, in a short comment after the value. -The presets of the core are the five standards, "standard" being the word for a preset that carries a style and says how the code looks: `psr12`, `perCs` on top of it, `nette` on `perCs` and the sets `cleanup`, `types` and `correctness`, with tabs and the layout of the Nette libraries, `symfony` on `perCs` and `laravel` on `perCs`, the preset laravel of Laravel Pint. What a project wants beyond the looks of its code is a set, a preset of one intent used beside the standard (`use: [nette, modernizations]`): `modernizations`, `deprecations`, `cleanup`, `correctness`, `types` and `compilerOptimizations`. A set decides nothing about the layout, so it never fights the standard, and a decision the core gains goes into its set, so it reaches every project using the set without a change of its configuration. A standard may use a set and keep some of its decisions, as `nette` keeps the native types and an else after a return; a project naming that set beside the standard gets none of them back, the set being laid once, below the standard, and the run says so; the project writes those decisions itself. `tests/DressCode/Presets/sets.phpt` holds that every construct and function newer PHP brought or retired is in a set and lists, with the reason, the decisions a project writes itself. A decision of a rule of a plugin reaches a project when a preset it uses or the project writes it. `psr12` and `perCs` follow their specifications section by section and add no style of their own, so a rule whose fixed behaviour contradicts a specification gets a value rather than a preset-side workaround. Without a configuration file nothing applies: DressCode has no style of its own to fall back on, so a run refuses, unless `--use` names what to use, which is then all the run has, so that a run over a foreign project can ask for that alone. +The presets of the core are the five standards, "standard" being the word for a preset that carries a style and says how the code looks: `psr12`, `perCs` on top of it, `nette` on `perCs` and the sets `cleanup`, `types` and `correctness`, with tabs and the layout of the Nette libraries, `symfony` on `perCs` and `laravel` on `perCs`, the preset laravel of Laravel Pint. What a project wants beyond the looks of its code is a set, a preset of one intent used beside the standard (`use: [nette, modernizations]`): `modernizations`, `deprecations`, `cleanup`, `correctness`, `types` and `compilerOptimizations`. A set decides nothing about the layout, so it never fights the standard, and a decision the core gains goes into its set, so it reaches every project using the set without a change of its configuration. A standard may use a set and keep some of its decisions, as `nette` keeps the native types and an else after a return; a project naming that set beside the standard gets none of them back, the set being laid once, below the standard, and the run says so; the project writes those decisions itself. `tests/DressCode/Presets/sets.phpt` holds that every construct and function newer PHP brought or retired is in a set and lists, with the reason, the decisions a project writes itself. A decision of a rule of a plugin reaches a project when a preset it uses or the project writes it. `psr12` and `perCs` follow their specifications section by section and add no style of their own, so a rule whose fixed behaviour contradicts a specification gets a value rather than a preset-side workaround. Without a configuration file nothing applies: DressCode has no style of its own to fall back on, so a run refuses and points to `dresscode init`, unless `--use` names what to use, which is then all the run has, so that a run over a foreign project can ask for that alone. `multiline.trailingComma` removes the comma of a list written on one line wherever its place is not `keep`, so `perCs`, `nette` and `symfony` also fix `function f($a,)`, `use ($a,)` and a `match` on one line whose last arm is followed by a comma. For `symfony` that is a deliberate departure from `no_trailing_comma_in_singleline` of PHP CS Fixer, and so is the comma `optional` removes where the closing bracket follows it on the line of the last item, which `trailing_comma_in_multiline` leaves. @@ -303,7 +303,21 @@ What the namespaces of the project declare is a property of the project too, and `ConfigResolver` puts both into `ResolvedConfig`, so they are in the identity of the result cache and in `dresscode config` with the layer that named each symbol, and `Analyses\Registry` builds `NameResolver` with them as `NamespacedSymbols`, which is also the analysis a rule asks for the lists themselves. -A certain resolution rests on lists that must stay complete, so the key turns on their guard, `NoUnlistedNamespacedDeclarationsRule`, whose decisions are the facts `namespaces.functions` and `namespaces.constants`: it reports a declaration missing from them under the path of the list, `--only` never narrows it away, since a fix resting on the lists must not lose what keeps them true, and a file whose resolution ends up uncertain has no guard; a declaration the lists do not name is decidable in its file, a declaration inside a condition and a `define()` with its name written as a string among them (`NameResolver::findNamespacedDeclarations()`), and the lists can then go stale only through a path left out of the run or a package of the same namespace. What a package declares for the files a project writes into its namespace is said by the data of the package (`namespaces` of its upgrading file), which lies under the lists of every layer; `dresscode config` names the source of each. +A certain resolution rests on lists that must stay complete, so the key turns on their guard, `NoUnlistedNamespacedDeclarationsRule`, whose decisions are the facts `namespaces.functions` and `namespaces.constants`: it reports a declaration missing from them under the path of the list, `--only` never narrows it away, since a fix resting on the lists must not lose what keeps them true, and a file whose resolution ends up uncertain has no guard; a declaration the lists do not name is decidable in its file, a declaration inside a condition and a `define()` with its name written as a string among them (`NameResolver::findNamespacedDeclarations()`), and the lists can then go stale only through a path left out of the run or a package of the same namespace. What a package declares for the files a project writes into its namespace is said by the data of the package (`namespaces` of its upgrading file), which lies under the lists of every layer, and for a package that does not say it yet DressCode knows it itself (`PackageDiscovery::Known`, the functions of the container configurator of Symfony for `symfony/dependency-injection`); `dresscode config` names the source of each. + +### init + +`dresscode init` writes `dresscode.neon` from how the code is already written, and it has no measuring code of its own: `Measuring\Survey` runs the rule of a decision under each of its values over a sample of the scope, and what the rule reports is what disagrees with that value. + +The scope is what the `autoload` and `autoload-dev` of the nearest `composer.json` name, which is the one place a project says where its code is (`psr-4`, `psr-0` and `classmap`, read by the same code that reads the target version from that file; a `files` entry is a single file and no scope), together with the conventional directories the root has beside it for what an autoload cannot name, the entry points and the test suites (`Proposal::Paths`, where a name with `*` is a pattern of the root); a path lying inside another one is already in the scope and drops out, and `.` is the scope when neither source says anything. + +The sample is every k-th of the sorted files, so that the same tree always gives the same one, at most `FileSample::MaxFiles` files **and** `FileSample::MaxBytes` bytes, because the cost of measuring is bytes and not files; a file over `FileSample::MaxFileSize` never enters it and neither does one whose head says a generator wrote it, which is the reason reported when both hold, and the directories of `Proposal::GeneratedDirs` are left out on top of the default exclusions and written into the file when the tree has them. + +A decision that is a property of the file (the indentation) is counted by files; one every place makes anew is counted by places, which needs a rule that reports each place once under every value: `literals.quotes` is reported at every plain string written the other way, and `multiline.edges.condition` once per condition, because the gaps of a construct decided through `Gap::once()` are one violation; a run with every value allowed counts the places in none of them. A file the rule fails in under any of the values is left out of the decision for all of them, because it would agree with the value it failed under, and the measurement says how many were. A decision written carries above it the description and the values the catalogue gives it, and beside its value what was measured; a value at least `Measurement::Threshold` of the places agree with is written with its share; below that the file writes the tolerance where the decision takes one and the values together reach it, else `keep` or a comment, never a bare value, and never `fixRisky`. + +The standard is not measured: the file gets the one given with `--use`, for a project formatted by Laravel Pint (it requires `laravel/pint` or has a `pint.json`) the standard of its preset, `laravel`, `psr12`, `perCs` for `per` or `symfony`, `perCs` otherwise, and a comment naming the others. What each standard would cost is measured instead, a consequence and not a likeness: the files of the sample its first fix would change (`Proposal::countChangedByStandard()`, the same dry run over the same decisions with that standard alone). Without a standard given all five are told, the cheapest first; with one, only that one. The prices are measured over a part of the sample, at most `Proposal::PricedFiles` files, and each is printed as soon as it is known. The prices stay out of the file, where a price of a standard that was not written defends no line and would go stale with the first fix. + +What the namespaces declare is not a decision and not sampled: every file of the scope naming a namespace or `define` is parsed, because a declaration a sample left out would be taken as global with nothing to report it, and `nameResolution: certain` is written only when all of them parse, the first that does not being named instead, and `typeAnalysis: phpstan` where they have `phpstan/phpstan`, with a comment saying what the types give, the one fact of the project `init` writes without measuring it, because a project with PHPStan wants the risky fixes a type decides made where the types tell they are safe. What the file says is read back and resolved before it is written and must be what was measured (`Proposal::checkResolution()`), the types aside where no PHPStan is installed beside DressCode, which then goes without the types the file asks for, and a configuration that exists is never overwritten (the proposal goes to the output and the exit code is 3). ### upgrade @@ -335,7 +349,7 @@ A decision has a description, the sentence `explain` and the reference print fir An exception or a warning is read by someone who has to act, so it says where, what and what to do, in that order. Where is a prefix naming what the reader edits (`Configuration file X: `, `Upgrading file X: `, `Preset X: `, `The override for src/: `, `The command line: `), left out only when nothing but the command itself can be meant; what is a sentence about that thing, with the value the reader wrote or has to write; what to do follows after a semicolon, or as a second sentence when the first has one, and it is the only place an imperative may stand. A message the reader cannot act on, a broken rule or a failing worker, says whose fault it is instead. Every message is a capitalized sentence ending with a period, warnings included, `must` and never `has to`; a message wrapping another one puts it after a colon and adds nothing behind it, and what it wraps is left as its author wrote it. -Everything in such a message that is code stands in backticks, as in Markdown: a file and a path, a command and an option, a key of the configuration and a value (`` `x` given `` for a value refused), a decision, a preset, a package and the class of a rule, a class, a function, a member and an annotation (``Configuration file `dresscode.neon`: ``, ``set `typeAnalysis: phpstan` ``). A word that only speaks about code is not code (`the configuration`, `a preset`). The console draws what the backticks enclose in color and drops them; every other output, a pipe, a log of CI, the formats `json` and `github`, the reader of the PHP API, prints them as they are, which reads well to a human and to an agent alike. Where a page of the manual says more than the message, the exception carries the path of that page (`ConfigurationException::$docs`), and the console prints it under the message as a link to `https://dresscode.run/`. What the author of a plugin reads (`RuleInfo`, the claims, `RuleTester`) states the contract that was broken; what the user of a configuration reads never names an internal class, node or slot. +Everything in such a message that is code stands in backticks, as in Markdown: a file and a path, a command and an option, a key of the configuration and a value (`` `x` given `` for a value refused), a decision, a preset, a package and the class of a rule, a class, a function, a member and an annotation (``Configuration file `dresscode.neon`: ``, ``Run `dresscode init` ``, ``set `typeAnalysis: phpstan` ``). A word that only speaks about code is not code (`the configuration`, `a preset`). The console draws what the backticks enclose in color and drops them; every other output, a pipe, a log of CI, the formats `json` and `github`, the reader of the PHP API, prints them as they are, which reads well to a human and to an agent alike. Where a page of the manual says more than the message, the exception carries the path of that page (`ConfigurationException::$docs`), and the console prints it under the message as a link to `https://dresscode.run/`. What the author of a plugin reads (`RuleInfo`, the claims, `RuleTester`) states the contract that was broken; what the user of a configuration reads never names an internal class, node or slot. ### Testing diff --git a/readme.md b/readme.md index 0f3c3bd49..5f2f07950 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,7 @@ ✅ Fixes style, [upgrades PHP and libraries](#upgrading-code-to-newer-php-from-80-to-86), in one run
✅ Changes only what it fixes, [not a byte more](#changes-only-what-it-touches-a-lossless-syntax-tree-instead-of-a-token-array)
-✅ [PER Coding Style, PSR-12, Nette](#coding-standards-and-decisions-per-coding-style-31-psr-12-nette) built in
+✅ [PER Coding Style, PSR-12, Nette, Symfony, Laravel](#coding-standards-and-decisions-per-coding-style-31-psr-12-nette-symfony-laravel) built in
✅ [More of PER Coding Style and PSR-12](#how-much-of-the-standard-per-coding-style-and-psr-12-against-php-cs-fixer-and-php_codesniffer) than PHP CS Fixer or PHP_CodeSniffer fixes
✅ [Made for AI coding agents](#made-for-ai-coding-agents-claude-code-and-codex): it tells the agent what it changed @@ -45,7 +45,8 @@ Installation and first run ========================== **1️⃣ Install it: `composer global require dresscode/dresscode`**
-**2️⃣ Check your code and fix it: `dresscode check`, `dresscode fix`** +**2️⃣ Have a configuration made to measure: `dresscode init`**
+**3️⃣ Check your code and fix it: `dresscode check`, `dresscode fix`** DressCode is a tool, not a library, so it does not have to be installed in the project it checks, and a global installation is the simplest way; just make sure the directory of global Composer binaries is in your @@ -85,9 +86,11 @@ A clean run ends with `OK 214 files, all up to the dress code`, and the exit co of the configuration. DressCode has no style of its own, so without a configuration it does not start: a silent default would -rewrite nearly every line of a project written another way. The configuration is `dresscode.neon` in the -root of the project, or `dresscode.php` with the same keys; to just try the tool, name a standard instead -with `dresscode check src --use perCs`: +rewrite nearly every line of a project written another way. `dresscode init` measures your code (the +indentation, the quotes, where a condition spread over lines begins and ends) and writes `dresscode.neon` +into the root of the project; to just try the tool, name a standard instead with +`dresscode check src --use perCs`. The configuration can also be written by hand, in NEON or as +`dresscode.php` with the same keys: ```neon use: @@ -137,8 +140,8 @@ of an upgrade contains the upgrade and nothing else.   -Coding standards and decisions: PER Coding Style 3.1, PSR-12, Nette -=================================================================== +Coding standards and decisions: PER Coding Style 3.1, PSR-12, Nette, Symfony, Laravel +===================================================================================== DressCode has over two hundred rules, and you never name one. The configuration says how your code is written, thing by thing: every key names a thing in the code, and its value says how it is written. @@ -174,7 +177,9 @@ you can answer right away. |---|---| | `perCs` | [PER Coding Style 3.1](https://www.php-fig.org/per/coding-style/) in full, the successor of PSR-12 | | `psr12` | [PSR-12](https://www.php-fig.org/psr/psr-12/), section by section | -| `nette` | [Nette Coding Standard](https://doc.nette.org/en/contributing/coding-standard), PER Coding Style with tabs and a few departures | +| `nette` | [Nette Coding Standard](https://doc.nette.org/contributing/coding-standard), PER Coding Style with tabs and a few departures | +| `symfony` | Symfony Coding Standards, as the `@Symfony` set of PHP CS Fixer has them | +| `laravel` | the style [Laravel Pint](https://laravel.com/docs/pint) writes by default, its `laravel` preset | A standard is a file of the same shape as yours, so you can read in it what it decides and why, and your file has the last word on every key. Where the standards differ, they give the same key different values, From 0757afc62b2cabdce7a8165785a50f3eea433b4c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 15 Oct 2026 07:26:15 +0200 Subject: [PATCH 08/13] added interop The configurations and the suppressions of PHP CS Fixer, PHP_CodeSniffer and Slevomat translated to the decisions of DressCode. --- AGENTS.md | 6 +- composer.json | 2 +- docs/internals.md | 44 +- docs/reference/decisions.md | 299 ++++ docs/reference/generate.php | 4 + readme.md | 42 +- src/Config/ConfigResolver.php | 3 +- src/Config/DecisionResolver.php | 12 +- src/Config/PluginRegistry.php | 31 +- src/Console/Application.php | 1 + src/Console/Commands/CatalogueCommand.php | 14 +- src/Console/Commands/ImportCommand.php | 99 ++ src/Engine/Suppression.php | 47 +- src/Interop/PhpCodeSniffer.php | 864 ++++++++++++ src/Interop/PhpCsFixer.php | 1019 ++++++++++++++ src/Interop/Translation.php | 505 +++++++ src/Interop/Translator.php | 310 +++++ src/Profile.php | 6 +- tests/DressCode/Config/ConfigResolver.phpt | 13 + tests/DressCode/Config/DecisionResolver.phpt | 5 + tests/DressCode/Config/PluginRegistry.phpt | 9 + tests/DressCode/Console/Application.phpt | 92 ++ tests/DressCode/Engine/Suppression.phpt | 92 +- tests/DressCode/Interop/Translator.phpt | 1296 ++++++++++++++++++ tests/DressCode/Rules/fuzz-whitespace.phpt | 5 +- 25 files changed, 4784 insertions(+), 36 deletions(-) create mode 100644 src/Console/Commands/ImportCommand.php create mode 100644 src/Interop/PhpCodeSniffer.php create mode 100644 src/Interop/PhpCsFixer.php create mode 100644 src/Interop/Translation.php create mode 100644 src/Interop/Translator.php create mode 100644 tests/DressCode/Interop/Translator.phpt diff --git a/AGENTS.md b/AGENTS.md index 8ff451201..fcf1d6ea6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,13 +5,13 @@ It is my fervent wish that this file guide every AI coding agent working with co ## Documentation -`docs/internals.md` is the source of truth for how DressCode works: the tree as a rule sees it, the mutation API, the engine of gaps and the rules. Read it before any non-trivial change. +`docs/internals.md` is the source of truth for how DressCode works: the tree as a rule sees it, the mutation API, the engine of gaps, the rules and the interop. Read it before any non-trivial change. ## Project overview DressCode is a PHP code style checker and fixer built on a **lossless concrete syntax tree**: every token of the source is in the tree, whitespace and comments are trivia attached to tokens, and printing the tree reproduces the input byte for byte. -The tree itself is the `phpsyntax/phpsyntax` library (namespace `PhpSyntax`), developed in a repository of its own. This one holds `DressCode` (`src/`): engine, rules API, the library of generic rules, configuration, CLI. +The tree itself is the `phpsyntax/phpsyntax` library (namespace `PhpSyntax`), developed in a repository of its own. This one holds `DressCode` (`src/`): engine, rules API, the library of generic rules, configuration, CLI. The names of other tools live only in `Interop/`, never in a rule. Rules use only the public API of `PhpSyntax`; whatever a rule in DressCode needs from it is public API for plugins too. What the rules only share among themselves (`Rules\NodeHelpers`, `Rules\Compiler` and the helpers beside the rules that carry `@internal`) is `@internal`; the grammar of a map of members (`Upgrading\MemberMapGrammar`, the map itself `MemberMap` with `findAccess()`, its `MemberMapEntry` and `MemberMapMatch`, `MemberPattern`, `ArgumentPattern`, `FunctionPattern`) is not, a rule shipped by a package being fed the same way, and neither is `GlobalCalls`, which answers which global function a call calls and why that may be another one, nor `CodeWriter`, which writes code in the shape of the file (a class written the way the file reaches it, an import added the way the file writes its imports) for a rule of a package as for one of the core. Presets define style; DressCode has no style of its own. The five standards carry one: `dresscode/perCs` (PER Coding Style 3.1), `dresscode/psr12`, `dresscode/nette` (the Nette Coding Standard, PER with tabs, the layout of the Nette libraries and their departures, which the repository itself is checked with), `dresscode/symfony` and `dresscode/laravel` (the preset laravel of Laravel Pint). What a project wants beyond the looks of its code comes in a set, a preset of one intent that decides nothing about the layout and is used beside the standard (`modernizations`, `deprecations`, `cleanup`, `correctness`, `types`, `compilerOptimizations`); a decision the core gains goes into its set, one that writes a newer construct and the replacement of what PHP deprecated alike into every set whose intent it meets (`upgrading.syntax.firstClassCallables` in `modernizations` and `deprecations`), and what no set holds the project decides itself. @@ -58,7 +58,7 @@ Rules use only the public API of `PhpSyntax`; whatever a rule in DressCode needs - A qualification writes a name imported or qualified only where that has an effect, so `function.optimized` takes a function only in a namespace where PHP optimizes one of its calls, the other functions there being decided by `function.global`. - Comments only where the code itself is not enough; never restate what the code shows; density follows the surrounding file. No phpDoc for what the types already say. - Code in a doc comment is written in backticks: a call, a variable or a property, an operator, a keyword, a literal, the case of an enum (`report()`, `$this`, `?->`, `static`, `'*'`, `Severity::Warning`). Doc comments are read as Markdown in an IDE, and what stands bare there is typeset as text: `->` becomes an arrow, `...` an ellipsis. Backticks mark the code itself, so what only resembles code stays bare, whatever the IDE makes of it: a keyword used as a word (a static method, a readonly class, but a `match` expression), `null`, `true` and `false` in "null when …", and the `=>` describing a map (path => its size). -- A framework or library is named in the core only where it is the subject of the code (its preset, a dependency, a file or key another tool defines). An example of a general mechanism uses invented names under the vendor `Acme`, in a domain of their own that mirrors no real API (`Acme\Shop\Order::STATUS_PAID`, `Acme\Mail\SmtpTransport`). +- A framework or library is named in the core only where it is the subject of the code (its preset, its interop, a dependency, a file or key another tool defines). An example of a general mechanism uses invented names under the vendor `Acme`, in a domain of their own that mirrors no real API (`Acme\Shop\Order::STATUS_PAID`, `Acme\Mail\SmtpTransport`). - Code, comments, identifiers and messages in English. - A string a `.neon` file has to quote is written in single quotes (`call: 'foo($a, $b)'`), double quotes only where it needs an escape or holds a single quote. diff --git a/composer.json b/composer.json index f29ba454e..12afe1c8b 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "8.4 - 8.6", "nette/command-line": "^2.0", - "nette/neon": "^3.4 || ^4.0", + "nette/neon": "^4.0", "nette/schema": "^1.4 || ^2.0", "nette/utils": "^4.1", "phpsyntax/phpsyntax": "^1.0", diff --git a/docs/internals.md b/docs/internals.md index 02753cf96..bd52483e8 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -25,7 +25,7 @@ Public: Internal: -- `Engine\*`, `Config\*`, `Measuring\*`, `Console\*`: the engine, the configuration, the measuring of `init` and the console +- `Engine\*`, `Interop\*`, `Config\*`, `Measuring\*`, `Console\*`: the engine, the translation of foreign configurations, the rest of the configuration, the measuring of `init` and the console - `Reporter`, `Reporters\*`: the formats of the output, whose contract is the output itself, and the interface they implement next to them; they print the `Engine\RunResult` of a run - `Rules\NodeHelpers`, `Rules\ListLayout`, `Rules\Compiler`, `Rules\NativeType`, `Rules\Classes\PropertyWrites`, `Rules\Classes\MemberModifiers`, `Rules\Functions\ForwardingClosure`, `Rules\Namespaces\NameReferences`, `Rules\Namespaces\QualificationPolicy`, `Rules\PhpDoc\AnnotationReplacement`, `Rules\PhpDoc\PhpdocTypeNotationVisitor`, `Rules\Whitespace\BlankLineClaims`, `Rules\Whitespace\Padding`: what the rules share among themselves or a rule does in a class of its own - `Rules\Upgrading\AnnotationArguments`, `Rules\Upgrading\CallableLiteral`, `Rules\Upgrading\CallTemplate`, `Rules\Upgrading\ClassReplacement`, `Rules\Upgrading\MagicCall`, `Rules\Upgrading\MemberTarget`, `Rules\Upgrading\AttributeTarget`, `Rules\Upgrading\Rewrite`, `Rules\Upgrading\AttributeForMemberEntry`, `Rules\Upgrading\AnnotationMap`, `Rules\Upgrading\enums.php`: the helpers of the rules fed by maps that are not the grammar of the maps @@ -139,7 +139,7 @@ A gap rule is made of claims alone: `GapRule` and `NodeRule` are the two shapes ### Identity -A rule is one instance per run and stateless across files; the state of a pass over a file lives in `RuleContext::$passStorage`, which every pass begins empty, so that the last pass reports all it finds. It has no name: what a user writes, reads in a report, silences and accepts is a decision, and the rule is known by its class, in the profile, in a fixture and in the message of a broken contract. Every rule declares or names a decision at least, and one that has none is a mistake of the configuration, refused by the catalogue and by the plan. It reports an occurrence under the path of the decision it violates (the `decision` of `report()`, `Claim::$decision`), which only a rule of one requirement leaves out; a report under a decision the rule does not declare or under a parameter is a broken contract, and one under a requirement that is `keep` or narrowed away records nothing and returns false. What decides it is the `Engine\Gate` of the rule, which the plan holds for every rule, made from the values of the run (`ResolvedConfig::getGates()`) or open, every requirement reported, for a rule run without a resolution; the requirements it admits are also those among which a narrowed run counts the entries of the baseline nothing matched. `PluginRegistry` keeps the classes in the order of their registration and the presets by name. +A rule is one instance per run and stateless across files; the state of a pass over a file lives in `RuleContext::$passStorage`, which every pass begins empty, so that the last pass reports all it finds. It has no name: what a user writes, reads in a report, silences and accepts is a decision, and the rule is known by its class, in the profile, in a fixture and in the message of a broken contract. Every rule declares or names a decision at least, and one that has none is a mistake of the configuration, refused by the catalogue and by the plan. It reports an occurrence under the path of the decision it violates (the `decision` of `report()`, `Claim::$decision`), which only a rule of one requirement leaves out; a report under a decision the rule does not declare or under a parameter is a broken contract, and one under a requirement that is `keep` or narrowed away records nothing and returns false. What decides it is the `Engine\Gate` of the rule, which the plan holds for every rule, made from the values of the run (`ResolvedConfig::getGates()`) or open, every requirement reported, for a rule run without a resolution; the requirements it admits are also those among which a narrowed run counts the entries of the baseline nothing matched. `PluginRegistry` keeps the classes in the order of their registration and the presets by name. What a rule of another tool means here is not part of that identity: it lives in `DressCode\Interop`, where a translation writes decisions. ### Report before mutation @@ -201,7 +201,7 @@ Its format follows the extension of its file, so it is written in whichever of t ### Suppression -Suppression is read once from the original comments before any mutation: `dresscode:ignore [paths]` on a line silences that line, a section or a structure standing for every decision under it, on its own line the nearest node starting on the next line, `disable`/`enable` a range, `ignoreFile` everything; what follows ` -- ` says why, as in ESLint and phpcs, and names no rule. A name no decision stands for is a warning of the run. A project that already marks its intended exceptions with a comment of its own says so in `suppressionComments`, a map of a pattern to the decisions it silences (`'~\bintentionally\s+[!=]=~': expressions.comparison.loose`, which the `nette` preset carries): a comment the pattern matches silences those decisions as `dresscode:ignore` would at its place, on its line or, alone on a line, on the next node; an entry written as `{decisions: …, example: '// @ '}` carries the example of such a comment too, which must match the pattern with a word put for ``, and a layer saying the pattern again replaces the entry, the example with it (`ResolvedConfig::$suppressionExamples`, what the output for an agent offers); a pattern is matched against the text of one comment, so `^` stands at its start (`'~^//\s*@\s+\S~'`), and where a configuration has any, every file has its comments walked. +Suppression is read once from the original comments before any mutation: `dresscode:ignore [paths]` on a line silences that line, a section or a structure standing for every decision under it, on its own line the nearest node starting on the next line, `disable`/`enable` a range, `ignoreFile` everything; what follows ` -- ` says why, as in ESLint and phpcs, and names no rule. The forms of PHP_CodeSniffer (`phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, `phpcs:ignoreFile` and the `@phpcsSuppress` annotation) name the rules of another tool, which `Interop\Translator` translates, one name possibly standing for several decisions, or the decisions of DressCode by their paths; a name no decision stands for is a warning of the run. A project that already marks its intended exceptions with a comment of its own says so in `suppressionComments`, a map of a pattern to the decisions it silences (`'~\bintentionally\s+[!=]=~': expressions.comparison.loose`, which the `nette` preset carries): a comment the pattern matches silences those decisions as `dresscode:ignore` would at its place, on its line or, alone on a line, on the next node; an entry written as `{decisions: …, example: '// @ '}` carries the example of such a comment too, which must match the pattern with a word put for ``, and a layer saying the pattern again replaces the entry, the example with it (`ResolvedConfig::$suppressionExamples`, what the output for an agent offers); a pattern is matched against the text of one comment, so `^` stands at its start (`'~^//\s*@\s+\S~'`), and where a configuration has any, every file has its comments walked. ### Processing a file @@ -279,7 +279,7 @@ An override (`overrides` in NEON, `Override` in PHP) is a profile for a part of The excluded paths only ever add up, the default list, those of the plugins and those of the configuration, so no layer can bring back what another left out. -`only` (`--only`, a parameter of the run and no key of a file) narrows a run to the decisions it names, and it changes no value: the tree is resolved whole and `only` lays a mask over it, `Values::isSelected()`, so a requirement outside the mask is neither reported nor fixed, while what the rules write keeps the values the project decided (a run narrowed to a rewrite of a structure in a project with four spaces writes the new block with four spaces, not a tab). A rule reads the value with `Values::get()` and asks whether a requirement is selected separately, and reading a value never enforces what the run did not select. A name of `only`, as of `fixRisky` and `severity`, is a decision, a section or a structure for every decision under it, a package of upgrading data, one of the vendor `dresscode` written without it too, for the maps and the requirements its files set, so the rules reading them, core and plugin alike, and only its maps where no other name covers a map (`ConfigResolver::collectPackageMaps()`), a library the same way for the files about it, whichever package ships them (`php` for the data of PHP), a name that is both a package shipping files and a library the files of another package are about standing for both, a preset for every decision it and what it uses write, or the class of a rule for its requirements. A section that shares its name with a preset (`types`) is written `types.*`, the bare name being refused as one that says both. A section or a preset narrows the run to those of its rules the rest turns on, never more, since its name is no decision about any one of them; where the project turned some of them off the run says which by their requirements, the first five, and a name that lets nothing in is an error rather than a run that checks nothing, naming `--use --only ` where the configuration does not use it. A name whose rules the target or the packages of the project leave out is skipped with a warning instead, so that one command serves projects of several targets, and is an error only where no other name of `only` runs. The result cache keys on the selection, and the workers get it on their command line. `fix --only` fixes those decisions and leaves the rest, so the next full `fix` changes more, which is the point and not a failure to converge. +`only` (`--only`, a parameter of the run and no key of a file) narrows a run to the decisions it names, and it changes no value: the tree is resolved whole and `only` lays a mask over it, `Values::isSelected()`, so a requirement outside the mask is neither reported nor fixed, while what the rules write keeps the values the project decided (a run narrowed to a rewrite of a structure in a project with four spaces writes the new block with four spaces, not a tab). A rule reads the value with `Values::get()` and asks whether a requirement is selected separately, and reading a value never enforces what the run did not select. A name of `only`, as of `fixRisky` and `severity`, is a decision, a section or a structure for every decision under it, a package of upgrading data, one of the vendor `dresscode` written without it too, for the maps and the requirements its files set, so the rules reading them, core and plugin alike, and only its maps where no other name covers a map (`ConfigResolver::collectPackageMaps()`), a library the same way for the files about it, whichever package ships them (`php` for the data of PHP), a name that is both a package shipping files and a library the files of another package are about standing for both, a preset for every decision it and what it uses write, or the class of a rule for its requirements; a name of another tool is refused with the decisions that cover it, which only a suppression takes. A section that shares its name with a preset (`types`) is written `types.*`, the bare name being refused as one that says both. A section or a preset narrows the run to those of its rules the rest turns on, never more, since its name is no decision about any one of them; where the project turned some of them off the run says which by their requirements, the first five, and a name that lets nothing in is an error rather than a run that checks nothing, naming `--use --only ` where the configuration does not use it. A name whose rules the target or the packages of the project leave out is skipped with a warning instead, so that one command serves projects of several targets, and is an error only where no other name of `only` runs. The result cache keys on the selection, and the workers get it on their command line. `fix --only` fixes those decisions and leaves the rest, so the next full `fix` changes more, which is the point and not a failure to converge. ### What the commands print @@ -287,7 +287,7 @@ The excluded paths only ever add up, the default list, those of the plugins and `dresscode config` prints that resolution in the shape of the file, every decision a layer said with the layer in a comment (`# dresscode/perCs`, `# the override for tests/`), why it takes no effect where it does not, whether it lies outside `--only` and whether its risky fixes are accepted, and `--preset ` prints one preset alone, what it uses included. `--file` narrows it to what the run uses for one file and `--format json` gives it as data: its `version`, the facts in the words of the configuration, the decisions by path, each with `value`, `layer`, `inactive`, `selected` and `severity` (`error` or `warning`), and the rules by class with the reason a rule does not run as a word (`turnedOff`, `php`, `package`, `types`, `narrowed`, `onlyOverride` or `notMentioned`) beside the sentence. A name in `fixRisky` whose rules run nowhere, not even in an override, is a warning of every run. -`dresscode catalogue` prints the catalogue of the installed set and `--format json` gives it as data, versioned (`Catalogue::Version`): every decision with its kind, its domain as data, its description and notes, the rules declaring it with what each requires and the values of the five standards as a configuration writes them. It is the interface an editor of the configuration is built on, and `composer reference` writes `docs/reference/decisions.md` from the same data. +`dresscode catalogue` prints the catalogue of the installed set and `--format json` gives it as data, versioned (`Catalogue::Version`): every decision with its kind, its domain as data, its description and notes, the rules declaring it with what each requires, the values of the five standards as a configuration writes them and the names of the other tools whose rules it covers. It is the interface an editor of the configuration is built on, and `composer reference` writes `docs/reference/decisions.md` from the same data. ### The target version of PHP @@ -339,7 +339,7 @@ The subject is the thing at the reported position, with a definite article and i A path names a thing in the code, by the name PHP gives it where it has one (`hygiene.is_null`, `mistakes.__serialize`) and by the name of the family where several things share the key (`upgrading.functions.arrayFirstLast`), under the section a reader looks for it in; nothing of how a decision behaves follows from its section. Its value says how the thing is written. In the sections of the style a decision with several shapes is a preference and names them (`sameLine`, `nextLine`), while one with `forbidden` or `required` has no second shape to choose: either the other one adds nothing or the project decides it as a policy. Under `upgrading` stands what a version of PHP above the floor decided, `required` writing a construct a newer PHP gave where the older way is found and `forbidden` writing the replacement of what PHP deprecated; under `mistakes` stands what is wrong or dangerous whatever the style. A decision that only refines another one is a parameter (`controlFlow.trailingIfMinStatements`), so that a list a project writes never turns a rule on by itself. -A path reads as a sentence: the section names the area, a group the question or the thing, the last link the place. A group stands only where it says what the section does not, in one of two ways: one question several places answer in one domain, the group naming the question or the thing the places belong to and its keys the places (`braces.position.class`, `multiline.trailingComma.array`, `qualification.function.global`), or one thing several questions are asked of, the group naming the thing and its keys the questions (`classes.modifiers.order`, `file.strictTypes.position`). A requirement whose only name is that of the thing stands flat, its parameters beside it carrying its name and saying what they refine (`controlFlow.trailingIf` and `controlFlow.trailingIfMinStatements`): a group named after the thing would leave the requirement a word that says nothing (`max`, `level`). A group holds a parameter beside requirements with names of their own, or parameters alone where they are one question asked of several places (`spacing.alignment.comma`, `multiline.fillMinItems.array`). A section asking one question of many places has no group (`naming`, `casing`), and a path has four links at most. What a key means is read at first sight, which comes before every other rule of the path: a group is named by a noun, the thing or the question (`strictTypes`, `position`, `trailingComma`), never by an adjective, a participle or a phrase with a preposition, and where no noun reads well the keys stand flat however long (`braces.emptyBody`, `controlFlow.elseAfterReturn`). `keep` on a group keeps every key of it, and a word, a count, a flag or a list on a group is the value of each of its keys of a closed vocabulary (words, counts, flags) that takes it (`multiline.operatorPosition: lineStart`, `qualification.function: imported`), a list giving each key the words of it the key takes, never a text, a list of names, a map or an example; a word no key takes, one that among requirements only a parameter takes, a word of presence (`required`, `forbidden`, `optional`) that a requirement of the group does not take or that is given to a group naming one thing several decisions ask about (`Catalogue::QuestionGroups`, where it would read as one about the thing itself), `[]` and `{}`, a list leaving a requirement none of its words, `keep` or `...` in a list and any value but `keep` on a section are refused. A value a group gives a decision whose rule the target or the packages of the project leave out is skipped silently, as one of a preset (`ResolvedDecision::$cascaded`), while the decision written by its path warns. Where the keys of a group are places, a kind of places stands for all of its places at once (`lists`, `operators`, `blocks`, `Config\Catalogue::PlaceKinds`), a place the same map names winning over its kind, while a later layer replaces whatever its value covers. A key of a configuration is one link of the path, the links nested a key under the other, and `DecisionResolver` refuses a key holding a dot, so that a decision has one way to be written; the dotted path only names a decision (`fixRisky`, `--only`, `dresscode:ignore`, the messages, the name of a map in an upgrading file), and what is given as paths (the header of a fixture, `RuleBuilder::resolveValues()`) is placed into the tree by `Engine\Helpers::placeValue()`. +A path reads as a sentence: the section names the area, a group the question or the thing, the last link the place. A group stands only where it says what the section does not, in one of two ways: one question several places answer in one domain, the group naming the question or the thing the places belong to and its keys the places (`braces.position.class`, `multiline.trailingComma.array`, `qualification.function.global`), or one thing several questions are asked of, the group naming the thing and its keys the questions (`classes.modifiers.order`, `file.strictTypes.position`). A requirement whose only name is that of the thing stands flat, its parameters beside it carrying its name and saying what they refine (`controlFlow.trailingIf` and `controlFlow.trailingIfMinStatements`): a group named after the thing would leave the requirement a word that says nothing (`max`, `level`). A group holds a parameter beside requirements with names of their own, or parameters alone where they are one question asked of several places (`spacing.alignment.comma`, `multiline.fillMinItems.array`). A section asking one question of many places has no group (`naming`, `casing`), and a path has four links at most. What a key means is read at first sight, which comes before every other rule of the path: a group is named by a noun, the thing or the question (`strictTypes`, `position`, `trailingComma`), never by an adjective, a participle or a phrase with a preposition, and where no noun reads well the keys stand flat however long (`braces.emptyBody`, `controlFlow.elseAfterReturn`). `keep` on a group keeps every key of it, and a word, a count, a flag or a list on a group is the value of each of its keys of a closed vocabulary (words, counts, flags) that takes it (`multiline.operatorPosition: lineStart`, `qualification.function: imported`), a list giving each key the words of it the key takes, never a text, a list of names, a map or an example; a word no key takes, one that among requirements only a parameter takes, a word of presence (`required`, `forbidden`, `optional`) that a requirement of the group does not take or that is given to a group naming one thing several decisions ask about (`Catalogue::QuestionGroups`, where it would read as one about the thing itself), `[]` and `{}`, a list leaving a requirement none of its words, `keep` or `...` in a list and any value but `keep` on a section are refused. A value a group gives a decision whose rule the target or the packages of the project leave out is skipped silently, as one of a preset (`ResolvedDecision::$cascaded`), while the decision written by its path warns. Where the keys of a group are places, a kind of places stands for all of its places at once (`lists`, `operators`, `blocks`, `Config\Catalogue::PlaceKinds`), a place the same map names winning over its kind, while a later layer replaces whatever its value covers. A key of a configuration is one link of the path, the links nested a key under the other, and `DecisionResolver` refuses a key holding a dot, so that a decision has one way to be written; the dotted path only names a decision (`fixRisky`, `--only`, `dresscode:ignore`, the messages, the name of a map in an upgrading file), and what is given as paths (the translations of `Interop`, the header of a fixture, `RuleBuilder::resolveValues()`) is placed into the tree by `Engine\Helpers::placeValue()`. Where a thing has a shape for each of several places, each place is a key of its own (`braces.position.class: nextLine`, `naming.method: camelCase`), and a list of places stands where one shape holds for all of them. A place is singular, in a key as in a value, and one place has one word across the catalogue: `class`, `anonymousClass`, `function`, `anonymousFunction`, `arrowFunction`, `method`, `constant`, `property`, `propertyHook`, `variable`, `enumCase`, `parameter`, `argument`, `instantiation`, `array`, `list`, `matchArm`, `closureUse`, `import`, `trait`, `traitUse`, `controlStructure`, `branch`, `switchCase`, `catch`, `return`, `body`, and for what spreads over lines `attribute`, `matchCondition`, `for`, `implements`, `condition`, `chain`, `ternary` and the operators by their family (`logical`, `concatenation`, `arithmetic`, `bitwise`, `coalesce`, `comparison`, `pipe`), a qualifier standing in front of the word where no group names it (`anonymousClass`, `promotedProperty`). The places of one group do not overlap: a narrower place takes its members from the wider one (`interfaceMethod` the methods of an interface from `method`, `anonymousClass` the instantiation of an anonymous class from `instantiation`), so that every occurrence is decided by one key; the places of a group fall into the kinds of `Config\Catalogue::PlaceKinds`, `lists`, `operators` and `blocks`. A key relating places keeps the plural English asks for (`blankLines.aroundMethods`), and so does a list of names the project brings (`mistakes.debugOutputFunctions`). A construct spread over lines takes its shape on two axes, `multiline.items` (`perLine`, `fill`) and `multiline.edges` (`apart`, `attached`, `closingApart`), a value being one shape or a list of the shapes accepted, the first of which is written where DressCode spreads the construct and on the axis whose shape the author's construct does not pass; a construct on one line always passes and a spread one is never joined. Where a token stands is `sameLine` or `nextLine`, an operator at a break `lineStart` or `lineEnd`, so that a new rule speaks in the words a reader knows. @@ -358,3 +358,35 @@ Everything in such a message that is code stands in backticks, as in Markdown: a `Testing\UpgradingTester` is for a package that ships upgrading files: `collectProblems($file, $root, $packageRules)` reads a file the way a run would, in the project whose vendor holds the library, the rules the package ships itself known besides those of DressCode, and gives back sentences. A key under `extra.dresscode` of the nearest `composer.json` at or above the file that `PackageDiscovery` does not read is a problem too (a run only warns of it, the package may be newer than the tool). `collectPackageProblems($root)` holds `extra.dresscode.packages` of the package to exactly the packages its upgrading files are about, by which `upgrade` offers it to a project lacking it. Every section, reached or not, has to be what the grammars of its maps accept, and what the sections come to at the installed version has to replace, in `replacedClasses` and `replacedMembers` and by a static method in `replacedFunctions`, by classes, members and methods that exist, asked of the loaded code (a function of a library is declared by a file loaded with its first use, so it is not looked up); the other maps are held to their grammar alone. A replacement the file replaces in its turn (`REALPATH`, then `Realpath`, then `RealPath`) is followed to its end, the way the passes of a run follow it, and one that leads back to where it started is a problem; an entry withdrawn with `keep` is thereby not checked, and one whose withdrawal was forgotten shows as a replacement that does not exist. What is replaced is not looked up, a library having usually removed it. The sentence of a `forbidden…` map is read as the end of the message it completes: in lower case unless it begins with a name, with its code in backticks, with no period, double quote or "should", and at most 160 characters, the backticks not counted; an entry of a `forbidden…` map that gives none (a project may leave it out) is a problem in the data of a package. `runSample($code, $root, $decisions)` is the other half: code written for the old API as the decisions given fix and report it, every section the installed version reaches applying, whatever the constraint of the project allows, the types taken from the PHPStan of the project, which reads the code from `temp/` of the root, and no result cache; it gives the `FileResult`, so that a package tests its data without reaching for the `@internal` factory of the run. The fixtures of every rule run again under each standard with the sets beside it (`robustness.phpt`), which no fixture of one rule tries: the run must not fail, write code that does not parse or fail to settle, whatever the other rules make of the code. What no fixture catches, the runs over the corpus do: the whitespace fuzz damages the whitespace of a fixed file and requires the fixer to come back to it, and `name-meaning.phpt` runs the rules that write names and imports over the corpus and requires every name outside the imports to resolve to what it resolved to before, because an import moved into a statement writing its items differently, or dropped while something still uses it, leaves code that parses and round-trips and says nothing else. + +## Interop: the rules of other tools + +- `Interop` is the only place that knows the names of PHP CS Fixer and PHP_CodeSniffer. Two tables, one per + configuration format: `PhpCsFixer` holds the fixers (friendsofphp, the custom fixers of kubawerlos) and + `PhpCodeSniffer` the sniffs (the standards shipped with phpcs, slevomat). The two name spaces do not overlap, a fixer is snake_case and a sniff is + `Standard.Category.Name`, so `Translator` searches one merged table and a mixed configuration translates too. + The tables are given to its constructor, the two of the core when none are; `PluginRegistry` holds the + instance the run uses. +- A translation is the decisions a foreign rule stands for, or a closure of the foreign options setting them + (`Translation::set()`, `setAll()`, `setBlankLines()` for a count of blank lines, `setOrder()` for a list whose + sequence carries meaning, `prefer()` for a value another foreign rule may set, and `keep()` for what a foreign rule + turned off stands for, or what a foreign rule governs while allowing either form); it sets only the decisions its + options name, and it warns about what it could not carry over instead of dropping it in silence. The foreign + configuration does not say whether the project has PHPStan, so a decision of a rule with `typesRequired` is left out + with a warning unless its value is `keep`, while `findPaths()` still lists it as covered. Every closure reads its + options through `??`, which is what lets the test call each of them with none and resolve the decisions they set + against the catalogue: a path that does not exist, or a value its domain does not take, fails there. +- Two foreign rules giving one decision a value equally firmly merge in either order: a list becomes the union unless + it is an order, a map merges key by key, a boolean is true when either says so, a count of blank lines widens to the + range of both and says so; the places of the trailing comma and the counts of `blankLines`, split across several + foreign rules, depend on it. A value only preferred, or `keep`, gives way to one set, and values that contradict each + other leave the decision out with a warning. How far a name is written out the foreign tools decide on two axes, the form of a qualified name + (`setQualificationShape()`) and whether a global name stands bare (`setQualificationFallback()`), which `Translation` puts together + into the decisions of `qualification`. A foreign rule turned off makes the requirements it stands for `keep`, unless + another foreign rule the configuration may run stands for them too. +- The tables also answer the reverse question, which decisions a foreign name stands for (`Translator::findPaths()`), + and that is what makes `phpcs:ignore ` work and what the reference and `dresscode catalogue` print as + "covers". A foreign name is not a valid key in the configuration: `dresscode import` translates a whole + configuration file once, options included, printing it as PHP or writing the file it is given in the format of its + extension, NEON or PHP, never over an existing one, and such a name written as a key of the file, in `--only` or in + `fixRisky` is refused with the decisions that cover it. diff --git a/docs/reference/decisions.md b/docs/reference/decisions.md index 2d0610439..043895dab 100644 --- a/docs/reference/decisions.md +++ b/docs/reference/decisions.md @@ -77,6 +77,7 @@ A byte order mark at the start of the file. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Files\NoBomRule`, stage Structure +- Covers: `encoding`, `Generic.Files.ByteOrderMark` ### file.closingTagAtEnd @@ -86,6 +87,7 @@ A `?>` ending a file of PHP only; a `?>` before HTML is content. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Files\NoClosingTagRule`, stage Structure +- Covers: `no_closing_tag`, `PSR2.Files.ClosingTag` ### file.finalLineEndings @@ -95,6 +97,7 @@ Exactly one line ending after the last line; a file ending in HTML is content. - Takes: exactly 1; `keep` - The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Files\FinalLineEndingsRule`, stage Formatting +- Covers: `single_blank_line_at_eof`, `PSR2.Files.EndFileNewline` ### file.lineEnding @@ -104,6 +107,7 @@ The line ending of every line, which the code written new takes too; under `keep - Takes: `LF` (every line ends with LF); `CRLF` (every line ends with CRLF); `majority` (every line ends as most lines of the file do, LF on a tie); `keep` - The standards: perCs `majority`, psr12 `majority`, nette `majority`, symfony `majority`, laravel `majority` - Rule `DressCode\Rules\Files\LineEndingRule`, stage Finishing +- Covers: `line_ending`, `Generic.Files.LineEndings` ### file.lineLength @@ -113,6 +117,7 @@ The widest line, by which what spreads over lines is split. - Takes: a count from 1; `none` (no line is too wide) - The standards: perCs `120`, psr12 `120`, nette `140`, symfony `none`, laravel `none` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing +- Covers: `Generic.Files.LineLength`, `SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature`, `SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition`, `SlevomatCodingStandard.Files.LineLength` ### file.lineLengthExcept @@ -134,6 +139,7 @@ What a line too wide leaves as it is: a construct named is not spread over lines - Read by rule `DressCode\Rules\Functions\MultilineClosureUseRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Functions\MultilineSignatureRule`, which this decision does not turn on - Read by rule `DressCode\Rules\Whitespace\MultilineAttributeRule`, which this decision does not turn on +- Covers: `Generic.Files.LineLength`, `SlevomatCodingStandard.Files.LineLength` ### file.longLines @@ -143,6 +149,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Files\LineLengthRule`, stage Finishing +- Covers: `Generic.Files.LineLength`, `SlevomatCodingStandard.Files.LineLength` ### file.openingTag @@ -152,6 +159,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - Takes: `required` (always there); `keep` - The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Files\OpeningTagNotationRule`, stage Structure +- Covers: `full_opening_tag`, `Generic.PHP.DisallowShortOpenTag` ### file.statementsPerLine @@ -161,6 +169,7 @@ A line wider than `file.lineLength` that nothing could split is reported, unless - Takes: exactly 1; `keep` - The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Whitespace\NoStatementsSharingLineRule`, stage Formatting +- Covers: `no_multiple_statements_per_line`, `Generic.Formatting.DisallowMultipleStatements`, `PSR12.Classes.ClosingBrace`, `PSR2.ControlStructures.SwitchDeclaration` ### file.strictTypes.declaration @@ -170,6 +179,7 @@ Every file of PHP code declares `strict_types=1` as its first statement, a file - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Files\StrictTypesRequiredRule`, stage Structure +- Covers: `declare_strict_types`, `Generic.PHP.RequireStrictTypes`, `SlevomatCodingStandard.TypeHints.DeclareStrictTypes` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### file.strictTypes.position @@ -180,6 +190,7 @@ Where the declaration of `strict_types` stands. - Takes: `sameLine` (` $x'` (no space before the parenthesis); `'fn ($x) => $x'` (a single space); `keep` - The standards: perCs `'fn($x) => $x'`, psr12 `keep`, nette `'fn($x) => $x'`, symfony `'fn ($x) => $x'`, laravel `'fn ($x) => $x'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting +- Covers: `function_declaration`, `SlevomatCodingStandard.Functions.ArrowFunctionDeclaration` ### spacing.attribute @@ -810,6 +874,7 @@ The whitespace inside the brackets of an attribute group and before the parenthe - Takes: `'#[Foo($a)]'` (none inside the brackets, none before the parenthesis); `keep` - The standards: perCs `'#[Foo($a)]'`, psr12 `keep`, nette `'#[Foo($a)]'`, symfony `'#[Foo($a)]'`, laravel `keep` - Rule `DressCode\Rules\Whitespace\AttributeSpacingRule`, stage Formatting +- Covers: `attribute_block_no_spaces` ### spacing.binaryOperator @@ -819,6 +884,7 @@ The spaces around a binary operator, an assignment, `instanceof`, `=>` and the ` - Takes: `'$a + $b'` (a single space around); `keep` - The standards: perCs `'$a + $b'`, psr12 `'$a + $b'`, nette `'$a + $b'`, symfony `'$a + $b'`, laravel `'$a + $b'` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting +- Covers: `binary_operator_spaces`, `PSR12.Operators.OperatorSpacing`, `Squiz.WhiteSpace.LogicalOperatorSpacing`, `Squiz.WhiteSpace.OperatorSpacing` ### spacing.body @@ -837,6 +903,7 @@ The whitespace between the name of a function and its parenthesis, of a call and - Takes: `'foo($a, $b)'` (no space before the parenthesis); `keep` - The standards: perCs `'foo($a, $b)'`, psr12 `'foo($a, $b)'`, nette `'foo($a, $b)'`, symfony `'foo($a, $b)'`, laravel `'foo($a, $b)'` - Rule `DressCode\Rules\Functions\FunctionNameSpacingRule`, stage Formatting +- Covers: `function_declaration`, `no_spaces_after_function_name`, `Generic.Functions.FunctionCallArgumentSpacing.SpaceBeforeOpenBracket`, `PSR2.Methods.FunctionCallSignature`, `Squiz.Functions.FunctionDeclaration` ### spacing.cast @@ -846,6 +913,7 @@ The space between a cast and its operand, which stays on the line of the cast. - Takes: `'(int) $x'` (a single space after the cast); `'(int)$x'` (no space after the cast); `keep` - The standards: perCs `'(int) $x'`, psr12 `'(int) $x'`, nette `'(int) $x'`, symfony `'(int) $x'`, laravel `'(int) $x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting +- Covers: `cast_spaces` ### spacing.catchType @@ -864,6 +932,7 @@ The whitespace around a comma, which stays on the line of what is before it, a l - Takes: `'$a, $b'` (none before, a single space after); `keep` - The standards: perCs `'$a, $b'`, psr12 `'$a, $b'`, nette `'$a, $b'`, symfony `'$a, $b'`, laravel `'$a, $b'` - Rule `DressCode\Rules\Whitespace\CommaSpacingRule`, stage Formatting +- Covers: `no_whitespace_before_comma_in_array`, `whitespace_after_comma_in_array`, `Generic.Functions.FunctionCallArgumentSpacing`, `Squiz.Functions.FunctionDeclarationArgumentSpacing` ### spacing.comment.afterCode @@ -883,6 +952,7 @@ The space after the marker of a comment and before the closing one, unless anoth - Takes: `'// text'` (a single space after the marker); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `'// text'`, symfony `'// text'`, laravel `'// text'` - Rule `DressCode\Rules\Comments\CommentSpacingRule`, stage Finishing +- Covers: `PhpCsFixerCustomFixers/comment_surrounded_by_spaces`, `single_line_comment_spacing` - Modifies comments ### spacing.concatenation @@ -893,6 +963,7 @@ The space around the concatenation operator, unless it sits at a line break. - Takes: `'$a . $b'` (a single space around); `'$a.$b'` (no space around); `keep` - The standards: perCs `'$a . $b'`, psr12 `'$a . $b'`, nette `'$a . $b'`, symfony `'$a.$b'`, laravel `'$a.$b'` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting +- Covers: `concat_space`, `PSR12.Operators.OperatorSpacing`, `Squiz.Strings.ConcatenationSpacing` ### spacing.declare @@ -902,6 +973,7 @@ The whitespace inside a `declare` statement. - Takes: `'declare(strict_types=1)'` (none inside); `keep` - The standards: perCs `'declare(strict_types=1)'`, psr12 `'declare(strict_types=1)'`, nette `'declare(strict_types=1)'`, symfony `'declare(strict_types=1)'`, laravel `'declare(strict_types=1)'` - Rule `DressCode\Rules\Files\DeclareSpacingRule`, stage Formatting +- Covers: `declare_equal_normalize`, `declare_parentheses`, `PSR12.Files.DeclareStatement` ### spacing.doubleColon @@ -911,6 +983,7 @@ The whitespace around the double colon. - Takes: `'Foo::bar()'` (none around); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `'Foo::bar()'`, symfony `keep`, laravel `'Foo::bar()'` - Rule `DressCode\Rules\Expressions\MemberAccessSpacingRule`, stage Formatting +- Covers: `no_space_around_double_colon`, `Squiz.WhiteSpace.ScopeKeywordSpacing` ### spacing.import @@ -930,6 +1003,7 @@ The space after a keyword followed by code on its line, `return`, `new`, `if`, ` - The standards: perCs `'return $x'`, psr12 `'return $x'`, nette `'return $x'`, symfony `'return $x'`, laravel `'return $x'` - Rule `DressCode\Rules\Classes\ClassHeadSpacingRule`, stage Formatting - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting +- Covers: `function_declaration`, `single_space_around_construct`, `Generic.WhiteSpace.LanguageConstructSpacing`, `PSR12.Classes.AnonClassDeclaration`, `PSR12.Traits.UseDeclaration`, `PSR2.Classes.ClassDeclaration`, `PSR2.Classes.ClassDeclaration.SpaceBeforeKeyword`, `PSR2.ControlStructures.SwitchDeclaration`, `Squiz.ControlStructures.ControlSignature`, `Squiz.ControlStructures.ForEachLoopDeclaration`, `Squiz.Functions.FunctionDeclaration` ### spacing.logicalNot @@ -939,6 +1013,7 @@ The whitespace between `!` and its operand, which stay on one line. - Takes: `'!$x'` (no space after the operator); `'! $x'` (a single space after the operator); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `['!$x', '! $x']`, psr12 `['!$x', '! $x']`, nette `'!$x'`, symfony `'!$x'`, laravel `'! $x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting +- Covers: `not_operator_with_successor_space`, `not_operator_without_successor_space`, `unary_operator_spaces`, `SlevomatCodingStandard.Operators.NegationOperatorSpacing`, `Squiz.WhiteSpace.OperatorSpacing.Unary` ### spacing.namedArgument @@ -948,6 +1023,7 @@ The whitespace around the colon of a named argument. - Takes: `'foo(name: $x)'` (none before the colon, a single space after); `keep` - The standards: perCs `'foo(name: $x)'`, psr12 `keep`, nette `'foo(name: $x)'`, symfony `'foo(name: $x)'`, laravel `keep` - Rule `DressCode\Rules\Functions\NamedArgumentSpacingRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Functions.NamedArgumentSpacing` ### spacing.objectOperator @@ -957,6 +1033,7 @@ The whitespace around the object operator. - Takes: `'$a->b()'` (none around); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `'$a->b()'`, symfony `'$a->b()'`, laravel `'$a->b()'` - Rule `DressCode\Rules\Expressions\MemberAccessSpacingRule`, stage Formatting +- Covers: `object_operator_without_whitespace`, `Squiz.WhiteSpace.ObjectOperatorSpacing` ### spacing.openBrace @@ -966,6 +1043,7 @@ The space before the brace or the body a structure opens on the line of its head - Takes: `'if ($a) {'` (a single space); `keep` - The standards: perCs `'if ($a) {'`, psr12 `'if ($a) {'`, nette `'if ($a) {'`, symfony `'if ($a) {'`, laravel `'if ($a) {'` - Rule `DressCode\Rules\Whitespace\ConstructSpacingRule`, stage Formatting +- Covers: `Squiz.ControlStructures.ControlSignature` ### spacing.padding.argument @@ -975,6 +1053,7 @@ The whitespace inside the parentheses of the arguments of a call, of `new` and o - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `function_declaration`, `no_spaces_inside_parenthesis`, `spaces_inside_parentheses`, `PSR12.Classes.AnonClassDeclaration`, `PSR12.ControlStructures.ControlStructureSpacing`, `PSR2.ControlStructures.ControlStructureSpacing`, `PSR2.Methods.FunctionCallSignature`, `Squiz.ControlStructures.ForEachLoopDeclaration`, `Squiz.ControlStructures.ForLoopDeclaration` ### spacing.padding.array @@ -984,6 +1063,7 @@ The whitespace inside the brackets or the parentheses of an array. - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `trim_array_spaces`, `SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace` ### spacing.padding.closureUse @@ -993,6 +1073,7 @@ The whitespace inside the parentheses of the `use` of a closure. - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `function_declaration`, `no_spaces_inside_parenthesis`, `spaces_inside_parentheses`, `PSR12.Classes.AnonClassDeclaration`, `PSR12.ControlStructures.ControlStructureSpacing`, `PSR2.ControlStructures.ControlStructureSpacing`, `PSR2.Methods.FunctionCallSignature`, `Squiz.ControlStructures.ForEachLoopDeclaration`, `Squiz.ControlStructures.ForLoopDeclaration` ### spacing.padding.controlStructure @@ -1002,6 +1083,7 @@ The whitespace inside the parentheses of `if`, `elseif`, `while`, `for`, `foreac - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `function_declaration`, `no_spaces_inside_parenthesis`, `spaces_inside_parentheses`, `PSR12.Classes.AnonClassDeclaration`, `PSR12.ControlStructures.ControlStructureSpacing`, `PSR2.ControlStructures.ControlStructureSpacing`, `PSR2.Methods.FunctionCallSignature`, `Squiz.ControlStructures.ForEachLoopDeclaration`, `Squiz.ControlStructures.ForLoopDeclaration`, `Squiz.WhiteSpace.ControlStructureSpacing` ### spacing.padding.grouping @@ -1011,6 +1093,7 @@ The whitespace inside the parentheses grouping an expression, `($a + $b) * $c`. - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `function_declaration`, `no_spaces_inside_parenthesis`, `spaces_inside_parentheses`, `PSR12.Classes.AnonClassDeclaration`, `PSR12.ControlStructures.ControlStructureSpacing`, `PSR2.ControlStructures.ControlStructureSpacing`, `PSR2.Methods.FunctionCallSignature`, `Squiz.ControlStructures.ForEachLoopDeclaration`, `Squiz.ControlStructures.ForLoopDeclaration` ### spacing.padding.list @@ -1020,6 +1103,7 @@ The whitespace inside the brackets or the parentheses of a destructuring. - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `trim_array_spaces`, `SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace` ### spacing.padding.offset @@ -1029,6 +1113,7 @@ The whitespace inside the brackets of an offset access and before them. - Takes: `forbidden` (never there); `required` (always there); `exceptLiterals` (a single space around an index other than a string or an integer literal, `$a[ $i ]` but `$a[0]`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `no_spaces_around_offset`, `Squiz.Arrays.ArrayBracketSpacing` ### spacing.padding.parameter @@ -1038,6 +1123,7 @@ The whitespace inside the parentheses of the parameters of a function, a method, - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\DelimiterSpacingRule`, stage Formatting +- Covers: `function_declaration`, `no_spaces_inside_parenthesis`, `spaces_inside_parentheses`, `PSR12.Classes.AnonClassDeclaration`, `PSR12.ControlStructures.ControlStructureSpacing`, `PSR2.ControlStructures.ControlStructureSpacing`, `PSR2.Methods.FunctionCallSignature`, `Squiz.ControlStructures.ForEachLoopDeclaration`, `Squiz.ControlStructures.ForLoopDeclaration` ### spacing.reference @@ -1047,6 +1133,7 @@ The whitespace between `&` and its operand. - Takes: `'&$x'` (none between); `keep` - The standards: perCs `'&$x'`, psr12 `'&$x'`, nette `'&$x'`, symfony `'&$x'`, laravel `keep` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting +- Covers: `SlevomatCodingStandard.PHP.ReferenceSpacing` ### spacing.semicolon.after @@ -1056,6 +1143,7 @@ The whitespace after a semicolon that more code follows on its line. - Takes: `'for ($i = 0; $i < 5; $i++)'` (a single space after a semicolon); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `'for ($i = 0; $i < 5; $i++)'`, symfony `'for ($i = 0; $i < 5; $i++)'`, laravel `'for ($i = 0; $i < 5; $i++)'` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting +- Covers: `space_after_semicolon`, `Squiz.ControlStructures.ForLoopDeclaration` ### spacing.semicolon.before @@ -1065,6 +1153,7 @@ The whitespace before a semicolon, which stays on the line of what it ends. - Takes: `'$a;'` (no space before a semicolon); `keep` - The standards: perCs `'$a;'`, psr12 `keep`, nette `'$a;'`, symfony `'$a;'`, laravel `'$a;'` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting +- Covers: `no_singleline_whitespace_before_semicolons`, `PSR12.Traits.UseDeclaration`, `Squiz.ControlStructures.ForLoopDeclaration`, `Squiz.WhiteSpace.SemicolonSpacing` ### spacing.spread @@ -1074,6 +1163,7 @@ The whitespace between `...` and its operand. - Takes: `'...$x'` (none between); `keep` - The standards: perCs `'...$x'`, psr12 `'...$x'`, nette `'...$x'`, symfony `'...$x'`, laravel `keep` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Operators.SpreadOperatorSpacing` ### spacing.switchCase @@ -1083,6 +1173,7 @@ The whitespace before the colon of a `case`. - Takes: `'case 1:'` (none before the colon); `keep` - The standards: perCs `'case 1:'`, psr12 `'case 1:'`, nette `'case 1:'`, symfony `'case 1:'`, laravel `'case 1:'` - Rule `DressCode\Rules\ControlFlow\SwitchCaseSpacingRule`, stage Formatting +- Covers: `switch_case_space`, `PSR2.ControlStructures.SwitchDeclaration`, `PSR2.ControlStructures.SwitchDeclaration.SpaceBeforeColonCASE` ### spacing.ternary @@ -1092,6 +1183,7 @@ The whitespace around `?` and `:` of a ternary, and around `?:` as a whole, unle - Takes: `'$a ? $b : $c'` (whitespace around `?` and `:`); `keep` - The standards: perCs `'$a ? $b : $c'`, psr12 `'$a ? $b : $c'`, nette `'$a ? $b : $c'`, symfony `'$a ? $b : $c'`, laravel `'$a ? $b : $c'` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting +- Covers: `ternary_operator_spaces`, `PSR12.Operators.OperatorSpacing` ### spacing.typeDeclaration @@ -1101,6 +1193,7 @@ The whitespace of a type declaration, which stays on one line with what it descr - Takes: `'?int $x'` (none inside a type, a single space before the name); `keep` - The standards: perCs `'?int $x'`, psr12 `'?int $x'`, nette `'?int $x'`, symfony `'?int $x'`, laravel `'?int $x'` - Rule `DressCode\Rules\Types\TypeDeclarationSpacingRule`, stage Formatting +- Covers: `compact_nullable_type_declaration`, `return_type_declaration`, `type_declaration_spaces`, `types_spaces`, `PSR12.Functions.NullableTypeDeclaration`, `PSR12.Functions.ReturnTypeDeclaration`, `SlevomatCodingStandard.Classes.BackedEnumTypeSpacing`, `SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing`, `SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing` ### spacing.unaryOperator @@ -1110,6 +1203,7 @@ The whitespace between a unary operator other than `!` and its operand, which st - Takes: `'-$x'` (no space after the operator); `keep` - The standards: perCs `'-$x'`, psr12 `'-$x'`, nette `'-$x'`, symfony `'-$x'`, laravel `'-$x'` - Rule `DressCode\Rules\Expressions\OperandSpacingRule`, stage Formatting +- Covers: `unary_operator_spaces`, `Generic.WhiteSpace.IncrementDecrementSpacing`, `Squiz.WhiteSpace.OperatorSpacing.Unary` ## multiline @@ -1121,6 +1215,7 @@ The attributes of a class, a function, a method, a property, a constant, a case - Takes: `above` (every group on a line of its own above the declaration); `sameLine` (every group on the line of the declaration); `keep` - The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `above`, laravel `keep` - Rule `DressCode\Rules\Whitespace\AttributePositionRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Attributes.DisallowMultipleAttributesPerLine` ### multiline.attributePosition.parameter @@ -1130,6 +1225,7 @@ Where the attributes of a parameter stand. - Takes: `above` (every group on a line of its own above the parameter in a list of parameters spread over lines, on the line of the parameter in a list on one line); `sameLine` (every group on the line of the parameter, in a list spread over lines too); `keep` - The standards: perCs `above`, psr12 `keep`, nette `above`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\AttributePositionRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Attributes.DisallowMultipleAttributesPerLine` ### multiline.edges.argument @@ -1139,6 +1235,7 @@ Where the arguments spread over lines begin against `(` and where `)` stands. - Takes: `apart` (the first argument on the line after `(`, `)` on a line of its own); `attached` (the first argument on the line of `(`, `)` right after the last one); `closingApart` (the first argument on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting +- Covers: `method_argument_space`, `PSR2.Methods.FunctionCallSignature` ### multiline.edges.array @@ -1148,6 +1245,7 @@ Where the items spread over lines begin against the opening bracket and where th - Takes: `apart` (the first item on the line after the opening bracket, the closing one on a line of its own); `attached` (the first item on the line of the opening bracket, the closing one right after the last one); `closingApart` (the first item on the line of the opening bracket, the closing one on a line of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `apart`, psr12 `keep`, nette `[apart, attached]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Arrays\MultilineArrayRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Arrays.MultiLineArrayEndBracketPlacement` ### multiline.edges.attribute @@ -1184,6 +1282,7 @@ Where the parts spread over lines begin against `(` and where `)` stands. - Takes: `apart` (the first part on the line after `(`, `)` on a line of its own); `attached` (the first part on the line of `(`, `)` right after the last one); `closingApart` (the first part on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `apart`, psr12 `apart`, nette `[apart, closingApart]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting +- Covers: `PSR12.ControlStructures.ControlStructureSpacing`, `SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition` ### multiline.edges.implements @@ -1193,6 +1292,7 @@ Where the interfaces spread over lines begin against the keyword. - Takes: `apart` (the first interface on the line after the keyword); `attached` (the first interface on the line of the keyword); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\MultilineImplementsRule`, stage Formatting +- Covers: `PSR12.Classes.AnonClassDeclaration`, `PSR2.Classes.ClassDeclaration` ### multiline.edges.import @@ -1220,6 +1320,7 @@ Where the parameters spread over lines begin against `(` and where `)` stands. - Takes: `apart` (the first parameter on the line after `(`, `)` on a line of its own); `attached` (the first parameter on the line of `(`, `)` right after the last one); `closingApart` (the first parameter on the line of `(`, `)` on a line of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `apart`, psr12 `apart`, nette `apart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature`, `Squiz.Functions.MultiLineFunctionDeclaration` ### multiline.expressionStart.arrayItem @@ -1330,6 +1431,7 @@ A sole argument that is a string, a message, is not spread for its width - Takes: `perLine` (every argument on a line of its own); `fill` (the arguments may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineCallRule`, stage Formatting +- Covers: `method_argument_space`, `PSR2.Methods.FunctionCallSignature` ### multiline.items.arithmetic @@ -1421,6 +1523,7 @@ A condition of `if`, `elseif`, `while` and `do-while` joined by boolean operator - Takes: `perLine` (every part on a line of its own); `fill` (the parts may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `[perLine, fill]`, psr12 `[perLine, fill]`, nette `[perLine, fill]`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting +- Covers: `PSR12.ControlStructures.ControlStructureSpacing`, `SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition` ### multiline.items.for @@ -1439,6 +1542,7 @@ The interfaces a class or an enum implements, or an interface extends, spread ov - Takes: `perLine` (every interface on a line of its own); `fill` (the interfaces may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\MultilineImplementsRule`, stage Formatting +- Covers: `PSR12.Classes.AnonClassDeclaration`, `PSR2.Classes.ClassDeclaration` ### multiline.items.import @@ -1503,6 +1607,7 @@ The parameters of a function or a method spread over lines by their author or fo - Takes: `perLine` (every parameter on a line of its own); `fill` (the parameters may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `perLine`, psr12 `perLine`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature`, `Squiz.Functions.MultiLineFunctionDeclaration` ### multiline.items.pipe @@ -1523,6 +1628,7 @@ A ternary spread over lines by its author, or for the width of its line. - Takes: `perLine` (every operand on a line of its own); `fill` (the operands may share lines, one on a line passing too, and one spanning lines stands on lines of its own); a list of them, every one passing and the first written where none matches; `keep` - The standards: perCs `perLine`, psr12 `keep`, nette `perLine`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineTernaryRule`, stage Formatting +- Covers: `SlevomatCodingStandard.ControlStructures.RequireMultiLineTernaryOperator` ### multiline.maxWidth.array @@ -1541,6 +1647,7 @@ Where `+`, `-`, `*`, `/`, `%` and `**` stand at a line break. - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` - The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting +- Covers: `operator_linebreak` ### multiline.operatorPosition.bitwise @@ -1550,6 +1657,7 @@ Where `|`, `&`, `^`, `<<` and `>>` stand at a line break. - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` - The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting +- Covers: `operator_linebreak` ### multiline.operatorPosition.coalesce @@ -1559,6 +1667,7 @@ Where `??` stands at a line break. - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` - The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting +- Covers: `operator_linebreak` ### multiline.operatorPosition.comparison @@ -1569,6 +1678,7 @@ Where a comparison stands at a line break: one ending its line moves only where - The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\BinaryOperatorSpacingRule`, stage Formatting - Read by rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, which this decision does not turn on +- Covers: `operator_linebreak` ### multiline.operatorPosition.concatenation @@ -1578,6 +1688,7 @@ Where `.` stands at a line break. - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` - The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting +- Covers: `operator_linebreak` ### multiline.operatorPosition.condition @@ -1587,6 +1698,7 @@ Where a boolean operator chaining a condition of `if`, `elseif`, `while` and `do - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` - The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\MultilineConditionRule`, stage Formatting +- Covers: `operator_linebreak`, `PSR12.ControlStructures.BooleanOperatorPlacement` ### multiline.operatorPosition.logical @@ -1596,6 +1708,7 @@ Where `&&`, `||`, `and`, `or` and `xor` outside a condition stand at a line brea - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` - The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `lineStart`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting +- Covers: `operator_linebreak` ### multiline.operatorPosition.pipe @@ -1605,6 +1718,7 @@ Where `|>` stands at a line break. - Takes: `lineStart` (one ending a line opens the next, unless a comment follows it); `lineEnd` (one opening a line ends the one before, unless a comment precedes it); `keep` - The standards: perCs `lineStart`, psr12 `lineStart`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\MultilineBinaryOperatorRule`, stage Formatting +- Covers: `operator_linebreak` ### multiline.operatorPosition.ternary @@ -1615,6 +1729,7 @@ Where `?`, `:` and `?:` of a ternary stand at a line break, of a ternary spread - The standards: perCs `lineStart`, psr12 `keep`, nette `lineStart`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\TernaryOperatorSpacingRule`, stage Formatting - Read by rule `DressCode\Rules\Expressions\MultilineTernaryRule`, which this decision does not turn on +- Covers: `operator_linebreak`, `SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperator` ### multiline.semicolon @@ -1624,6 +1739,7 @@ Where the semicolon of a statement spanning several lines stands. - Takes: `sameLine` (closing the last line of the statement); `nextLine` (on a line of its own below a chain of method calls spread over lines, closing the last line of any other statement); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `sameLine`, symfony `keep`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\SemicolonSpacingRule`, stage Formatting +- Covers: `multiline_whitespace_before_semicolons`, `Squiz.WhiteSpace.SemicolonSpacing` ### multiline.spreadAlways @@ -1633,6 +1749,7 @@ What is spread over lines whatever its width. - Takes: a list of `promotedProperty` (the parameters of a signature declaring a promoted property) - The standards: perCs `[]`, psr12 `[]`, nette `[promotedProperty]`, symfony `[]`, laravel `[]` - Rule `DressCode\Rules\Functions\MultilineSignatureRule`, stage Formatting +- Covers: `multiline_promoted_properties`, `SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature` ### multiline.spreadItem @@ -1651,6 +1768,7 @@ The trailing comma of the multi-line arguments of a call and the variables of `i - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` - The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure +- Covers: `no_trailing_comma_in_singleline`, `SlevomatCodingStandard.Functions.RequireTrailingCommaInCall` ### multiline.trailingComma.array @@ -1660,6 +1778,7 @@ The trailing comma of a multi-line array, a destructuring written with `[...]` i - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` - The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure +- Covers: `no_trailing_comma_in_singleline`, `trailing_comma_in_multiline`, `SlevomatCodingStandard.Arrays.TrailingArrayComma` ### multiline.trailingComma.closureUse @@ -1678,6 +1797,7 @@ The trailing comma of the names of a multi-line group use; any value but `keep` - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` - The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure +- Covers: `no_trailing_comma_in_singleline` ### multiline.trailingComma.list @@ -1687,6 +1807,7 @@ The trailing comma of a multi-line `list()`; any value but `keep` also removes t - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` - The standards: perCs `optional`, psr12 `keep`, nette `required`, symfony `optional`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure +- Covers: `no_trailing_comma_in_singleline` ### multiline.trailingComma.matchArm @@ -1705,6 +1826,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - Takes: `required` (always there where the closing bracket stands on its own line, never where it follows the last item); `optional` (as the author wrote it where the closing bracket stands on its own line, never where it follows the last item); `keep` - The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `required`, laravel `optional` - Rule `DressCode\Rules\Arrays\TrailingCommaRule`, stage Structure +- Covers: `function_declaration`, `SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration` ## braces @@ -1716,6 +1838,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\NoAlternativeSyntaxRule`, stage Structure +- Covers: `no_alternative_syntax` ### braces.bareStatementGroup @@ -1725,6 +1848,7 @@ The trailing comma of a multi-line list of parameters; any value but `keep` also - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\UselessBracesRule`, stage Structure +- Covers: `no_unneeded_braces`, `no_unneeded_curly_braces` ### braces.bracelessBody @@ -1734,6 +1858,7 @@ The body of a control structure written without braces, `if ($a) foo();`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\NoBracelessBodiesRule`, stage Structure +- Covers: `control_structure_braces`, `Generic.ControlStructures.InlineControlStructure` - Modifies comments ### braces.emptyBody.anonymousClass @@ -1744,6 +1869,7 @@ How the empty body of an anonymous class is written, one written on one line sta - Takes: `collapsed` (`{}` on the line of the head, one holding a comment staying on its line as written); `expanded` (`{` and `}` placed as those of any other body); `keep` - The standards: perCs `collapsed`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position`, `single_line_empty_body` ### braces.emptyBody.class @@ -1753,6 +1879,7 @@ How the empty body of a class, an interface, a trait and an enum is written; a c - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` - The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `single_line_empty_body` ### braces.emptyBody.closure @@ -1762,6 +1889,7 @@ How the empty body of a closure is written; a comment inside makes it not empty. - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` - The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `single_line_empty_body` ### braces.emptyBody.controlStructure @@ -1780,6 +1908,7 @@ How the empty body of a function and a method is written; a comment inside makes - Takes: `collapsed` (`{}` on the line of the head); `expanded` (`{` and `}` placed as those of any other body); `keep` - The standards: perCs `collapsed`, psr12 `expanded`, nette `expanded`, symfony `expanded`, laravel `collapsed` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `single_line_empty_body` ### braces.emptyBody.propertyHook @@ -1798,6 +1927,7 @@ Where the `{` of an anonymous class stands, on the next line always where its in - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` - The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position`, `PSR12.Classes.AnonClassDeclaration` ### braces.position.class @@ -1807,6 +1937,7 @@ Where the `{` of a class, an interface, a trait and an enum stands, the members - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` - The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine`, laravel `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position`, `PSR2.Classes.ClassDeclaration` ### braces.position.closure @@ -1816,6 +1947,7 @@ Where the `{` of a closure stands, the body starting a line below it and the `}` - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` - The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position`, `Squiz.Functions.MultiLineFunctionDeclaration` ### braces.position.continuingKeyword @@ -1825,6 +1957,7 @@ Where the keyword continuing a structure after its `}` stands: `else`, `elseif`, - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` - The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `control_structure_continuation_position`, `Squiz.ControlStructures.ControlSignature` ### braces.position.controlStructure @@ -1834,6 +1967,7 @@ Where the `{` of `if`, `else`, a loop, `declare`, `try`, `catch`, `finally`, `sw - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` - The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position`, `PSR12.Files.DeclareStatement`, `Squiz.ControlStructures.ControlSignature` ### braces.position.function @@ -1843,6 +1977,7 @@ Where the `{` of a function and of a method whose parameters stand on one line s - Takes: `sameLine` (on the line of what comes before); `nextLine` (on the next line); `keep` - The standards: perCs `nextLine`, psr12 `nextLine`, nette `nextLine`, symfony `nextLine`, laravel `nextLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position`, `Squiz.Functions.MultiLineFunctionDeclaration` ### braces.position.multilineSignature @@ -1852,6 +1987,7 @@ Where the `{` of a function and of a method whose parameters are spread over lin - Takes: `sameLine` (on the line of the `)`); `nextLine` (on the line below the `)`); `keep` - The standards: perCs `sameLine`, psr12 `sameLine`, nette `sameLine`, symfony `sameLine`, laravel `sameLine` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position`, `Squiz.Functions.MultiLineFunctionDeclaration` ### braces.position.typedMultilineSignature @@ -1870,6 +2006,7 @@ A closure written whole on one line. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `keep`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\Whitespace\BracesPositionRule`, stage Formatting +- Covers: `braces_position` ## blankLines @@ -1881,6 +2018,7 @@ Between the last group of attributes and the declaration, the member, the parame - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` - The standards: perCs `0`, psr12 `keep`, nette `0`, symfony `0`, laravel `keep` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting +- Covers: `SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing` ### blankLines.afterDeclare @@ -1890,6 +2028,7 @@ After `declare(...);`, before whatever follows it but a namespace declaration. - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` - The standards: perCs `1`, psr12 `1`, nette `1`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting +- Covers: `PSR12.Files.FileHeader` ### blankLines.afterImports @@ -1899,6 +2038,7 @@ After the last import, before the rest of the code. - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` - The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting +- Covers: `single_line_after_imports`, `PSR12.Files.FileHeader`, `PSR2.Namespaces.UseDeclaration` ### blankLines.afterNamespace @@ -1908,6 +2048,7 @@ After an unbraced namespace declaration. - Takes: a count from 0, a range `1-2`, an open one `1+`; `keep` - The standards: perCs `1`, psr12 `1`, nette `1`, symfony `1`, laravel `1` - Rule `DressCode\Rules\Whitespace\StatementBlankLinesRule`, stage Formatting +- Covers: `blank_line_after_namespace`, `PSR12.Files.FileHeader`, `PSR2.Namespaces.NamespaceDeclaration`, `SlevomatCodingStandard.Namespaces.NamespaceSpacing` ### blankLines.afterOpeningTag @@ -1917,6 +2058,7 @@ After the line of `bar - Takes: `forbidden` (never there); `required` (always there); `keep` - The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\InstantiationWrappingRule`, stage Structure +- Covers: `new_expression_parentheses` - Needs PHP >=8.4 ### classes.internalClass.except @@ -2290,6 +2463,7 @@ The `final` of a class its author marked as internal, which nothing outside the - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\FinalForInternalClassRule`, stage Structure +- Covers: `final_internal_class` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### classes.internalClass.markers @@ -2309,6 +2483,7 @@ A declaration of several constants of a class, `const A = 1, B = 2;`. - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` - The standards: perCs `separated`, psr12 `keep`, nette `keep`, symfony `separated`, laravel `separated` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure +- Covers: `single_class_element_per_statement`, `SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition` ### classes.members.grouping.property @@ -2318,6 +2493,7 @@ A declaration of several properties, `public $a, $b;`. - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` - The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated`, laravel `separated` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure +- Covers: `single_class_element_per_statement`, `SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition` ### classes.members.grouping.traitUse @@ -2327,6 +2503,7 @@ A `use` of several traits, `use A, B;`. - Takes: `separated` (one member per declaration, each sharing the modifiers and the type of the original); `keep` - The standards: perCs `separated`, psr12 `separated`, nette `separated`, symfony `separated`, laravel `keep` - Rule `DressCode\Rules\Classes\NoGroupedDeclarationsRule`, stage Structure +- Covers: `single_trait_insert_per_statement`, `PSR12.Traits.UseDeclaration`, `SlevomatCodingStandard.Classes.TraitUseDeclaration` ### classes.members.order @@ -2336,6 +2513,7 @@ The order of the members of a class by kind, a member taking the most particular - Takes: an order of `traitUse` (a trait use); `enumCase` (a case of an enum); `constant` (a constant of any visibility); `publicConstant` (a public constant); `protectedConstant` (a protected constant); `privateConstant` (a private constant); `property` (a property of any visibility); `publicProperty` (a public property); `protectedProperty` (a protected property); `privateProperty` (a private property); `publicStaticProperty` (a public static property); `protectedStaticProperty` (a protected static property); `privateStaticProperty` (a private static property); `method` (a method of any visibility); `publicMethod` (a public method); `protectedMethod` (a protected method); `privateMethod` (a private method); `publicStaticMethod` (a public static method); `protectedStaticMethod` (a protected static method); `privateStaticMethod` (a private static method); `constructor` (the constructor); `destructor` (the destructor); `magicMethod` (a method whose name begins with `__`); `keep` - The standards: perCs `[traitUse]`, psr12 `[traitUse]`, nette `[traitUse, enumCase, publicConstant, protectedConstant, privateConstant, publicProperty, protectedProperty, privateProperty]`, symfony `[traitUse]`, laravel `keep` - Rule `DressCode\Rules\Classes\MemberOrderRule`, stage Structure +- Covers: `ordered_class_elements`, `PSR12.Traits.UseDeclaration` ### classes.modifiers.implied @@ -2345,6 +2523,7 @@ A modifier PHP implies anyway, `final` in a final class, `readonly` in a readonl - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\UselessModifierRule`, stage Structure +- Covers: `no_redundant_readonly_property`, `no_unneeded_final_method`, `Generic.CodeAnalysis.UnnecessaryFinalModifier` ### classes.modifiers.order @@ -2354,6 +2533,7 @@ The order of the modifiers of a property, a promoted one included, a method, a c - Takes: `canonical` (`abstract` or `final`, visibility, set visibility, `static`, `readonly`); `keep` - The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical`, laravel `canonical` - Rule `DressCode\Rules\Classes\ModifierOrderRule`, stage Structure +- Covers: `modifier_keywords`, `visibility_required`, `PSR2.Classes.PropertyDeclaration`, `PSR2.Methods.MethodDeclaration`, `SlevomatCodingStandard.Classes.PropertyDeclaration` ### classes.overridingMethod.parameterName @@ -2407,6 +2587,7 @@ A `protected` member of an enum, which is `private`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Classes\PrivateForProtectedRule`, stage Structure +- Covers: `protected_to_private` ### classes.protectedInFinalClass @@ -2416,6 +2597,7 @@ A `protected` member of a final class with no parent and no trait, which is `pri - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Classes\PrivateForProtectedRule`, stage Structure +- Covers: `protected_to_private` ### classes.staticMethod @@ -2425,6 +2607,7 @@ The `static` keyword of a method that does not use `$this`, of the visibilities - Takes: `required` (always there, `static function`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\StaticForMethodWithoutThisRule`, stage Structure +- Covers: `static_private_method` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### classes.staticMethodVisibilities @@ -2444,6 +2627,7 @@ The visibilities of the methods made static. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Classes\UselessNullInitializationRule`, stage Structure +- Covers: `no_null_property_initialization` ### classes.visibility.constant @@ -2453,6 +2637,7 @@ The visibility of a constant of a class, an interface, a trait or an enum. - Takes: `required` (always there); `keep` - The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `keep` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure +- Covers: `modifier_keywords`, `visibility_required`, `PSR12.Properties.ConstantVisibility`, `PSR2.Classes.PropertyDeclaration`, `SlevomatCodingStandard.Classes.ClassConstantVisibility`, `Squiz.Scope.MethodScope` ### classes.visibility.interfaceMethod @@ -2462,6 +2647,7 @@ The `public` of a method of an interface, which is public whatever it says. - Takes: `required` (always there); `forbidden` (never there); `keep` - The standards: perCs `required`, psr12 `required`, nette `forbidden`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure +- Covers: `modifier_keywords`, `visibility_required`, `Squiz.Scope.MethodScope` ### classes.visibility.method @@ -2471,6 +2657,7 @@ The visibility of a method outside an interface, whose methods take `interfaceMe - Takes: `required` (always there); `keep` - The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure +- Covers: `modifier_keywords`, `visibility_required`, `PSR12.Properties.ConstantVisibility`, `PSR2.Classes.PropertyDeclaration`, `SlevomatCodingStandard.Classes.ClassConstantVisibility`, `Squiz.Scope.MethodScope` ### classes.visibility.property @@ -2480,6 +2667,7 @@ The visibility of a property, a promoted one included, `var` becoming `public` a - Takes: `required` (always there); `keep` - The standards: perCs `required`, psr12 `required`, nette `required`, symfony `required`, laravel `required` - Rule `DressCode\Rules\Classes\VisibilityRequiredRule`, stage Structure +- Covers: `modifier_keywords`, `visibility_required`, `PSR12.Properties.ConstantVisibility`, `PSR2.Classes.PropertyDeclaration`, `SlevomatCodingStandard.Classes.ClassConstantVisibility`, `Squiz.Scope.MethodScope` ### classes.visibility.publicWithSet @@ -2503,6 +2691,7 @@ Not an arrow function, whose value is its expression. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure +- Covers: `void_return`, `SlevomatCodingStandard.TypeHints.ReturnTypeHint` - Modifies comments ### types.declaration.constant @@ -2526,6 +2715,7 @@ Not a parameter of a closure, whose types belong to the call it is passed to. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure +- Covers: `phpdoc_to_param_type`, `SlevomatCodingStandard.TypeHints.ParameterTypeHint` - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2537,6 +2727,7 @@ A property declares its type, a promoted one included, taken from `@var`, or the - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure +- Covers: `phpdoc_to_property_type`, `SlevomatCodingStandard.TypeHints.PropertyTypeHint` - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2548,6 +2739,7 @@ A function or a method declares its return type, taken from `@return`, `void` wh - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure +- Covers: `phpdoc_to_return_type`, `void_return`, `SlevomatCodingStandard.TypeHints.ReturnTypeHint` - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2559,6 +2751,7 @@ How a native type of one type and `null` is written. - Takes: `'?int'` (a single type with `null` written with `?`); `'int|null'` (a union with `null`, which `types.nullPosition` places); `keep` - The standards: perCs `'?int'`, psr12 `keep`, nette `'?int'`, symfony `'?int'`, laravel `'?int'` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure +- Covers: `nullable_type_declaration` ### types.nullPosition @@ -2568,6 +2761,7 @@ Where `null` stands in a native union type. - Takes: `last` (`int|string|null`); `first` (`null|int|string`); `keep` - The standards: perCs `last`, psr12 `keep`, nette `last`, symfony `last`, laravel `keep` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure +- Covers: `ordered_types` ### types.traversableClasses @@ -2588,6 +2782,7 @@ The order of the types of a native union. - Takes: `alphabetical` (the types beside `null` sorted by name, case-insensitively, an intersection by its first name); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\TypeNotationRule`, stage Structure +- Covers: `ordered_types` ## functions @@ -2610,6 +2805,7 @@ Not about the `static` of the arrow function, which `functions.staticClosure` de - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure +- Covers: `use_arrow_functions`, `SlevomatCodingStandard.Functions.RequireArrowFunction` ### functions.arrowFunctionNested @@ -2619,6 +2815,7 @@ Whether a closure holding another closure or arrow function becomes one too. - Takes: `yes`; `no` - The standards: perCs `no`, psr12 `no`, nette `yes`, symfony `no`, laravel `no` - Rule `DressCode\Rules\Functions\ArrowFunctionForClosureRule`, stage Structure +- Covers: `SlevomatCodingStandard.Functions.RequireArrowFunction` ### functions.innerFunction @@ -2628,6 +2825,7 @@ A named function declared inside a function, a method, a closure or a property h - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoInnerFunctionsRule`, stage Structure +- Covers: `Squiz.PHP.InnerFunctions` ### functions.namedBooleanArgument @@ -2656,6 +2854,7 @@ The `static` keyword of a closure or an arrow function that does not use `$this` - Takes: `required` (always there, `static function`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\StaticForClosureWithoutThisRule`, stage Structure +- Covers: `static_lambda`, `SlevomatCodingStandard.Functions.StaticClosure` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### functions.trailingBareReturn @@ -2666,6 +2865,7 @@ The `static` keyword of a closure or an arrow function that does not use `$this` - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\UselessReturnRule`, stage Structure +- Covers: `no_useless_return` ### functions.unreachableParameterDefault @@ -2675,6 +2875,7 @@ A default a required parameter after it makes unreachable. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\Functions\UselessParameterDefaultRule`, stage Structure +- Covers: `no_unreachable_default_argument_value`, `PEAR.Functions.ValidDefaultValue`, `SlevomatCodingStandard.Functions.UselessParameterDefaultValue` ## hygiene @@ -2686,6 +2887,7 @@ The sets of functions whose aliases are written by their canonical names. - Takes: a list of `all` (every set below); `core` (the core functions, `sizeof()` is `count()`, `join()` is `implode()`); `imap` (`imap_*`); `ldap` (`ldap_*`); `mysqli` (`mysqli_*`); `pg` (`pg_exec()` is `pg_query()`); `oci` (`oci_*`); `odbc` (`odbc_*`); `openssl` (`openssl_*`); `sodium` (`sodium_*`); `ftp` (`ftp_quit()` is `ftp_close()`); `posix` (`posix_errno()` is `posix_get_last_error()`); `pcntl` (`pcntl_errno()` is `pcntl_get_last_error()`); `time` (`mktime()` and `gmmktime()` without arguments are `time()`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `[core, imap, pg]`, symfony `keep`, laravel `[core, imap, pg]` - Rule `DressCode\Rules\Functions\NoAliasFunctionsRule`, stage Structure +- Covers: `no_alias_functions` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### hygiene.call_user_func @@ -2696,6 +2898,7 @@ The sets of functions whose aliases are written by their canonical names. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoCallUserFuncRule`, stage Structure +- Covers: `regular_callable_call` - Needs PHP >=7.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2717,6 +2920,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ClassNameNotationRule`, stage Structure +- Covers: `SlevomatCodingStandard.Classes.ModernClassNameReference` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### hygiene.classNameNotation.object @@ -2727,6 +2931,7 @@ Whether a method the callable names as `[$this, 'run']`, `[self::class, 'make']` - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ClassNameNotationRule`, stage Structure +- Covers: `get_class_to_class_keyword`, `SlevomatCodingStandard.Classes.ModernClassNameReference` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### hygiene.conversionFunction @@ -2739,6 +2944,7 @@ Not about which name of `floatval()` the code writes, `doubleval()` being an ali - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoConversionFunctionsRule`, stage Structure +- Covers: `modernize_types_casting` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### hygiene.defaultArgument @@ -2759,6 +2965,7 @@ An argument PHP ignores or that repeats the default. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoDirnameOfFileRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/no_useless_dirname_call`, `combine_nested_dirname`, `dir_constant` ### hygiene.is_null @@ -2768,6 +2975,7 @@ An argument PHP ignores or that repeats the default. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoIsNullRule`, stage Structure +- Covers: `is_null` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### hygiene.nestedDirname @@ -2798,6 +3006,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoSeparateIssetsRule`, stage Structure +- Covers: `combine_consecutive_issets` ### hygiene.separateUnsets @@ -2807,6 +3016,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoSeparateUnsetsRule`, stage Structure +- Covers: `combine_consecutive_unsets` ### hygiene.settype @@ -2816,6 +3026,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoSettypeRule`, stage Structure +- Covers: `set_type_to_cast` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### hygiene.strlenEmptyTest @@ -2826,6 +3037,7 @@ Two adjacent catches of one body, written as one `catch (A|B $e)`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoManualEmptyStringTestsRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/no_useless_strlen`, `string_length_to_empty` - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -2837,6 +3049,7 @@ The variable of a `catch` that is never read, `catch (E)` written instead. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\UselessCatchVariableRule`, stage Structure +- Covers: `SlevomatCodingStandard.Exceptions.RequireNonCapturingCatch` - Needs PHP >=8.0 ### hygiene.__invokeCall @@ -2847,6 +3060,7 @@ The variable of a `catch` that is never read, `catch (E)` written instead. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoExplicitInvokeCallsRule`, stage Structure +- Covers: `SlevomatCodingStandard.PHP.DisallowDirectMagicInvokeCall` ## controlFlow @@ -2860,6 +3074,7 @@ Not about an `if` and an `else` each returning a value, which `controlFlow.ifEls - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\UselessElseRule`, stage Structure +- Covers: `no_useless_else`, `SlevomatCodingStandard.ControlStructures.EarlyExit` ### controlFlow.elseif @@ -2869,6 +3084,7 @@ The keyword of a branch that follows `if`. - Takes: `'elseif'` (`elseif` in one word, never `else if`); `keep` - The standards: perCs `'elseif'`, psr12 `'elseif'`, nette `'elseif'`, symfony `'elseif'`, laravel `'elseif'` - Rule `DressCode\Rules\ControlFlow\ElseifNotationRule`, stage Structure +- Covers: `elseif`, `PSR2.ControlStructures.ElseIfDeclaration` ### controlFlow.elseifAfterReturn @@ -2878,6 +3094,7 @@ An `elseif` after an `if` that always leaves, which becomes an `if` of its own w - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\UselessElseRule`, stage Structure +- Covers: `no_superfluous_elseif`, `SlevomatCodingStandard.ControlStructures.EarlyExit` ### controlFlow.emptyStatement @@ -2887,6 +3104,7 @@ The second `;` of `foo();;`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\NoEmptyStatementsRule`, stage Structure +- Covers: `no_empty_statement`, `SlevomatCodingStandard.PHP.UselessSemicolon` ### controlFlow.ifElseReturning @@ -2898,6 +3116,7 @@ Not about the ternary of `true` and `false` it may write, which `expressions.ter - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\TernaryForIfRule`, stage Structure +- Covers: `SlevomatCodingStandard.ControlStructures.RequireTernaryOperator` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### controlFlow.ifReturningTrueFalse @@ -2910,6 +3129,7 @@ Not about branches returning other values, which `controlFlow.ifElseReturning` w - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\ReturnForBooleanIfRule`, stage Structure +- Covers: `simplified_if_return`, `SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn` ### controlFlow.switch.fallThroughComment @@ -2919,6 +3139,7 @@ The text of the comment a non-empty `case` falling through to the next one carri - Takes: a text, or a list of them, every one passing and the first written; `keep` - The standards: perCs `no break`, psr12 `no break`, nette `break omitted`, symfony `no break`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\FallThroughCommentRule`, stage Structure +- Covers: `no_break_comment`, `PSR2.ControlStructures.SwitchDeclaration` - Modifies comments ### controlFlow.trailingIf @@ -2929,6 +3150,7 @@ An `if` ending a function or a loop body becomes a guard that leaves early, and - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\EarlyExitForTrailingIfRule`, stage Structure +- Covers: `SlevomatCodingStandard.ControlStructures.EarlyExit` ### controlFlow.trailingIfMinStatements @@ -2938,6 +3160,7 @@ The statements the body of a trailing `if` has at least for it to become a guard - Takes: a count from 1 - The standards: perCs `2`, psr12 `2`, nette `2`, symfony `2`, laravel `2` - Rule `DressCode\Rules\ControlFlow\EarlyExitForTrailingIfRule`, stage Structure +- Covers: `SlevomatCodingStandard.ControlStructures.EarlyExit` ## expressions @@ -2949,6 +3172,7 @@ The statements the body of a trailing `if` has at least for it to become a guard - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Expressions\IncrementRule`, stage Structure +- Covers: `standardize_increment` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### expressions.assignment.increment @@ -2959,6 +3183,7 @@ An increment or a decrement whose value is not read, a statement or a step of `f - Takes: `prefix` (`++$i`, `--$i`); `postfix` (`$i++`, `$i--`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `postfix`, symfony `prefix`, laravel `postfix` - Rule `DressCode\Rules\Expressions\IncrementRule`, stage Structure +- Covers: `increment_style` ### expressions.assignment.targetInValue @@ -2970,6 +3195,7 @@ Not about the `$a += 1` it may write, which `expressions.assignment.addingOne` w - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\CombinedAssignmentForRepeatedTargetRule`, stage Structure +- Covers: `assign_null_coalescing_to_coalesce_equal`, `long_to_shorthand_operator`, `SlevomatCodingStandard.ControlStructures.RequireNullCoalesceEqualOperator`, `SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### expressions.cast @@ -2980,6 +3206,7 @@ The type name of a cast. - Takes: `canonical` (`(int)`, `(bool)`, `(float)`, `(string)` in lowercase, never `(integer)`, `(Boolean)`, `(double)`, `(binary)`); `keep` - The standards: perCs `canonical`, psr12 `canonical`, nette `canonical`, symfony `canonical`, laravel `canonical` - Rule `DressCode\Rules\Expressions\CastCanonicalTypeRule`, stage Structure +- Covers: `lowercase_cast`, `short_scalar_cast`, `Generic.PHP.LowerCaseType`, `PSR12.Keywords.ShortFormTypeKeywords`, `SlevomatCodingStandard.PHP.TypeCast`, `Squiz.WhiteSpace.CastSpacing` ### expressions.coalesce @@ -2989,6 +3216,7 @@ The type name of a cast. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\NullCoalescingForNullTernaryRule`, stage Structure +- Covers: `ternary_to_null_coalescing`, `SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### expressions.comparison.loose @@ -2999,6 +3227,7 @@ A loose comparison, `==`, `!=` or `<>`, is written `===` or `!==`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\NoLooseComparisonsRule`, stage Structure +- Covers: `strict_comparison`, `SlevomatCodingStandard.Operators.DisallowEqualOperators` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### expressions.comparison.notEquals @@ -3011,6 +3240,7 @@ Not about whether the comparison is loose, which `expressions.comparison.loose` - Takes: `'!='` (`!=`, never `<>`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `'!='`, symfony `'!='`, laravel `'!='` - Rule `DressCode\Rules\Expressions\NotEqualsNotationRule`, stage Structure +- Covers: `standardize_not_equals` ### expressions.comparison.yoda @@ -3020,6 +3250,7 @@ A Yoda comparison, the constant on its left side, a comparison of two variables - Takes: `forbidden` (never there, the variable on the left and the constant on the right, `$a === 1`); `required` (always there, the constant on the left, `1 === $a`, so that an accidental assignment does not compile); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `required`, laravel `forbidden` - Rule `DressCode\Rules\Expressions\YodaRule`, stage Structure +- Covers: `yoda_style`, `SlevomatCodingStandard.ControlStructures.DisallowYodaComparison`, `SlevomatCodingStandard.ControlStructures.RequireYodaComparison` ### expressions.doubleNegation @@ -3029,6 +3260,7 @@ A Yoda comparison, the constant on its left side, a comparison of two variables - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Expressions\NoDoubleNegationsRule`, stage Structure +- Covers: `no_short_bool_cast` ### expressions.explicitPrecedence @@ -3038,6 +3270,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\ExplicitPrecedenceRequiredRule`, stage Structure +- Covers: `Generic.CodeAnalysis.RequireExplicitBooleanOperatorPrecedence` ### expressions.parenthesesAfterConstruct @@ -3047,6 +3280,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Expressions\UselessParenthesesAfterConstructRule`, stage Structure +- Covers: `include`, `no_unneeded_control_parentheses`, `SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses`, `Squiz.Strings.EchoedStrings` ### expressions.ternary.ofTrueFalse @@ -3056,6 +3290,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\UselessTernaryOperatorRule`, stage Structure +- Covers: `SlevomatCodingStandard.ControlStructures.UselessTernaryOperator` ### expressions.ternary.short @@ -3065,6 +3300,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - Takes: `required` (always there); `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\ShortTernaryRule`, stage Structure +- Covers: `ternary_to_elvis_operator`, `SlevomatCodingStandard.ControlStructures.DisallowShortTernaryOperator`, `SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator` ### expressions.wordLogicalOperator @@ -3074,6 +3310,7 @@ An operand where logical or bitwise operators are easy to misread is parenthesiz - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\LogicalOperatorNotationRule`, stage Structure +- Covers: `logical_operators`, `Squiz.Operators.ValidLogicalOperators` ## literals @@ -3085,6 +3322,7 @@ The syntax of an array. - Takes: `short` (`[1, 2]`, never `array(1, 2)`); `long` (`array(1, 2)`, never `[1, 2]`); `keep` - The standards: perCs `short`, psr12 `keep`, nette `short`, symfony `short`, laravel `short` - Rule `DressCode\Rules\Arrays\ArraySyntaxRule`, stage Structure +- Covers: `array_syntax`, `Generic.Arrays.DisallowLongArraySyntax`, `Generic.Arrays.DisallowShortArraySyntax` ### literals.arraySyntax.list @@ -3094,6 +3332,7 @@ The syntax of a destructuring. - Takes: `short` (`[$a, $b] =`, never `list($a, $b) =`); `long` (`list($a, $b) =`, never `[$a, $b] =`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `short`, symfony `keep`, laravel `short` - Rule `DressCode\Rules\Arrays\ArraySyntaxRule`, stage Structure +- Covers: `list_syntax`, `SlevomatCodingStandard.PHP.ShortList` ### literals.classNameInString @@ -3103,6 +3342,7 @@ The syntax of a destructuring. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ClassKeywordForStringRule`, stage Structure +- Covers: `class_keyword` - Needs the types of the code, so it takes effect only with `typeAnalysis: phpstan` ### literals.concatenatedLiterals @@ -3113,6 +3353,7 @@ Two string literals concatenated on one line, which are one literal, `'ab'` for - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/no_superfluous_concatenation`, `no_useless_concat_operator`, `Generic.Strings.UnnecessaryStringConcat` ### literals.concatenatedLiteralsOverLines @@ -3122,6 +3363,7 @@ Whether two string literals concatenated over a line break are one literal too. - Takes: `yes`; `no` - The standards: perCs `no`, psr12 `no`, nette `no`, symfony `no`, laravel `no` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure +- Covers: `Generic.Strings.UnnecessaryStringConcat` ### literals.digitSeparator.fraction @@ -3131,6 +3373,7 @@ The separator grouping by three the digits of the fraction of a decimal number, - Takes: `required` (always there); `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/numeric_literal_separator`, `numeric_literal_separator`, `SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator`, `SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator` ### literals.digitSeparator.integer @@ -3140,6 +3383,7 @@ The separator grouping by three the digits of a decimal integer, or of the integ - Takes: `required` (always there); `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/numeric_literal_separator`, `numeric_literal_separator`, `SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator`, `SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator` ### literals.digitSeparatorMinDigits.fraction @@ -3149,6 +3393,7 @@ The number of digits from which `literals.digitSeparator.fraction` groups them. - Takes: a count from 1 - The standards: perCs `4`, psr12 `4`, nette `20`, symfony `4`, laravel `4` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure +- Covers: `SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator` ### literals.digitSeparatorMinDigits.integer @@ -3158,6 +3403,7 @@ The number of digits from which `literals.digitSeparator.integer` groups them. - Takes: a count from 1 - The standards: perCs `4`, psr12 `4`, nette `7`, symfony `4`, laravel `4` - Rule `DressCode\Rules\Literals\NumberNotationRule`, stage Structure +- Covers: `SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator` ### literals.emptyStringConcatenation @@ -3167,6 +3413,7 @@ The concatenation with an empty string, which is a cast or nothing, `(string) $x - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\UselessStringConcatenationRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/no_superfluous_concatenation`, `Generic.Strings.UnnecessaryStringConcat` ### literals.heredocWithoutInterpolation @@ -3176,6 +3423,7 @@ A heredoc with nothing to interpolate, which is written as a nowdoc. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\Literals\NowdocForHeredocRule`, stage Structure +- Covers: `heredoc_to_nowdoc`, `SlevomatCodingStandard.PHP.RequireNowdoc` ### literals.implicitBackslash @@ -3185,6 +3433,7 @@ A backslash of a string that escapes nothing, written `\\` instead. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NoImplicitBackslashesRule`, stage Structure +- Covers: `escape_implicit_backslashes`, `string_implicit_backslashes` ### literals.quotes @@ -3194,6 +3443,7 @@ The quotes of a plain string, one without interpolation, without an escape seque - Takes: `single` (`'text'` where double quotes gain nothing); `double` (`"text"` for every plain string, the backslashes of a single-quoted one escaped); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `single`, symfony `single`, laravel `single` - Rule `DressCode\Rules\Literals\StringQuotesRule`, stage Structure +- Covers: `single_quote`, `Squiz.Strings.DoubleQuoteUsage` ## comments @@ -3205,6 +3455,7 @@ An empty comment, which is removed. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Comments\NoEmptyCommentsRule`, stage Finishing +- Covers: `no_empty_comment`, `SlevomatCodingStandard.Commenting.EmptyComment` - Modifies comments ### comments.singleline @@ -3215,6 +3466,7 @@ The marker of a single-line comment. - Takes: `'//'` (`//`, never `#`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `'//'`, symfony `'//'`, laravel `'//'` - Rule `DressCode\Rules\Comments\NoHashCommentsRule`, stage Finishing +- Covers: `single_line_comment_style`, `PEAR.Commenting.InlineComment` - Modifies comments ## phpdoc @@ -3227,6 +3479,7 @@ The doc comment stands above the attributes. - Takes: `required` (always there); `keep` - The standards: perCs `required`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocAboveAttributesRule`, stage Structure +- Covers: `SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment` - Modifies comments ### phpdoc.annotation.casing @@ -3237,6 +3490,7 @@ The letter case of a known annotation. - Takes: `canonical` (as it is known, `@inheritDoc`, `@phpstan-var`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\AnnotationCasingRule`, stage Structure +- Covers: `phpdoc_tag_casing`, `SlevomatCodingStandard.Commenting.AnnotationName` - Modifies comments ### phpdoc.annotation.constantVar @@ -3247,6 +3501,7 @@ A `@var` on a class constant that says nothing. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\UselessConstantVarAnnotationRule`, stage Finishing +- Covers: `SlevomatCodingStandard.TypeHints.UselessConstantTypeHint` - Modifies comments ### phpdoc.annotation.duplicateParam @@ -3266,6 +3521,7 @@ A second `@return` of a function. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure +- Covers: `Squiz.Commenting.FunctionComment.DuplicateReturn` ### phpdoc.annotation.duplicateVar @@ -3275,6 +3531,7 @@ A second `@var` of a property. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure +- Covers: `Squiz.Commenting.VariableComment` ### phpdoc.annotation.empty @@ -3284,6 +3541,7 @@ A `@param`, `@return`, `@var` or `@see` without content. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure +- Covers: `Squiz.Commenting.VariableComment` ### phpdoc.annotation.extraParam @@ -3293,6 +3551,7 @@ A `@param` naming a parameter the function does not have. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoInvalidAnnotationsRule`, stage Structure +- Covers: `Squiz.Commenting.FunctionComment.ExtraParamComment` ### phpdoc.annotation.inlineVar @@ -3302,6 +3561,7 @@ An inline `@var` says what `assert($x instanceof Foo)` checks. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\AssertForInlineVarRule`, stage Structure +- Covers: `SlevomatCodingStandard.PHP.RequireExplicitAssertion` - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3313,6 +3573,7 @@ An array, iterable or traversable declaration has an annotation saying what its - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Types\NativeTypeRequiredRule`, stage Structure +- Covers: `SlevomatCodingStandard.TypeHints.ParameterTypeHint`, `SlevomatCodingStandard.TypeHints.PropertyTypeHint`, `SlevomatCodingStandard.TypeHints.ReturnTypeHint` - Modifies comments ### phpdoc.annotation.nativeTypeOnly @@ -3323,6 +3584,7 @@ A `@param`, `@return` or `@var` without a description that says only the native - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `forbidden` - Rule `DressCode\Rules\PhpDoc\UselessTypeAnnotationsRule`, stage Structure +- Covers: `no_superfluous_phpdoc_tags`, `SlevomatCodingStandard.Commenting.UselessFunctionDocComment`, `SlevomatCodingStandard.TypeHints.ParameterTypeHint`, `SlevomatCodingStandard.TypeHints.PropertyTypeHint`, `SlevomatCodingStandard.TypeHints.ReturnTypeHint` - Modifies comments ### phpdoc.annotation.plainComment @@ -3333,6 +3595,7 @@ An annotation in a plain comment, `/* @var */` or `/**@var` without the blank, w - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoPlainPhpdocsRule`, stage Finishing +- Covers: `SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.InvalidCommentType` - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3344,6 +3607,7 @@ The blank lines inside a doc comment. - Takes: `trimmed` (none at the start or the end, at most one in a row inside); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `trimmed`, symfony `trimmed`, laravel `trimmed` - Rule `DressCode\Rules\PhpDoc\PhpdocBlankLinesRule`, stage Finishing +- Covers: `phpdoc_trim`, `phpdoc_trim_consecutive_blank_line_separation` - Modifies comments ### phpdoc.empty @@ -3354,6 +3618,7 @@ An empty doc comment, which is removed. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Comments\NoEmptyCommentsRule`, stage Finishing +- Covers: `no_empty_phpdoc` - Modifies comments ### phpdoc.forbiddenAnnotations @@ -3364,6 +3629,7 @@ The annotations removed from doc comments, written with the `@`, such as `@autho - Takes: a list of annotations, each with its `@`; `keep` - The standards: perCs `keep`, psr12 `keep`, nette `[@access, @author, @copyright, @created, @license, @package, @since, @subpackage, @todo, @version]`, symfony `keep`, laravel `[@access, @package, @subpackage]` - Rule `DressCode\Rules\PhpDoc\ForbiddenAnnotationsRule`, stage Structure +- Covers: `general_phpdoc_annotation_remove`, `phpdoc_no_access`, `phpdoc_no_package`, `SlevomatCodingStandard.Commenting.ForbiddenAnnotations` - Modifies comments ### phpdoc.forbiddenLines @@ -3374,6 +3640,7 @@ The patterns of the lines of a description removed from doc comments, such as `~ - Takes: a list of regular expressions; `keep` - The standards: perCs `keep`, psr12 `keep`, nette `['~^(?:(?!private|protected|static)\S+ )?(?:con|de)structor\.\z~i', '~^\S+ [gs]etter\.\z~i']`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\ForbiddenPhpdocLinesRule`, stage Structure +- Covers: `SlevomatCodingStandard.Commenting.ForbiddenComments` - Modifies comments ### phpdoc.inheritdocOnly @@ -3384,6 +3651,7 @@ A doc comment of `@inheritDoc` alone. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\UselessInheritdocRule`, stage Structure +- Covers: `phpdoc_no_useless_inheritdoc`, `SlevomatCodingStandard.Commenting.UselessInheritDocComment` - Modifies comments ### phpdoc.property.lineSpan @@ -3394,6 +3662,7 @@ A property doc comment with a single annotation and no description. - Takes: `collapsed` (on one line, `/** @var int */`); `expanded` (over three lines, the annotation on the one in the middle); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `collapsed`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PropertyPhpdocLineSpanRule`, stage Formatting +- Covers: `phpdoc_line_span`, `SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment`, `SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment` - Modifies comments ### phpdoc.property.plainComment @@ -3404,6 +3673,7 @@ A plain comment describing a property, `//` or `/* */` where `/** */` belongs, r - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\NoPlainPhpdocsRule`, stage Finishing +- Covers: `Squiz.Commenting.VariableComment` - Modifies comments ### phpdoc.property.promoted @@ -3424,6 +3694,7 @@ The `*` opening each line of a doc comment. - Takes: `aligned` (each `*` below the first `*` of the opening `/**`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `aligned`, symfony `keep`, laravel `aligned` - Rule `DressCode\Rules\PhpDoc\PhpdocAlignmentRule`, stage Formatting +- Covers: `align_multiline_comment`, `phpdoc_indent`, `Squiz.Commenting.DocCommentAlignment` - Modifies comments ### phpdoc.types.array @@ -3434,6 +3705,7 @@ How an array type of a doc comment is written; `list` is a different type and - Takes: `'array'` (the generic notation); `'T[]'` (the brackets); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing +- Covers: `PhpCsFixerCustomFixers/phpdoc_array_style`, `PhpCsFixerCustomFixers/phpdoc_type_list`, `phpdoc_array_type`, `phpdoc_list_type`, `SlevomatCodingStandard.TypeHints.DisallowArrayTypeHintSyntax` - Modifies comments ### phpdoc.types.builtin @@ -3444,6 +3716,7 @@ How a built-in type in a doc comment is written, `int` and never `integer` or `I - Takes: `canonical` (the short lowercase name of a built-in type, `never` for `no-return`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `canonical`, symfony `canonical`, laravel `canonical` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing +- Covers: `PhpCsFixerCustomFixers/phpdoc_type_list`, `phpdoc_list_type`, `phpdoc_scalar`, `phpdoc_types`, `SlevomatCodingStandard.TypeHints.LongTypeHints` - Modifies comments ### phpdoc.types.duplicate @@ -3454,6 +3727,7 @@ A type a union of a doc comment names twice, `int|integer`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing +- Covers: `phpdoc_no_duplicate_types`, `phpdoc_types_no_duplicates` - Modifies comments ### phpdoc.types.nullable @@ -3474,6 +3748,7 @@ Where `null` stands in a union type of a doc comment. - Takes: `last` (`int|string|null`); `first` (`null|int|string`); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `last`, symfony `last`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing +- Covers: `phpdoc_types_order`, `SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition` - Modifies comments ### phpdoc.types.unionOrder @@ -3484,6 +3759,7 @@ The order of the types of a union type of a doc comment. - Takes: `alphabetical` (the types beside `null` sorted by name, case-insensitively); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\PhpDoc\PhpdocTypeNotationRule`, stage Finishing +- Covers: `phpdoc_types_order` - Modifies comments ## mistakes @@ -3505,6 +3781,7 @@ A `continue` that only leaves a `switch`, where `break` is meant, which is only - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\NoContinueInSwitchRule`, stage Structure +- Covers: `switch_continue_to_break`, `SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch` ### mistakes.debugOutput @@ -3514,6 +3791,7 @@ A statement calling a debugging function to print, changed into a comment, `// v - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoDebugOutputRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/commented_out_function` - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -3525,6 +3803,7 @@ The debugging functions whose calls print. - Takes: a list of names - The standards: perCs `[print_r, var_dump, var_export]`, psr12 `[print_r, var_dump, var_export]`, nette `[print_r, var_dump, var_export]`, symfony `[print_r, var_dump, var_export]`, laravel `[print_r, var_dump, var_export]` - Rule `DressCode\Rules\Functions\NoDebugOutputRule`, stage Structure +- Covers: `PhpCsFixerCustomFixers/commented_out_function` - Modifies comments ### mistakes.errorSuppression @@ -3544,6 +3823,7 @@ The `@` operator, which silences errors and is only reported. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\ThrowableForExceptionRule`, stage Structure +- Covers: `SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly` ### mistakes.finalParent @@ -3563,6 +3843,7 @@ The `global` statement, which is only reported. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoGlobalStatementsRule`, stage Structure +- Covers: `Squiz.PHP.GlobalKeyword` ### mistakes.invisibleCharacter @@ -3572,6 +3853,7 @@ Non-breaking, zero-width and bidi characters, replaced in comments and escaped i - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Files\NoInvisibleCharactersRule`, stage Structure +- Covers: `non_printable_character` - Modifies comments ### mistakes.neverReturnType.anonymousFunction @@ -3623,6 +3905,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Variables\NoRepeatedAssignmentsRule`, stage Structure +- Covers: `SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable` ### mistakes.staticSetState @@ -3641,6 +3924,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\StrictComparisonArgumentRequiredRule`, stage Structure +- Covers: `strict_param`, `SlevomatCodingStandard.Functions.StrictCall` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### mistakes.thisOutsideObject @@ -3651,6 +3935,7 @@ A variable assigned twice in one expression, `$a = $a = 1`. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoThisOutsideObjectRule`, stage Structure +- Covers: `Squiz.Scope.StaticThisUsage` ### mistakes.trailingWhitespaceInString @@ -3660,6 +3945,7 @@ At the end of the lines of a multi-line string or inline HTML, which changes the - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\NoTrailingWhitespaceInStringRule`, stage Structure +- Covers: `no_trailing_whitespace_in_string` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### mistakes.unimplementedAbstractMethod @@ -3680,6 +3966,7 @@ A `catch` after one catching `Throwable` or a class it catches, which is only re - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\ControlFlow\NoUnreachableCatchesRule`, stage Structure +- Covers: `SlevomatCodingStandard.Exceptions.DeadCatch` ### mistakes.__debugInfoReturningNull @@ -3753,6 +4040,7 @@ A function that may not be called, with what to do instead. - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure - Read by rule `DressCode\Rules\Upgrading\NoDeprecatedFunctionsRule`, which this decision does not turn on +- Covers: `Generic.PHP.ForbiddenFunctions`, `Generic.PHP.SAPIUsage`, `Squiz.PHP.DiscouragedFunctions` ### upgrading.api.forbiddenMembers @@ -3976,6 +4264,7 @@ A constant, method or property whose declaration is `@deprecated`, replaced by t - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Functions\NoManualSubstringTestsRule`, stage Structure +- Covers: `modernize_strpos` - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4005,6 +4294,7 @@ The backtick operator PHP 8.5 deprecated, written as a `shell_exec()` call. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Expressions\NoBacktickOperatorsRule`, stage Structure +- Covers: `backtick_to_shell_exec` ### upgrading.php.caseSemicolon @@ -4014,6 +4304,7 @@ The backtick operator PHP 8.5 deprecated, written as a `shell_exec()` call. - Takes: `forbidden` (never there); `keep` - The standards: perCs `forbidden`, psr12 `forbidden`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\ControlFlow\SwitchCaseNotationRule`, stage Structure +- Covers: `switch_case_semicolon_to_colon`, `PSR2.ControlStructures.SwitchDeclaration` ### upgrading.php.constructorReturnValue @@ -4046,6 +4337,7 @@ A function, a constant or a member PHP deprecated, written as what replaces it a - Rule `DressCode\Rules\Upgrading\ReplacedFunctionCallsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ForbiddenSymbolsRule`, stage Structure - Rule `DressCode\Rules\Upgrading\ForbiddenMembersRule`, stage Structure +- Covers: `Generic.PHP.DeprecatedFunctions` - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` ### upgrading.php.deprecatedArgument @@ -4065,6 +4357,7 @@ An argument whose value PHP deprecated is reported, `E_USER_ERROR` given to `tri - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `keep` - Rule `DressCode\Rules\Literals\NoDollarBraceInterpolationsRule`, stage Structure +- Covers: `simple_to_complex_string_variable` ### upgrading.php.implicitNullable @@ -4074,6 +4367,7 @@ A parameter type made nullable by its default `null` alone, `int $x = null`, dep - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `forbidden`, symfony `forbidden`, laravel `forbidden` - Rule `DressCode\Rules\Types\NullableTypeForDefaultNullRule`, stage Structure +- Covers: `nullable_type_declaration_for_default_null_value`, `SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue` ### upgrading.php.nullArrayKey @@ -4111,6 +4405,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - Takes: `forbidden` (never there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\NoSleepAndWakeupRule`, stage Structure +- Covers: `modern_serialization_methods` ### upgrading.phpdoc.deprecated @@ -4132,6 +4427,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\ReadonlyForAnnotationRule`, stage Structure +- Covers: `phpdoc_readonly_class_comment_to_keyword` - Needs PHP >=8.1 - Modifies comments - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` @@ -4200,6 +4496,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Expressions\NullsafeForGuardedAccessRule`, stage Structure +- Covers: `SlevomatCodingStandard.ControlStructures.RequireNullSafeObjectOperator` - Needs PHP >=8.0 ### upgrading.syntax.octalPrefix @@ -4210,6 +4507,7 @@ A `return` inside `finally`, which PHP 8.6 deprecated. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `required`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Literals\OctalNotationRule`, stage Structure +- Covers: `octal_notation` - Needs PHP >=8.1 ### upgrading.syntax.pipe @@ -4240,6 +4538,7 @@ A constructor parameter only assigned to a property. - Takes: `required` (always there); `keep` - The standards: perCs `keep`, psr12 `keep`, nette `keep`, symfony `keep`, laravel `keep` - Rule `DressCode\Rules\Classes\PromotedPropertyForAssignmentRule`, stage Structure +- Covers: `SlevomatCodingStandard.Classes.RequireConstructorPropertyPromotion` - Needs PHP >=8.0 - May have risky fixes, made once the decision is named in `fixRisky` or with `--fix-risky` diff --git a/docs/reference/generate.php b/docs/reference/generate.php index 797cbd041..04c8a82ba 100644 --- a/docs/reference/generate.php +++ b/docs/reference/generate.php @@ -162,6 +162,10 @@ function formatValue(DressCode\Value $value): string $facts[] = 'Read by rule `' . $rule . '`, which this decision does not turn on'; } + if ($covers = $registry->translator->findForeignNames([$path])) { + $facts[] = 'Covers: ' . implode(', ', array_map(fn($a) => "`$a`", $covers)); + } + if (isset($phpNeeds[$path])) { $facts[] = "Needs PHP $phpNeeds[$path]"; } diff --git a/readme.md b/readme.md index 5f2f07950..08d312a6e 100644 --- a/readme.md +++ b/readme.md @@ -468,6 +468,43 @@ missing.   +Migrating from PHP CS Fixer, PHP_CodeSniffer and Laravel Pint +============================================================= + +DressCode knows not only its own decisions, but also the names their rules have in PHP CS Fixer, +PHP_CodeSniffer and Slevomat. So the move takes two steps, and you can stop after the first. + +First, leave the code as it is. Comments `// phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, +`phpcs:ignoreFile` and the annotation `@phpcsSuppress` keep working: DressCode translates the foreign rule +name to its decisions, and the suppression holds. + +Second, translate the configuration. `import` reads it and writes its equivalent, and it tells you what it +could not carry over: + +```shell +dresscode import phpcs.xml dresscode.neon +``` + +``` +Read 4 rules; set 4 decisions and 1 preset. + `SlevomatCodingStandard.Namespaces.UnusedUses` without `searchAnnotations` removes an import only a doc comment uses; DressCode keeps it. + No DressCode rule covers `Squiz.Commenting.FunctionComment`. +``` + +`.php-cs-fixer.dist.php` is translated the same way, as long as PHP CS Fixer is still installed in the +project, because that file is PHP which has to run. + +**Laravel Pint** runs PHP CS Fixer, with a command line of its own and presets of its own, `laravel` the +default one. A project on that preset has nothing to translate: DressCode has the style as the standard +`laravel`, and `dresscode init` in a project with Pint writes the standard of its preset itself. Over 1,615 +files of the first-party packages of Laravel run through Pint, `dresscode check --use laravel` finds 129 +places, each a deliberate difference the manual lists (October 2026). Rules a `pint.json` adds to the preset +go into the configuration by hand. On top of it, the same run upgrades the code to a newer PHP and, with +`dresscode/upgrade-laravel`, to a newer Laravel, and it imports the classes named in every tag of a doc +comment, `@template` and `@method` included. + +  + Made for AI coding agents: Claude Code and Codex ================================================ @@ -610,7 +647,10 @@ DressCode is made to run after every edit, so what counts most is the time one f to a half of what PHP CS Fixer needs, 118 ms for a small file against 323 ms. Over a whole project, doing the same work, it is faster too. Run with only the rules that match the PER Coding Style set of PHP CS Fixer, DressCode takes 64 to 90 percent of its time in one process, and 46 to 64 percent in parallel -processes, over Laravel, WordPress, Symfony and Nette. +processes, over Laravel, WordPress, Symfony and Nette. Symfony with its own configuration of PHP CS Fixer, +translated by `dresscode import`, is checked in 56 percent of the time, 14.2 s against 25.3 s over its +7,704 files; PHP CS Fixer runs a hundred more rules there, on phpDoc and PHPUnit, that have no +counterpart in DressCode. The preset `perCs` does more than that set: it also breaks long signatures, conditions, chains and arrays over lines and fixes the casing of names, none of which the set does. Preset against preset, a run diff --git a/src/Config/ConfigResolver.php b/src/Config/ConfigResolver.php index 2ab6e01df..9f7487e97 100644 --- a/src/Config/ConfigResolver.php +++ b/src/Config/ConfigResolver.php @@ -111,7 +111,7 @@ public function resolve( self::addSymbols($symbols, $profile->namespaces, $layer->describe()); if ($profile->decisions !== []) { - new DecisionResolver($this->getCatalogue())->checkLayer($profile->decisions); + new DecisionResolver($this->getCatalogue(), translator: $this->registry->translator)->checkLayer($profile->decisions); $decisionLayers[] = [$layer, $profile->decisions]; } @@ -171,6 +171,7 @@ public function resolve( $this->project, typesAnalyzed: $typeAnalysis !== null && $this->typesAvailable, certainNames: $resolution === 'certain', + translator: $this->registry->translator, ); $decisions = $resolver->resolve($decisionLayers); if ($packageLayers = $this->collectPackageMaps($decisions, $narrowed)) { diff --git a/src/Config/DecisionResolver.php b/src/Config/DecisionResolver.php index 609271a10..1c26e1450 100644 --- a/src/Config/DecisionResolver.php +++ b/src/Config/DecisionResolver.php @@ -9,6 +9,7 @@ use DressCode\{Config, ConfigurationException, Decision, DecisionKind, Domain, Rule, RuleInfo, Value, Values}; use DressCode\Domains\{Count, Flag, Map, Words}; +use DressCode\Interop\Translator; use Nette\Utils\Helpers; use function count, is_array, is_bool, is_int, is_string, strlen; @@ -36,6 +37,8 @@ public function __construct( private bool $typesAnalyzed = false, /** whether the namespaces of the configuration are complete, which turns on their guard */ private bool $certainNames = false, + /** knows the names of the rules of other tools and the decisions that cover them */ + private Translator $translator = new Translator, ) { } @@ -297,7 +300,14 @@ private function refuseKey(string $path, string $prefix): never } } + $covered = $prefix === '' ? $this->translator->findPaths($path) : []; $hint = Helpers::getSuggestion(array_keys($known), $path); - throw new ConfigurationException("Key `$path` is unknown" . ($hint !== null ? "; write `$hint`." : '.')); + throw new ConfigurationException("Key `$path` is unknown" . match (true) { + $covered !== [] => '. It is the name of a rule of another tool, covered by `' + . implode('` and `', $covered) + . '`; `dresscode import` translates a configuration of another tool.', + $hint !== null => "; write `$hint`.", + default => '.', + }, docs: $covered !== [] ? 'migration#import' : null); } } diff --git a/src/Config/PluginRegistry.php b/src/Config/PluginRegistry.php index f1ee4f4c0..321bcadff 100644 --- a/src/Config/PluginRegistry.php +++ b/src/Config/PluginRegistry.php @@ -8,13 +8,14 @@ namespace DressCode\Config; use DressCode\{ConfigurationException, Decision, DecisionKind, Plugin, QualifiedNames, Rule}; +use DressCode\Interop\Translator; use Nette\Utils\Helpers; use function array_key_exists, strlen; /** * What a run knows of its plugins: the rules by their classes, the presets by name, the pages of their documentation, - * the sections and the catalogue. A name of a preset without a vendor is the + * the sections, the translator and the catalogue. A name of a preset without a vendor is the * one of the core of that name, so `'perCs'` is `'dresscode/perCs'`. * @internal */ @@ -61,8 +62,9 @@ final class PluginRegistry private ?Catalogue $catalogue = null; - public function __construct() - { + public function __construct( + public readonly Translator $translator = new Translator, + ) { $core = (new CorePlugin)->getManifest(); $this->rules = array_keys($core->rules); $this->coreDecisions = $core->decisions; @@ -129,7 +131,8 @@ public function registerDependencies(string $section, array $sections): void /** * What a name in a suppression comment stands for, as a report is told by: a decision or a section for itself, the - * class of a rule for its requirements; empty when nothing does. + * class of a rule for its requirements, and a name of another tool for the requirements standing for it; empty when + * nothing does. * @return list */ public function expandSuppressedName(string $name): array @@ -145,7 +148,10 @@ public function expandSuppressedName(string $name): array return array_values(array_map(fn(Decision $decision) => $decision->path, $requirements)); } - return []; + return array_values(array_filter( + $this->translator->findPaths($name), + fn(string $path) => ($catalogue->find($path)->kind ?? DecisionKind::Parameter) !== DecisionKind::Parameter, + )); } @@ -311,11 +317,22 @@ public function findPreset(string $preset): ?string /** - * The error of a name of `only`, `fixRisky` or `severity` nobody owns, with the nearest preset. + * The error of a name of `only`, `fixRisky` or `severity` nobody owns, with the decisions that cover it where it belongs + * to another tool, else the nearest preset. */ public function createUnknownNameException(string $name): ConfigurationException { - return new ConfigurationException("Unknown decision, preset or rule `$name`." . self::suggest($name, array_keys($this->presets))); + $message = "Unknown decision, preset or rule `$name`."; + $covered = $this->translator->findPaths($name); + return match (true) { + $covered !== [] => new ConfigurationException( + "$message It is covered by `" + . implode('` and `', $covered) + . '`; `dresscode import` translates a configuration of another tool.', + docs: 'migration#import', + ), + default => new ConfigurationException($message . self::suggest($name, array_keys($this->presets))), + }; } diff --git a/src/Console/Application.php b/src/Console/Application.php index a35d691fb..3e3f95b69 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -138,6 +138,7 @@ private function defineCommandLine(): Command new Commands\ExplainCommand($context), new Commands\CatalogueCommand($context), new Commands\InitCommand($context), + new Commands\ImportCommand($context), ] as $handlers) { $program->addCommandsFrom($handlers); } diff --git a/src/Console/Commands/CatalogueCommand.php b/src/Console/Commands/CatalogueCommand.php index c35567623..82da92da9 100644 --- a/src/Console/Commands/CatalogueCommand.php +++ b/src/Console/Commands/CatalogueCommand.php @@ -14,8 +14,8 @@ /** - * The command `catalogue`: every decision of the catalogue with its description, those the configuration makes marked; - * or the catalogue as data, the values of the standards included. + * The command `catalogue`: every decision of the catalogue with its description and the names of other tools standing + * for it, those the configuration makes marked; or the catalogue as data, the values of the standards included. * @internal */ final readonly class CatalogueCommand @@ -45,6 +45,7 @@ public function catalogue( { [$factory, $resolution] = $this->context->resolveProject($args, $configuration, $only); $catalogue = $resolution->getCatalogue(); + $translator = $factory->registry->translator; $out = $this->context->out; if ($format === 'json') { $data = $catalogue->toArray(); @@ -58,6 +59,11 @@ public function catalogue( } } + foreach ($data['decisions'] as $path => &$decision) { + $decision['covers'] = $translator->findForeignNames([$path]); + } + + unset($decision); $out->write(Json::encode($data, pretty: true) . "\n"); return 0; } @@ -72,10 +78,12 @@ public function catalogue( ); foreach ($decisions as $path => $decision) { $set = isset($made[$path]) && $made[$path]->layers !== [] && !$made[$path]->value->isKept(); + $covers = $translator->findForeignNames([$path]); $out->writeLine( ($set ? '*' : ' ') . ' ' . Ansi::pad($out->color($set ? 'white' : null, $path), 50) - . ' ' . Markup::highlightCode($out, $decision->description), + . ' ' . Markup::highlightCode($out, $decision->description) + . ($covers ? $out->color('gray', ' (covers ' . implode(', ', $covers) . ')') : ''), ); } diff --git a/src/Console/Commands/ImportCommand.php b/src/Console/Commands/ImportCommand.php new file mode 100644 index 000000000..c31ca106b --- /dev/null +++ b/src/Console/Commands/ImportCommand.php @@ -0,0 +1,99 @@ +context->resolvePath($file); + $target = $target === null ? null : $this->context->resolvePath($target); + $format = $target === null ? 'php' : strtolower(pathinfo($target, PATHINFO_EXTENSION)); + if (!in_array($format, ['neon', 'php'], true)) { + throw new UsageException( + "The configuration `$target` is written as NEON or PHP, so its extension is `.neon` or `.php`.", + $args->command, + ); + } elseif ($target !== null && file_exists($target)) { + throw new UsageException("The configuration `$target` exists already; remove it, or name another file.", $args->command); + } + + $translator = new Translator; + if (preg_match('~\.xml(\.dist)?$~Di', $file)) { + [$rules, $unread] = PhpCodeSniffer::readConfig($file); + $translation = $translator->translate($rules); + } else { + [$rules, $indent, $lineEnding] = PhpCsFixer::readConfig($file); + $unread = []; + $translation = $translator->translate($rules, $indent, $lineEnding); + } + + foreach ($unread as $warning) { + $translation->warn($warning); + } + + $source = $format === 'neon' ? $translation->toNeon() : $translation->toPhp(); + if ($target === null) { + $this->context->out->write($source); + } else { + FileSystem::write($target, $source); + $this->context->writeNote("Written `$target`.\n"); + } + + $this->context->writeNote(sprintf( + "\nRead %s; set %s and %s.\n", + Markup::formatCount(count($rules), 'rule'), + Markup::formatCount(count($translation->getPaths()), 'decision'), + Markup::formatCount(count($translation->presets), 'preset'), + )); + foreach ($translation->warnings as $warning) { + $this->context->writeNote(" $warning\n"); + } + + $untranslated = array_diff_key( + ['indentation.unit' => 'the indentation', 'file.lineEnding' => 'the line ending'], + array_flip($translation->getPaths()), + ); + if (!$translation->presets && $untranslated) { + $this->context->writeNote(sprintf( + " %s %s not translated; set %s with `%s`.\n", + ucfirst(implode(' and ', $untranslated)), + count($untranslated) > 1 ? 'are' : 'is', + count($untranslated) > 1 ? 'them' : 'it', + implode('` and `', array_keys($untranslated)), + )); + } + + return 0; + } +} diff --git a/src/Engine/Suppression.php b/src/Engine/Suppression.php index 65684aad3..955a62dc4 100644 --- a/src/Engine/Suppression.php +++ b/src/Engine/Suppression.php @@ -14,7 +14,8 @@ /** * Which rules are silenced on which original lines, read once from the comments of the file before any mutation: - * the dresscode:ignore, disable, enable and ignoreFile comments. What follows ` -- ` says why, for the reader alone. + * the dresscode:ignore, disable, enable and ignoreFile comments, and the forms of phpcs, whose names Interop + * translates, or which name ours. What follows ` -- ` says why, for the reader alone. * @internal */ final class Suppression @@ -36,19 +37,28 @@ final class Suppression public static function fromFile(FileNode $file, \Closure $expandName, ?string $code = null, array $comments = []): self { $suppression = new self; - if ($code !== null && $comments === [] && !str_contains($code, 'dresscode:')) { // nothing to read + if ($code !== null && $comments === [] && !str_contains($code, 'dresscode:') && !str_contains($code, 'phpcs')) { // nothing to read return $suppression; } $disabled = []; $lastLine = max($file->endOfFile->line, 1); foreach ($file->getTokens() as $token) { - foreach ([$token->leadingTrivia, $token->trailingTrivia] as $trivias) { + foreach ([$token->leadingTrivia, $token->trailingTrivia] as $side => $trivias) { foreach ($trivias as $index => $trivia) { if (!$trivia->isComment()) { continue; - } elseif (preg_match( - '~dresscode:(ignoreFile|ignore|disable|enable)(?:\s+([\w/.\\\\][\w/.\\\\,\s-]*?))?(?:\s+--(?:\s.*?)?)?(?=\s*(?:\*/|$))~m', + } elseif ($trivia->id === Trivia::DocComment && str_contains($trivia->text, '@phpcsSuppress')) { + $suppression->collectPhpcsSuppress( + $trivia, + $side === 0 ? $token->getPrevious() : $token, + $side === 0 ? $token : $token->getNext(), + $expandName, + ); + } + + if (preg_match( + '~(?:dresscode|phpcs):(ignoreFile|ignore|disable|enable)(?:\s+([\w/.\\\\][\w/.\\\\,\s-]*?))?(?:\s+--(?:\s.*?)?)?(?=\s*(?:\*/|$))~m', $trivia->text, $m, )) { @@ -169,6 +179,33 @@ public function getUnknownNames(): array } + /** + * The tags of a doc comment silence the node it documents: the one starting at the token after it, or one ending + * at the token before it, as a parameter does. + * @param \Closure(string): list $expandName + */ + private function collectPhpcsSuppress(Trivia $doc, ?Token $before, ?Token $after, \Closure $expandName): void + { + $node = $after ? self::findOutermostNode($after) : null; + if ($node?->getDocComment() !== $doc) { + for ( + $node = $before?->parent; + $node && $node->getLastToken() === $before && $node->getDocComment() !== $doc; + $node = $node->parent + ); + } + + $from = $node?->getFirstToken()?->line; + if ( + $from !== null + && $node->getDocComment() === $doc + && preg_match_all('~@phpcsSuppress[ \t]+([\w/][\w/.-]*(?:[ \t]*,[ \t]*[\w/][\w/.-]*)*)~', $doc->text, $m) + ) { + $this->add($this->expandNames(implode(',', $m[1]), $expandName), $from, self::getEndLine($node)); + } + } + + /** * @param array> $comments * @return list diff --git a/src/Interop/PhpCodeSniffer.php b/src/Interop/PhpCodeSniffer.php new file mode 100644 index 000000000..40fc15f2a --- /dev/null +++ b/src/Interop/PhpCodeSniffer.php @@ -0,0 +1,864 @@ +|\Closure(array, Translation): mixed> sniff => its decisions, or what to + * set for its properties; every property is read through `??` so that a sniff with none translates too + */ + public static function getTranslations(): array + { + // the blank lines before a property, a constant or an enum case, `blankLines` counting those after a member of + // another kind and before a documented one for every member together + $memberSpacing = fn(string $place) => fn(array $o, Translation $t) => $t + ->setBlankLines( + "blankLines.betweenMembers.$place", + $o['minLinesCountBeforeWithoutComment'] ?? 0, + $o['maxLinesCountBeforeWithoutComment'] ?? 1, + ) + ->setBlankLines( + 'blankLines.betweenMemberKinds', + $o['minLinesCountBeforeWithoutComment'] ?? 0, + $o['maxLinesCountBeforeWithoutComment'] ?? 1, + ) + ->setBlankLines( + 'blankLines.beforeDocumentedMember', + $o['minLinesCountBeforeWithComment'] ?? 1, + $o['maxLinesCountBeforeWithComment'] ?? 1, + ); + + // the nullable and the place of null in a type, which the sniffs check only where their properties name them + $typeFormat = fn(string $sniff) => function (array $o, Translation $t) use ($sniff) { + if (!isset($o['shortNullable']) && !isset($o['nullPosition'])) { + $t->warn("`$sniff` without `shortNullable` or `nullPosition` checks nothing DressCode decides."); + } + + $values = array_filter([ + 'nullable' => isset($o['shortNullable']) ? ($o['shortNullable'] === 'yes' ? 'questionMark' : 'union') : null, + 'nullPosition' => $o['nullPosition'] ?? null, + ], fn($v) => $v !== null); + // the doc comments are checked alike where the sniff is told so + foreach (($o['enableForDocComments'] ?? false) ? ['types', 'phpdoc.types'] : ['types'] as $section) { + foreach ($values as $key => $value) { + $t->set("$section.$key", $value); + } + } + }; + + return [ + 'Generic.Arrays.DisallowLongArraySyntax' => ['literals.arraySyntax.array' => 'short'], + 'Generic.Arrays.DisallowShortArraySyntax' => ['literals.arraySyntax.array' => 'long'], + 'Generic.CodeAnalysis.RequireExplicitBooleanOperatorPrecedence' => ['expressions.explicitPrecedence' => 'required'], + 'Generic.CodeAnalysis.UnnecessaryFinalModifier' => ['classes.modifiers.implied' => 'forbidden'], + 'Generic.ControlStructures.InlineControlStructure' => ['braces.bracelessBody' => 'forbidden'], + 'Generic.Files.ByteOrderMark' => ['file.bom' => 'forbidden'], + 'Generic.Files.LineEndings' => ['file.lineEnding' => 'majority'], + 'Generic.Files.LineLength' => fn(array $o, Translation $t) => $t->setLineLength( + ($o['absoluteLineLimit'] ?? 100) ?: ($o['lineLimit'] ?? 80), + ) + ->setAll(['file.longLines' => 'forbidden', 'file.lineLengthExcept' => ['imports']]), + 'Generic.Formatting.DisallowMultipleStatements' => ['file.statementsPerLine' => 1], + 'Generic.Functions.FunctionCallArgumentSpacing' => ['spacing.comma' => 'spaced', 'spacing.alignment.comma' => 'tabs'], + 'Generic.Functions.FunctionCallArgumentSpacing.SpaceBeforeOpenBracket' => ['spacing.call' => 'compact'], + 'Generic.NamingConventions.CamelCapsFunctionName' => fn(array $o, Translation $t) => $t->setAll([ + 'naming.method' => 'camelCase', + 'naming.function' => 'camelCase', + ]), + 'Generic.NamingConventions.UpperCaseConstantName' => function (array $o, Translation $t) { + $t->warn('`Generic.NamingConventions.UpperCaseConstantName` also checks `define()`, which DressCode does not.'); + $t->set('naming.constant', 'UPPER_CASE'); + }, + 'Generic.PHP.DeprecatedFunctions' => ['upgrading.php.deprecatedApi' => 'forbidden'], + 'Generic.PHP.DisallowShortOpenTag' => ['file.openingTag' => 'required'], + 'Generic.PHP.ForbiddenFunctions' => fn(array $o, Translation $t) => $t->set( + 'upgrading.api.forbiddenFunctions', + // the sniff reads the value `null` of a ruleset as no replacement + array_map( + fn($v) => $v === null || $v === 'null' ? null : "use `$v()`", + $o['forbiddenFunctions'] ?? ['sizeof' => 'count', 'delete' => 'unset'], + ), + ), + 'Generic.PHP.LowerCaseConstant' => ['casing.trueFalseNull' => 'lowercase'], + 'Generic.PHP.LowerCaseKeyword' => ['casing.keyword' => 'lowercase'], + // `array`, `self` and the like are keywords of a declaration + 'Generic.PHP.LowerCaseType' => [ + 'expressions.cast' => 'canonical', + 'casing.type' => 'lowercase', + 'casing.keyword' => 'lowercase', + ], + 'Generic.PHP.RequireStrictTypes' => ['file.strictTypes.declaration' => 'required', 'file.strictTypes.position' => 'nextLine'], + 'Generic.PHP.SAPIUsage' => fn(array $o, Translation $t) => $t->set( + 'upgrading.api.forbiddenFunctions', + ['php_sapi_name' => 'use `PHP_SAPI`'], + ), + 'Generic.Strings.UnnecessaryStringConcat' => fn(array $o, Translation $t) => $t->setAll([ + 'literals.concatenatedLiterals' => 'forbidden', + 'literals.concatenatedLiteralsOverLines' => !($o['allowMultiline'] ?? false), + 'literals.emptyStringConcatenation' => 'forbidden', + ]), + 'Generic.WhiteSpace.DisallowSpaceIndent' => ['indentation.unit' => 'tab'], + // spaces of a width the sniff does not say, four unless a sniff naming the width decides it, in either order + 'Generic.WhiteSpace.DisallowTabIndent' => fn(array $o, Translation $t) => $t->prefer('indentation.unit', '4 spaces'), + 'Generic.WhiteSpace.IncrementDecrementSpacing' => ['spacing.unaryOperator' => 'compact'], + 'Generic.WhiteSpace.LanguageConstructSpacing' => ['spacing.keyword' => 'spaced'], + 'Generic.WhiteSpace.ScopeIndent' => function (array $o, Translation $t) { + $unit = ($o['tabIndent'] ?? false) ? 'tab' : match ($o['indent'] ?? 4) { + 4 => '4 spaces', + 2 => '2 spaces', + default => null, + }; + if ($unit === null) { + $t->warn("`Generic.WhiteSpace.ScopeIndent` with `indent={$o['indent']}` has no equivalent; DressCode indents by a tab, four spaces or two."); + } else { + $t->set('indentation.unit', $unit); + } + }, + 'PEAR.Commenting.InlineComment' => ['comments.singleline' => 'slashes'], + 'PEAR.Functions.ValidDefaultValue' => ['functions.unreachableParameterDefault' => 'forbidden'], + 'PEAR.WhiteSpace.ObjectOperatorIndent' => fn(array $o, Translation $t) => $t + ->set('indentation.chain', ($o['multilevel'] ?? false) ? 'nested' : 'flat'), + 'PSR1.Methods.CamelCapsMethodName' => fn(array $o, Translation $t) => $t->set('naming.method', 'camelCase'), + 'PSR12.Classes.AnonClassDeclaration' => [ + 'braces.position.anonymousClass' => 'sameLine', + 'spacing.keyword' => 'spaced', + 'spacing.anonymousClass' => 'spaced', + 'multiline.items.implements' => 'perLine', + 'multiline.edges.implements' => 'apart', + ...Translation::NoSpaceInParentheses, + ], + 'PSR12.Classes.ClassInstantiation' => ['classes.emptyParentheses.instantiation' => 'required'], + 'PSR12.Classes.ClosingBrace' => function (array $o, Translation $t) { + $t->warn('`PSR12.Classes.ClosingBrace` also forbids a comment after the closing brace of a class or a function, which DressCode does not.'); + $t->set('file.statementsPerLine', 1); + }, + 'PSR12.Classes.OpeningBraceSpace' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.bodyStart.class' => 0, + 'blankLines.bodyStart.anonymousClass' => 0, + ]), + 'PSR12.ControlStructures.BooleanOperatorPlacement' => function (array $o, Translation $t) { + $only = $o['allowOnly'] ?? null; + if ($only === 'first') { + $t->set('multiline.operatorPosition.condition', 'lineStart'); + } elseif ($only === 'last') { + $t->set('multiline.operatorPosition.condition', 'lineEnd'); + } else { + $t->warn('`PSR12.ControlStructures.BooleanOperatorPlacement` forbids a condition mixing boolean operators at the start and at the end of its lines, which DressCode does not check.'); + $t->keep('multiline.operatorPosition.condition'); + } + }, + 'PSR12.ControlStructures.ControlStructureSpacing' => function (array $o, Translation $t) { + $t->warn('`PSR12.ControlStructures.ControlStructureSpacing` also puts the parentheses of a multi-line `for`, `foreach`, `switch`, `catch` and `match` on lines of their own, which DressCode does not.'); + $t->setAll([ + ...Translation::NoSpaceInParentheses, + 'multiline.items.condition' => ['perLine', 'fill'], + 'multiline.edges.condition' => 'apart', + ]); + }, + 'PSR12.Files.DeclareStatement' => function (array $o, Translation $t) { + $t->warn('`PSR12.Files.DeclareStatement` also checks the case of the directive of a `declare`, which DressCode does not.'); + $t->setAll(['spacing.declare' => 'compact', 'braces.position.controlStructure' => 'sameLine']); + }, + 'PSR12.Files.FileHeader' => [ + 'blankLines.afterOpeningTag' => 1, + 'blankLines.afterDeclare' => 1, + 'blankLines.beforeNamespace' => 1, + 'blankLines.afterNamespace' => 1, + 'blankLines.afterImports' => 1, + 'blankLines.beforeDeclaration' => 1, + 'blankLines.betweenImports' => 0, + 'blankLines.betweenImportKinds' => 1, + ], + 'PSR12.Files.ImportStatement' => fn(array $o, Translation $t) => $t->set('imports.leadingBackslash', 'forbidden'), + 'PSR12.Files.OpenTag' => function (array $o, Translation $t) { + $t->warn('`PSR12.Files.OpenTag` leaves the line below the opening tag as it is, where DressCode puts a blank line.'); + $t->set('blankLines.afterOpeningTag', 1); + }, + 'PSR12.Functions.NullableTypeDeclaration' => ['spacing.typeDeclaration' => 'compact'], + 'PSR12.Functions.ReturnTypeDeclaration' => ['spacing.typeDeclaration' => 'compact'], + 'PSR12.Keywords.ShortFormTypeKeywords' => ['expressions.cast' => 'canonical'], + 'PSR12.Operators.OperatorSpacing' => fn(array $o, Translation $t) => $t->setAll([ + 'spacing.binaryOperator' => 'spaced', + 'spacing.alignment.binaryOperator' => 'spaces', + 'spacing.ternary' => 'spaced', + 'spacing.alignment.ternary' => 'optional', + 'spacing.concatenation' => 'spaced', + ]), + 'PSR12.Properties.ConstantVisibility' => [ + 'classes.visibility.constant' => 'required', + 'classes.visibility.property' => 'required', + 'classes.visibility.method' => 'required', + ], + 'PSR12.Traits.UseDeclaration' => function (array $o, Translation $t) { + $t->warn('`PSR12.Traits.UseDeclaration` also lays out the conflict block of a trait use, its opening brace on the line of `use` and a rule per line, which DressCode does not.'); + $t->setAll([ + 'blankLines.bodyStart.class' => 0, + 'blankLines.betweenTraitUses' => 0, + 'blankLines.afterTraitUses' => 1, + 'blankLines.bodyEnd.class' => 0, + // the space after `use`, around `as` and `insteadof`, and none before the semicolon + 'spacing.keyword' => 'spaced', + 'spacing.semicolon.before' => 'compact', + ]); + $t->set('classes.members.grouping.traitUse', 'separated'); + // a full order another rule sets puts the trait uses where it says + $t->prefer('classes.members.order', ['traitUse']); + }, + 'PSR2.Classes.ClassDeclaration' => [ + 'braces.position.class' => 'nextLine', + 'spacing.keyword' => 'spaced', + 'multiline.items.implements' => 'perLine', + 'multiline.edges.implements' => 'apart', + ], + 'PSR2.Classes.ClassDeclaration.SpaceBeforeKeyword' => ['spacing.keyword' => 'spaced'], + 'PSR2.Classes.PropertyDeclaration' => [ + 'classes.visibility.constant' => 'required', + 'classes.visibility.property' => 'required', + 'classes.visibility.method' => 'required', + 'classes.modifiers.order' => 'canonical', + ], + 'PSR2.ControlStructures.ControlStructureSpacing' => Translation::NoSpaceInParentheses, + 'PSR2.ControlStructures.ElseIfDeclaration' => ['controlFlow.elseif' => 'oneWord'], + 'PSR2.ControlStructures.SwitchDeclaration' => function (array $o, Translation $t) { + $t->warn('`PSR2.ControlStructures.SwitchDeclaration` also starts the body of a case on the line below it and forbids braces around it, which DressCode does not.'); + $t->setAll([ + 'upgrading.php.caseSemicolon' => 'forbidden', + 'spacing.switchCase' => 'compact', + 'controlFlow.switch.fallThroughComment' => 'no break', + 'casing.keyword' => 'lowercase', + 'spacing.keyword' => 'spaced', + 'file.statementsPerLine' => 1, + 'indentation.switchCase' => 1, + ]); + }, + 'PSR2.ControlStructures.SwitchDeclaration.SpaceBeforeColonCASE' => ['spacing.switchCase' => 'compact'], + 'PSR2.Files.ClosingTag' => ['file.closingTagAtEnd' => 'forbidden'], + 'PSR2.Files.EndFileNewline' => ['file.finalLineEndings' => 1], + 'PSR2.Methods.FunctionCallSignature' => function (array $o, Translation $t) { + if (($o['requiredSpacesAfterOpen'] ?? 0) > 0 || ($o['requiredSpacesBeforeClose'] ?? 0) > 0) { + $t->warn('`PSR2.Methods.FunctionCallSignature` with spaces inside the parentheses has no equivalent; DressCode writes none.'); + } + $t->setAll([ + 'spacing.call' => 'compact', + ...Translation::NoSpaceInParentheses, + 'multiline.items.argument' => ($o['allowMultipleArguments'] ?? false) ? 'fill' : 'perLine', + 'multiline.edges.argument' => 'apart', + ]); + }, + 'PSR2.Methods.FunctionClosingBrace' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.bodyEnd.function' => 0, + 'blankLines.bodyEnd.closure' => 0, + ]), + 'PSR2.Methods.MethodDeclaration' => function (array $o, Translation $t) { + $t->warn('`PSR2.Methods.MethodDeclaration` also rules on the underscore prefix of method names, which DressCode does not.'); + $t->set('classes.modifiers.order', 'canonical'); + }, + 'PSR2.Namespaces.NamespaceDeclaration' => ['blankLines.afterNamespace' => 1, 'blankLines.beforeDeclaration' => 1], + 'PSR2.Namespaces.UseDeclaration' => [ + 'blankLines.afterImports' => 1, + 'blankLines.beforeDeclaration' => 1, + 'imports.grouping.class' => 'separated', + 'imports.grouping.function' => 'separated', + 'imports.grouping.constant' => 'separated', + ], + 'SlevomatCodingStandard.Arrays.MultiLineArrayEndBracketPlacement' => ['multiline.edges.array' => ['apart', 'attached']], + 'SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace' => Translation::NoSpaceInArrays, + 'SlevomatCodingStandard.Arrays.TrailingArrayComma' => fn(array $o, Translation $t) => $t->set( + 'multiline.trailingComma.array', + 'required', + ), + 'SlevomatCodingStandard.Attributes.AttributeAndTargetSpacing' => fn(array $o, Translation $t) => $t->set( + 'blankLines.afterAttributes', + $o['linesCountBetweenAttributeAndTarget'] ?? 0, + ), + 'SlevomatCodingStandard.Attributes.DisallowMultipleAttributesPerLine' => [ + 'multiline.attributePosition.declaration' => 'above', + 'multiline.attributePosition.parameter' => 'above', + ], + 'SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment' => ['phpdoc.aboveAttributes' => 'required'], + 'SlevomatCodingStandard.Classes.BackedEnumTypeSpacing' => [ + 'spacing.typeDeclaration' => 'compact', + ], + 'SlevomatCodingStandard.Classes.ClassConstantVisibility' => [ + 'classes.visibility.constant' => 'required', + 'classes.visibility.property' => 'required', + 'classes.visibility.method' => 'required', + ], + // only between members of two kinds, a trait use, a constant, a property, a method + 'SlevomatCodingStandard.Classes.ClassMemberSpacing' => fn(array $o, Translation $t) => $t + ->setBlankLines('blankLines.betweenMemberKinds', $o['linesCountBetweenMembers'] ?? 1, $o['linesCountBetweenMembers'] ?? 1), + 'SlevomatCodingStandard.Classes.ConstantSpacing' => $memberSpacing('constant'), + 'SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition' => ['classes.members.grouping.constant' => 'separated'], + 'SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition' => ['classes.members.grouping.property' => 'separated'], + 'SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.bodyStart.class' => $o['linesCountAfterOpeningBrace'] ?? 1, + 'blankLines.bodyStart.anonymousClass' => $o['linesCountAfterOpeningBrace'] ?? 1, + 'blankLines.bodyEnd.class' => $o['linesCountBeforeClosingBrace'] ?? 1, + 'blankLines.bodyEnd.anonymousClass' => $o['linesCountBeforeClosingBrace'] ?? 1, + ]), + 'SlevomatCodingStandard.Classes.EnumCaseSpacing' => $memberSpacing('enumCase'), + 'SlevomatCodingStandard.Classes.MethodSpacing' => fn(array $o, Translation $t) => $t + ->setBlankLines('blankLines.aroundMethods', $o['minLinesCount'] ?? 1, $o['maxLinesCount'] ?? 1) + ->setBlankLines('blankLines.aroundInterfaceMethods', $o['minLinesCount'] ?? 1, $o['maxLinesCount'] ?? 1), + 'SlevomatCodingStandard.Classes.ModernClassNameReference' => fn(array $o, Translation $t) => $t->setAll([ + 'hygiene.classNameNotation.currentClass' => 'required', + 'hygiene.classNameNotation.object' => ($o['enableOnObjects'] ?? false) ? 'required' : 'keep', + ]), + 'SlevomatCodingStandard.Classes.PropertyDeclaration' => [ + 'classes.modifiers.order' => 'canonical', + ], + 'SlevomatCodingStandard.Classes.PropertySpacing' => $memberSpacing('property'), + 'SlevomatCodingStandard.Classes.RequireConstructorPropertyPromotion' => fn(array $o, Translation $t) => ($o['enable'] ?? true) + ? $t->set('upgrading.syntax.promotedProperties', 'required') + : $t->keep('upgrading.syntax.promotedProperties'), + 'SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature' => function (array $o, Translation $t) { + if (($min = $o['minLineLength'] ?? 121) > 1) { + $t->setLineLength($min - 1); + } else { + $t->warn("`SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature` with `minLineLength=$min` has no equivalent; DressCode spreads over lines only a signature longer than the line."); + } + + $t->setAll([ + 'multiline.items.parameter' => 'perLine', + 'multiline.edges.parameter' => 'apart', + 'multiline.spreadAlways' => ($o['withPromotedProperties'] ?? false) ? ['promotedProperty'] : [], + ]); + }, + 'SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming' => fn(array $o, Translation $t) => $t->set( + 'naming.abstractClassAffix', + 'forbidden', + ), + 'SlevomatCodingStandard.Classes.SuperfluousErrorNaming' => fn(array $o, Translation $t) => $t->set( + 'naming.errorAffix', + 'forbidden', + ), + 'SlevomatCodingStandard.Classes.SuperfluousExceptionNaming' => fn(array $o, Translation $t) => $t->set( + 'naming.exceptionAffix', + 'forbidden', + ), + 'SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming' => fn(array $o, Translation $t) => $t->set( + 'naming.interfaceAffix', + 'forbidden', + ), + 'SlevomatCodingStandard.Classes.SuperfluousTraitNaming' => fn(array $o, Translation $t) => $t->set( + 'naming.traitAffix', + 'forbidden', + ), + 'SlevomatCodingStandard.Classes.TraitUseDeclaration' => ['classes.members.grouping.traitUse' => 'separated'], + 'SlevomatCodingStandard.Classes.TraitUseSpacing' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.betweenTraitUses' => $o['linesCountBetweenUses'] ?? 0, + 'blankLines.afterTraitUses' => $o['linesCountAfterLastUse'] ?? 1, + ]), + 'SlevomatCodingStandard.Classes.UselessLateStaticBinding' => fn(array $o, Translation $t) => $t->setAll([ + 'qualification.currentClass' => 'self', + 'qualification.staticInFinalClass' => 'required', + ]), + 'SlevomatCodingStandard.Commenting.AnnotationName' => ['phpdoc.annotation.casing' => 'canonical'], + 'SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment' => ['phpdoc.property.lineSpan' => 'expanded'], + 'SlevomatCodingStandard.Commenting.EmptyComment' => ['comments.empty' => 'forbidden'], + 'SlevomatCodingStandard.Commenting.ForbiddenAnnotations' => fn(array $o, Translation $t) => $t->set( + 'phpdoc.forbiddenAnnotations', + ($o['forbiddenAnnotations'] ?? []) ?: 'keep', + ), + 'SlevomatCodingStandard.Commenting.ForbiddenComments' => fn(array $o, Translation $t) => $t->set( + 'phpdoc.forbiddenLines', + $o['forbiddenCommentPatterns'] ?? [], + ), + 'SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.InvalidCommentType' => ['phpdoc.annotation.plainComment' => 'forbidden'], + 'SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment' => ['phpdoc.property.lineSpan' => 'collapsed'], + 'SlevomatCodingStandard.Commenting.UselessFunctionDocComment' => ['phpdoc.annotation.nativeTypeOnly' => 'forbidden'], + 'SlevomatCodingStandard.Commenting.UselessInheritDocComment' => ['phpdoc.inheritdocOnly' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing' => function (array $o, Translation $t) { + $supported = ['if', 'do', 'while', 'for', 'foreach', 'switch', 'try']; + $unsupported = array_diff($o['controlStructures'] ?? [], $supported); + if ($unsupported) { + $t->warn('`SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing` with `' . implode('`, `', $unsupported) . '` has no equivalent; DressCode counts the blank lines before a `case` in `blankLines.betweenCases`.'); + } + + $statements = array_values(array_intersect($o['controlStructures'] ?? $supported, $supported)); + if ($statements) { + $t->setAll([ + 'blankLines.beforeStatement' => array_fill_keys($statements, $o['linesCountBefore'] ?? 1), + 'blankLines.afterStatement' => array_fill_keys($statements, $o['linesCountAfter'] ?? 1), + ]); + } + }, + 'SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch' => function (array $o, Translation $t) { + $t->warn('`SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch` is only reported by DressCode, which does not rewrite the `continue`.'); + $t->set('mistakes.continueInSwitch', 'forbidden'); + }, + 'SlevomatCodingStandard.ControlStructures.DisallowShortTernaryOperator' => ['expressions.ternary.short' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.DisallowTrailingMultiLineTernaryOperator' => ['multiline.operatorPosition.ternary' => 'lineStart'], + 'SlevomatCodingStandard.ControlStructures.DisallowYodaComparison' => ['expressions.comparison.yoda' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.RequireYodaComparison' => ['expressions.comparison.yoda' => 'required'], + 'SlevomatCodingStandard.ControlStructures.EarlyExit' => function (array $o, Translation $t) { + foreach (['ignoreStandaloneIfInScope', 'ignoreOneLineTrailingIf'] as $property) { + if ($o[$property] ?? false) { + $t->warn("`SlevomatCodingStandard.ControlStructures.EarlyExit` with `$property=true` has no equivalent; DressCode makes a guard of every trailing `if` that has enough statements."); + } + } + + $t->setAll([ + 'controlFlow.trailingIf' => 'forbidden', + 'controlFlow.trailingIfMinStatements' => ($o['ignoreTrailingIfWithOneInstruction'] ?? false) ? 2 : 1, + 'controlFlow.elseAfterReturn' => 'forbidden', + 'controlFlow.elseifAfterReturn' => 'forbidden', + ]); + }, + 'SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.beforeStatement' => array_fill_keys( + array_intersect( + $o['jumpStatements'] ?? ['break', 'continue', 'return', 'throw', 'yield'], + ['break', 'continue', 'return', 'throw', 'yield'], + ), + $o['linesCountBefore'] ?? 1, + ), + 'blankLines.afterStatement' => array_fill_keys( + array_intersect( + $o['jumpStatements'] ?? ['break', 'continue', 'return', 'throw', 'yield'], + ['break', 'continue', 'return', 'throw', 'yield'], + ), + $o['linesCountAfter'] ?? 1, + ), + ]), + 'SlevomatCodingStandard.ControlStructures.LanguageConstructWithParentheses' => ['expressions.parenthesesAfterConstruct' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.NewWithParentheses' => ['classes.emptyParentheses.instantiation' => 'required'], + 'SlevomatCodingStandard.ControlStructures.NewWithoutParentheses' => ['classes.emptyParentheses.instantiation' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition' => function (array $o, Translation $t) { + if (($min = $o['minLineLength'] ?? 121) > 1) { + $t->setLineLength($min - 1); + } else { + $t->warn("`SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition` with `minLineLength=$min` has no equivalent; DressCode spreads over lines only a condition longer than the line."); + } + + $t->setAll([ + 'multiline.items.condition' => ($o['alwaysSplitAllConditionParts'] ?? false) ? 'perLine' : ['perLine', 'fill'], + 'multiline.edges.condition' => ($o['alwaysSplitAllConditionParts'] ?? false) ? 'apart' : 'closingApart', + ]); + }, + 'SlevomatCodingStandard.ControlStructures.RequireMultiLineTernaryOperator' => function (array $o, Translation $t) { + if (($limit = $o['lineLengthLimit'] ?? 0) > 0) { + $t->setLineLength($limit); + } else { + $t->warn('`SlevomatCodingStandard.ControlStructures.RequireMultiLineTernaryOperator` with `lineLengthLimit=0` has no equivalent; DressCode spreads over lines only a ternary longer than the line.'); + } + + if (isset($o['minExpressionsLength'])) { + $t->warn('`SlevomatCodingStandard.ControlStructures.RequireMultiLineTernaryOperator` with `minExpressionsLength` has no equivalent; DressCode does not measure the expressions of a ternary.'); + } + + $t->set('multiline.items.ternary', 'perLine'); + }, + 'SlevomatCodingStandard.ControlStructures.RequireNullCoalesceEqualOperator' => ['expressions.assignment.targetInValue' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.RequireNullCoalesceOperator' => ['expressions.coalesce' => 'required'], + 'SlevomatCodingStandard.ControlStructures.RequireNullSafeObjectOperator' => fn(array $o, Translation $t) => ($o['enable'] ?? true) + ? $t->set('upgrading.syntax.nullsafe', 'required') + : $t->keep('upgrading.syntax.nullsafe'), + 'SlevomatCodingStandard.ControlStructures.RequireShortTernaryOperator' => ['expressions.ternary.short' => 'required'], + 'SlevomatCodingStandard.ControlStructures.RequireTernaryOperator' => ['controlFlow.ifElseReturning' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn' => ['controlFlow.ifReturningTrueFalse' => 'forbidden'], + 'SlevomatCodingStandard.ControlStructures.UselessTernaryOperator' => ['expressions.ternary.ofTrueFalse' => 'forbidden'], + 'SlevomatCodingStandard.Exceptions.DeadCatch' => ['mistakes.unreachableCatch' => 'forbidden'], + 'SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly' => ['mistakes.exceptionWhereThrowableBelongs' => 'forbidden'], + 'SlevomatCodingStandard.Exceptions.RequireNonCapturingCatch' => ['hygiene.unreadCatchVariable' => 'forbidden'], + 'SlevomatCodingStandard.Files.LineLength' => fn(array $o, Translation $t) => $t->setLineLength($o['lineLengthLimit'] ?? 120) + ->setAll([ + 'file.longLines' => 'forbidden', + 'file.lineLengthExcept' => ($o['ignoreImports'] ?? true) ? ['imports'] : [], + ]), + 'SlevomatCodingStandard.Functions.ArrowFunctionDeclaration' => fn(array $o, Translation $t) => match ($o['spacesCountAfterKeyword'] ?? 1) { + 0 => $t->set('spacing.arrowFunction', 'compact'), + 1 => $t->set('spacing.arrowFunction', 'spaced'), + default => $t->warn("`SlevomatCodingStandard.Functions.ArrowFunctionDeclaration` with `spacesCountAfterKeyword={$o['spacesCountAfterKeyword']}` has no equivalent; DressCode writes a single space after `fn` or none."), + }, + 'SlevomatCodingStandard.Functions.NamedArgumentSpacing' => ['spacing.namedArgument' => 'spacedAfter'], + 'SlevomatCodingStandard.Functions.RequireArrowFunction' => fn(array $o, Translation $t) => $t->setAll([ + 'functions.arrowFunction.oneExpression' => 'required', + 'functions.arrowFunctionNested' => $o['allowNested'] ?? true, + ]), + 'SlevomatCodingStandard.Functions.RequireTrailingCommaInCall' => fn(array $o, Translation $t) => $t->set( + 'multiline.trailingComma.argument', + 'required', + ), + 'SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration' => fn(array $o, Translation $t) => $t->set( + 'multiline.trailingComma.parameter', + 'required', + ), + 'SlevomatCodingStandard.Functions.StaticClosure' => ['functions.staticClosure' => 'required'], + 'SlevomatCodingStandard.Functions.StrictCall' => ['mistakes.strictArgument' => 'required'], + 'SlevomatCodingStandard.Functions.UselessParameterDefaultValue' => ['functions.unreachableParameterDefault' => 'forbidden'], + 'SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses' => fn(array $o, Translation $t) => $t->setAll([ + 'imports.order.names' => 'alphabetical', + 'imports.order.kinds' => ($o['psr12Compatible'] ?? true) ? ['class', 'function', 'constant'] : 'keep', + 'imports.order.caseSensitive' => $o['caseSensitive'] ?? false, + ]), + 'SlevomatCodingStandard.Namespaces.DisallowGroupUse' => function (array $o, Translation $t) { + foreach (['class', 'function', 'constant'] as $kind) { + $t->setAllowed("imports.grouping.$kind", ['separated', 'combined']); + } + }, + 'SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalConstants' => function (array $o, Translation $t) { + $named = [...$o['include'] ?? [], ...$o['exclude'] ?? []]; + if ($named !== []) { + $t->warn('`SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalConstants` names constants one by one (`' . implode('`, `', $named) . '`), which DressCode decides by group; a constant named follows `qualification.constant.global`.'); + } + + // only an include left empty names every constant + if (($o['include'] ?? []) === []) { + $t->setQualificationShape(['globalConstant' => 'backslashed']); + $t->setQualificationFallback(['constant' => 'qualified']); + } + }, + 'SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions' => function (array $o, Translation $t) { + $named = [...$o['include'] ?? [], ...$o['exclude'] ?? []]; + if ($named !== []) { + $t->warn('`SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions` names functions one by one (`' . implode('`, `', $named) . '`), which DressCode decides by group; a function named follows `qualification.function.global`, or `qualification.function.optimized` where the compiler optimizes it.'); + } + + // the special functions join the include, and only an include left empty names every function + if ($o['includeSpecialFunctions'] ?? false) { + $t->setQualificationShape(['optimizedFunction' => 'backslashed']); + $t->setQualificationFallback(['optimizedFunction' => 'qualified']); + } elseif (($o['include'] ?? []) === []) { + $t->setQualificationShape(['globalFunction' => 'backslashed']); + $t->setQualificationFallback(['function' => 'qualified']); + } + }, + // a comma between the names of a group use is one too + 'SlevomatCodingStandard.Namespaces.MultipleUsesPerLine' => [ + 'imports.grouping.class' => 'separated', + 'imports.grouping.function' => 'separated', + 'imports.grouping.constant' => 'separated', + ], + 'SlevomatCodingStandard.Namespaces.NamespaceSpacing' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.beforeNamespace' => $o['linesCountBeforeNamespace'] ?? 1, + 'blankLines.afterNamespace' => $o['linesCountAfterNamespace'] ?? 1, + ]), + 'SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly' => function (array $o, Translation $t) { + // a global name written with the backslash may stay so, and a bare one may stand on the fallback, or else it is imported + $shape = fn(bool $backslash) => $backslash ? 'keep' : 'imported'; + $t->setQualificationShape([ + 'class' => 'imported', + 'globalClass' => ($o['allowFullyQualifiedGlobalClasses'] ?? false) ? 'keep' : 'imported', + 'function' => 'imported', + 'globalFunction' => $shape($o['allowFullyQualifiedGlobalFunctions'] ?? false), + 'constant' => 'imported', + 'globalConstant' => $shape($o['allowFullyQualifiedGlobalConstants'] ?? false), + ]); + $fallback = array_filter([ + 'function' => ($o['allowFallbackGlobalFunctions'] ?? true) ? null : 'qualified', + 'constant' => ($o['allowFallbackGlobalConstants'] ?? true) ? null : 'qualified', + ]); + $t->setQualificationFallback($fallback); + if (($o['allowPartialUses'] ?? true) === false) { + $t->warn('`SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly` with `allowPartialUses=false` has no equivalent; DressCode lets a partial name stand.'); + } + }, + 'SlevomatCodingStandard.Namespaces.UnusedUses' => function (array $o, Translation $t) { + if (!($o['searchAnnotations'] ?? false)) { + $t->warn('`SlevomatCodingStandard.Namespaces.UnusedUses` without `searchAnnotations` removes an import only a doc comment uses; DressCode keeps it.'); + } + + $t->set('imports.unused', 'forbidden'); + }, + 'SlevomatCodingStandard.Namespaces.UseDoesNotStartWithBackslash' => fn(array $o, Translation $t) => $t->set( + 'imports.leadingBackslash', + 'forbidden', + ), + 'SlevomatCodingStandard.Namespaces.UseFromSameNamespace' => ['imports.fromCurrentNamespace' => 'forbidden'], + 'SlevomatCodingStandard.Namespaces.UselessAlias' => ['imports.aliasEqualToName' => 'forbidden'], + 'SlevomatCodingStandard.Numbers.DisallowNumericLiteralSeparator' => [ + 'literals.digitSeparator.integer' => 'forbidden', + 'literals.digitSeparator.fraction' => 'forbidden', + ], + 'SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator' => fn(array $o, Translation $t) => $t->setAll([ + 'literals.digitSeparator.integer' => 'required', + 'literals.digitSeparator.fraction' => 'required', + 'literals.digitSeparatorMinDigits.integer' => $o['minDigitsBeforeDecimalPoint'] ?? 4, + 'literals.digitSeparatorMinDigits.fraction' => $o['minDigitsAfterDecimalPoint'] ?? 4, + ]), + 'SlevomatCodingStandard.Operators.DisallowEqualOperators' => ['expressions.comparison.loose' => 'forbidden'], + 'SlevomatCodingStandard.Operators.NegationOperatorSpacing' => fn(array $o, Translation $t) => $t->set( + 'spacing.logicalNot', + ($o['spacesCount'] ?? 0) === 0 ? 'compact' : 'spaced', + ), + 'SlevomatCodingStandard.Operators.RequireCombinedAssignmentOperator' => ['expressions.assignment.targetInValue' => 'forbidden'], + 'SlevomatCodingStandard.Operators.SpreadOperatorSpacing' => ['spacing.spread' => 'compact'], + 'SlevomatCodingStandard.PHP.DisallowDirectMagicInvokeCall' => ['hygiene.__invokeCall' => 'forbidden'], + 'SlevomatCodingStandard.PHP.OptimizedFunctionsWithoutUnpacking' => fn(array $o, Translation $t) => $t->markCallsOptimized(), + 'SlevomatCodingStandard.PHP.ReferenceSpacing' => ['spacing.reference' => 'compact'], + 'SlevomatCodingStandard.PHP.RequireExplicitAssertion' => ['phpdoc.annotation.inlineVar' => 'forbidden'], + 'SlevomatCodingStandard.PHP.RequireNowdoc' => ['literals.heredocWithoutInterpolation' => 'forbidden'], + 'SlevomatCodingStandard.PHP.ShortList' => ['literals.arraySyntax.list' => 'short'], + 'SlevomatCodingStandard.PHP.TypeCast' => ['expressions.cast' => 'canonical'], + 'SlevomatCodingStandard.PHP.UselessSemicolon' => ['controlFlow.emptyStatement' => 'forbidden'], + 'SlevomatCodingStandard.TypeHints.DeclareStrictTypes' => function (array $o, Translation $t) { + if (($o['spacesCountAroundEqualsSign'] ?? 1) !== 0) { + $t->warn('`SlevomatCodingStandard.TypeHints.DeclareStrictTypes` with spaces around the equals sign has no equivalent; DressCode writes `declare(strict_types=1)` without spaces.'); + } + $t->setAll([ + 'file.strictTypes.declaration' => 'required', + 'file.strictTypes.position' => ($o['declareOnFirstLine'] ?? false) ? 'sameLine' : 'nextLine', + ]); + }, + 'SlevomatCodingStandard.TypeHints.DisallowArrayTypeHintSyntax' => fn(array $o, Translation $t) => $t->set( + 'phpdoc.types.array', + 'generic', + ), + 'SlevomatCodingStandard.TypeHints.DNFTypeHintFormat' => $typeFormat('SlevomatCodingStandard.TypeHints.DNFTypeHintFormat'), + 'SlevomatCodingStandard.TypeHints.LongTypeHints' => fn(array $o, Translation $t) => $t->set( + 'phpdoc.types.builtin', + 'canonical', + ), + 'SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition' => fn(array $o, Translation $t) => $t->set( + 'phpdoc.types.nullPosition', + 'last', + ), + 'SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue' => ['upgrading.php.implicitNullable' => 'forbidden'], + 'SlevomatCodingStandard.TypeHints.ParameterTypeHint' => fn(array $o, Translation $t) => $t + ->setAll(array_filter([ + 'types.declaration.parameter' => 'required', + 'phpdoc.annotation.nativeTypeOnly' => 'forbidden', + 'phpdoc.annotation.itemTypes' => 'required', + 'types.traversableClasses' => $o['traversableTypeHints'] ?? null, + ], fn($v) => $v !== null)), + 'SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing' => [ + 'spacing.typeDeclaration' => 'compact', + ], + 'SlevomatCodingStandard.TypeHints.PropertyTypeHint' => fn(array $o, Translation $t) => $t + ->setAll(array_filter([ + 'types.declaration.property' => 'required', + 'phpdoc.annotation.nativeTypeOnly' => 'forbidden', + 'phpdoc.annotation.itemTypes' => 'required', + 'types.traversableClasses' => $o['traversableTypeHints'] ?? null, + ], fn($v) => $v !== null)), + 'SlevomatCodingStandard.TypeHints.ReturnTypeHint' => fn(array $o, Translation $t) => $t + ->setAll(array_filter([ + 'types.declaration.return' => 'required', + 'types.declaration.anonymousFunction' => ($o['enableClosure'] ?? true) ? 'required' : null, + 'phpdoc.annotation.nativeTypeOnly' => 'forbidden', + 'phpdoc.annotation.itemTypes' => 'required', + 'types.traversableClasses' => $o['traversableTypeHints'] ?? null, + ], fn($v) => $v !== null)), + 'SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing' => [ + 'spacing.typeDeclaration' => 'compact', + ], + 'SlevomatCodingStandard.TypeHints.UnionTypeHintFormat' => $typeFormat('SlevomatCodingStandard.TypeHints.UnionTypeHintFormat'), + 'SlevomatCodingStandard.TypeHints.UselessConstantTypeHint' => ['phpdoc.annotation.constantVar' => 'forbidden'], + 'SlevomatCodingStandard.Variables.DuplicateAssignmentToVariable' => ['mistakes.repeatedAssignment' => 'forbidden'], + 'Squiz.Arrays.ArrayBracketSpacing' => ['spacing.padding.offset' => 'forbidden'], + 'Squiz.Classes.SelfMemberReference' => ['qualification.currentClass' => 'self'], + 'Squiz.Classes.ValidClassName' => fn(array $o, Translation $t) => $t->set('naming.class', 'PascalCase'), + 'Squiz.Commenting.DocCommentAlignment' => ['phpdoc.stars' => 'aligned'], + 'Squiz.Commenting.FunctionComment.DuplicateReturn' => ['phpdoc.annotation.duplicateReturn' => 'forbidden'], + 'Squiz.Commenting.FunctionComment.ExtraParamComment' => ['phpdoc.annotation.extraParam' => 'forbidden'], + 'Squiz.Commenting.VariableComment' => fn(array $o, Translation $t) => $t->setAll([ + 'phpdoc.property.plainComment' => 'forbidden', + 'phpdoc.annotation.duplicateVar' => 'forbidden', + 'phpdoc.annotation.empty' => 'forbidden', + ]), + 'Squiz.ControlStructures.ControlSignature' => fn(array $o, Translation $t) => $t->setAll([ + 'braces.position.controlStructure' => 'sameLine', + 'braces.position.continuingKeyword' => 'sameLine', + 'spacing.keyword' => 'spaced', + 'spacing.openBrace' => 'spaced', + ]), + 'Squiz.ControlStructures.ForEachLoopDeclaration' => function (array $o, Translation $t) { + if (($o['requiredSpacesAfterOpen'] ?? 0) > 0 || ($o['requiredSpacesBeforeClose'] ?? 0) > 0) { + $t->warn('`Squiz.ControlStructures.ForEachLoopDeclaration` with spaces inside the parentheses has no equivalent; DressCode writes none.'); + } + $t->warn('`Squiz.ControlStructures.ForEachLoopDeclaration` also puts a single space around the `=>` of a `foreach`, which DressCode decides for every binary operator together, by `spacing.binaryOperator`.'); + $t->setAll([ + ...Translation::NoSpaceInParentheses, + 'spacing.keyword' => 'spaced', + 'casing.keyword' => 'lowercase', + ]); + }, + 'Squiz.ControlStructures.ForLoopDeclaration' => function (array $o, Translation $t) { + if (($o['requiredSpacesAfterOpen'] ?? 0) > 0 || ($o['requiredSpacesBeforeClose'] ?? 0) > 0) { + $t->warn('`Squiz.ControlStructures.ForLoopDeclaration` with spaces inside the parentheses has no equivalent; DressCode writes none.'); + } + $t->setAll([ + ...Translation::NoSpaceInParentheses, + 'spacing.semicolon.before' => 'compact', + 'spacing.semicolon.after' => 'spaced', + ]); + }, + 'Squiz.ControlStructures.LowercaseDeclaration' => ['casing.keyword' => 'lowercase'], + 'Squiz.Functions.FunctionDeclaration' => ['spacing.keyword' => 'spaced', 'spacing.call' => 'compact'], + 'Squiz.Functions.FunctionDeclarationArgumentSpacing' => ['spacing.comma' => 'spaced', 'spacing.alignment.comma' => 'tabs'], + 'Squiz.Functions.LowercaseFunctionKeywords' => ['casing.keyword' => 'lowercase'], + 'Squiz.Functions.MultiLineFunctionDeclaration' => [ + 'braces.position.function' => 'nextLine', + 'braces.position.multilineSignature' => 'sameLine', + 'braces.position.closure' => 'sameLine', + 'multiline.items.parameter' => 'perLine', + 'multiline.edges.parameter' => 'apart', + ], + 'Squiz.NamingConventions.ValidFunctionName' => fn(array $o, Translation $t) => $t->setAll([ + 'naming.method' => 'camelCase', + 'naming.function' => 'camelCase', + ]), + 'Squiz.NamingConventions.ValidVariableName' => function (array $o, Translation $t) { + $t->warn('`Squiz.NamingConventions.ValidVariableName` also rules on the underscore prefix of private properties, which DressCode does not.'); + $t->setAll(['naming.property' => 'camelCase', 'naming.variable' => 'camelCase']); + }, + 'Squiz.Operators.ValidLogicalOperators' => ['expressions.wordLogicalOperator' => 'forbidden'], + 'Squiz.PHP.DiscouragedFunctions' => fn(array $o, Translation $t) => $t->set( + 'upgrading.api.forbiddenFunctions', + ['error_log' => null, 'print_r' => null, 'var_dump' => null], + ), + 'Squiz.PHP.GlobalKeyword' => ['mistakes.globalStatement' => 'forbidden'], + 'Squiz.PHP.InnerFunctions' => ['functions.innerFunction' => 'forbidden'], + 'Squiz.PHP.LowercasePHPFunctions' => ['casing.function' => 'declared'], + 'Squiz.Scope.MethodScope' => ['classes.visibility.constant' => 'required', + 'classes.visibility.property' => 'required', + 'classes.visibility.method' => 'required', 'classes.visibility.interfaceMethod' => 'required'], + 'Squiz.Scope.StaticThisUsage' => ['mistakes.thisOutsideObject' => 'forbidden'], + 'Squiz.Strings.ConcatenationSpacing' => fn(array $o, Translation $t) => $t->set( + 'spacing.concatenation', + ($o['spacing'] ?? 0) > 0 ? 'spaced' : 'compact', + ), + 'Squiz.Strings.DoubleQuoteUsage' => fn(array $o, Translation $t) => $t->set('literals.quotes', 'single'), + 'Squiz.Strings.EchoedStrings' => ['expressions.parenthesesAfterConstruct' => 'forbidden'], + 'Squiz.WhiteSpace.CastSpacing' => ['expressions.cast' => 'canonical'], + 'Squiz.WhiteSpace.ControlStructureSpacing' => function (array $o, Translation $t) { + $t->setAll([ + 'spacing.padding.controlStructure' => 'forbidden', + 'blankLines.bodyStart.controlStructure' => 0, + 'blankLines.bodyEnd.controlStructure' => 0, + 'blankLines.afterStatement' => array_fill_keys(['do', 'for', 'foreach', 'if', 'switch', 'try', 'while'], [1, null]), + ]); + }, + 'Squiz.WhiteSpace.FunctionOpeningBraceSpace' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.bodyStart.function' => 0, + 'blankLines.bodyStart.closure' => 0, + ]), + 'Squiz.WhiteSpace.FunctionSpacing' => function (array $o, Translation $t) { + $t->warn('`Squiz.WhiteSpace.FunctionSpacing` leaves the blank lines around classes alone, while `blankLines` sets them together with those around functions.'); + $t->setAll([ + 'blankLines.aroundDeclarations' => $o['spacing'] ?? 2, + 'blankLines.aroundMethods' => $o['spacing'] ?? 2, + 'blankLines.aroundInterfaceMethods' => $o['spacing'] ?? 2, + 'blankLines.bodyStart.class' => $o['spacingBeforeFirst'] ?? 2, + 'blankLines.bodyEnd.class' => $o['spacingAfterLast'] ?? 2, + ]); + }, + 'Squiz.WhiteSpace.LogicalOperatorSpacing' => [ + 'spacing.binaryOperator' => 'spaced', + 'spacing.alignment.binaryOperator' => 'spaces', + ], + 'Squiz.WhiteSpace.ObjectOperatorSpacing' => ['spacing.objectOperator' => 'compact'], + 'Squiz.WhiteSpace.OperatorSpacing' => ['spacing.binaryOperator' => 'spaced', 'spacing.alignment.binaryOperator' => 'spaces'], + 'Squiz.WhiteSpace.OperatorSpacing.Unary' => ['spacing.unaryOperator' => 'compact', 'spacing.logicalNot' => 'compact'], + 'Squiz.WhiteSpace.ScopeClosingBrace' => fn(array $o, Translation $t) => $t->warn('`Squiz.WhiteSpace.ScopeClosingBrace` puts a closing brace on a line of its own at the indentation of the line opening it, which DressCode does wherever `braces` places the opening brace and `indentation.unit` indents.'), + 'Squiz.WhiteSpace.ScopeKeywordSpacing' => ['spacing.doubleColon' => 'compact'], + 'Squiz.WhiteSpace.SemicolonSpacing' => ['spacing.semicolon.before' => 'compact', 'multiline.semicolon' => 'sameLine'], + 'Squiz.WhiteSpace.SuperfluousWhitespace' => ['file.trailingWhitespace' => 'forbidden'], + ]; + } + + + /** + * Rules of a phpcs.xml ruleset: every with the properties it sets and every inside it or + * of 0 as a rule switched off, and what the ruleset says that no rule carries. A value is typed by its text, true, false + * and digits, because XML carries every value as a string. + * @return array{array>, list} + * @throws ConfigurationException + */ + public static function readConfig(string $file): array + { + if (!is_file($file)) { + throw new ConfigurationException("File `$file` does not exist."); + } + + $internal = libxml_use_internal_errors(use_errors: true); + libxml_clear_errors(); + $xml = simplexml_load_file($file); + $error = libxml_get_errors()[0] ?? null; + libxml_clear_errors(); + libxml_use_internal_errors($internal); + if ($xml === false) { + throw new ConfigurationException("File `$file` is not a PHP_CodeSniffer ruleset" . ($error ? ': ' . trim($error->message) . " on line $error->line." : '.')); + } + + $rules = $warnings = []; + if (isset($xml->{'exclude-pattern'})) { + $warnings[] = 'The paths the ruleset excludes are not carried over; set them with the key `excludePaths`.'; + } + + foreach ($xml->rule as $rule) { + $ref = (string) $rule['ref']; + $properties = []; + foreach ($rule->properties->property ?? [] as $property) { + $properties[(string) $property['name']] = self::readValue($property); + } + + $rules[$ref] = (string) $rule->severity === '0' ? false : ($properties === [] ? true : $properties); + foreach ($rule->exclude ?? [] as $exclude) { + $rules[(string) $exclude['name']] = false; + } + + if (isset($rule->{'exclude-pattern'})) { + $warnings[] = "The paths excluded from `$ref` are not carried over; turn its rules off there with an override."; + } + } + + return [$rules, $warnings]; + } + + + private static function readValue(\SimpleXMLElement $property): mixed + { + if ((string) $property['type'] === 'array') { + $items = []; + foreach ($property->element as $element) { + $key = (string) $element['key']; + $value = (string) $element['value']; + if ($key === '') { + $items[] = $value; + } else { + $items[$key] = $value; + } + } + + return $items === [] + ? array_map(trim(...), explode(',', (string) $property['value'])) + : $items; + } + + $value = (string) $property['value']; + return match (true) { + $value === 'true' => true, + $value === 'false' => false, + ctype_digit($value) => (int) $value, + default => $value, + }; + } + + + /** @return array standard => preset */ + public static function getSets(): array + { + return [ + 'PSR1' => 'dresscode/psr12', + 'PSR2' => 'dresscode/psr12', + 'PSR12' => 'dresscode/psr12', + ]; + } +} diff --git a/src/Interop/PhpCsFixer.php b/src/Interop/PhpCsFixer.php new file mode 100644 index 000000000..6de2b7a7d --- /dev/null +++ b/src/Interop/PhpCsFixer.php @@ -0,0 +1,1019 @@ +|\Closure(array, Translation): mixed> fixer => its decisions, or what to + * enable for its options; every option is read through `??` so that a fixer enabled with none translates too + */ + public static function getTranslations(): array + { + // the fixers indent by the indent of the configuration object, which the rules do not carry + $indentByConfig = function (array $o, Translation $t) { + $unit = match ($t->indent) { + "\t" => 'tab', + ' ' => '4 spaces', + ' ' => '2 spaces', + default => null, + }; + if ($unit === null) { + $t->warn('The fixers of the indentation indent by the `indent` ' . var_export($t->indent, return: true) . ' of the configuration, which has no equivalent; DressCode indents by a tab, four spaces or two.'); + } else { + $t->set('indentation.unit', $unit); + } + }; + + // the visibility of the elements the fixer names, `const`, `method` and `property` by default + $visibility = function (array $o, Translation $t) { + $elements = $o['elements'] ?? ['const', 'method', 'property']; + $t->setAll(array_filter([ + 'classes.visibility.constant' => in_array('const', $elements, true) ? 'required' : null, + 'classes.visibility.property' => in_array('property', $elements, true) ? 'required' : null, + 'classes.visibility.method' => in_array('method', $elements, true) ? 'required' : null, + 'classes.visibility.interfaceMethod' => in_array('method', $elements, true) ? 'required' : null, + 'classes.modifiers.order' => 'canonical', + ])); + }; + + // the options of the fixers of the types from the doc comments that reach beyond what `types.declaration` writes + $phpdocToType = fn(string $fixer, string $path) => function (array $o, Translation $t) use ($fixer, $path) { + foreach (['scalar_types' => 'a scalar type', 'union_types' => 'a union type'] as $option => $what) { + if (!($o[$option] ?? true)) { + $t->warn("`$fixer` with `$option=false` has no equivalent; DressCode also writes $what."); + } + } + $t->set($path, 'required'); + }; + + return [ + 'PhpCsFixerCustomFixers/declare_after_opening_tag' => fn(array $o, Translation $t) => $t->set( + 'file.strictTypes.position', + 'sameLine', + ), + 'PhpCsFixerCustomFixers/comment_surrounded_by_spaces' => ['spacing.comment.marker' => 'spaced'], + 'PhpCsFixerCustomFixers/commented_out_function' => fn(array $o, Translation $t) => $t->setAll([ + 'mistakes.debugOutput' => 'forbidden', + 'mistakes.debugOutputFunctions' => array_values(array_unique($o['functions'] ?? ['print_r', 'var_dump', 'var_export'])), + ]), + 'PhpCsFixerCustomFixers/no_leading_slash_in_global_namespace' => fn(array $o, Translation $t) => $t->setAll([ + 'imports.leadingBackslash' => 'forbidden', + 'qualification.leadingBackslash' => 'forbidden', + ]), + 'PhpCsFixerCustomFixers/no_superfluous_concatenation' => [ + 'literals.concatenatedLiterals' => 'forbidden', + 'literals.emptyStringConcatenation' => 'forbidden', + ], + 'PhpCsFixerCustomFixers/no_useless_dirname_call' => ['hygiene.dirnameOfFile' => 'forbidden'], + 'PhpCsFixerCustomFixers/no_useless_strlen' => ['hygiene.strlenEmptyTest' => 'forbidden'], + 'PhpCsFixerCustomFixers/numeric_literal_separator' => [ + 'literals.digitSeparator.integer' => 'required', + 'literals.digitSeparator.fraction' => 'required', + ], + 'PhpCsFixerCustomFixers/phpdoc_array_style' => fn(array $o, Translation $t) => $t->set('phpdoc.types.array', 'generic'), + 'PhpCsFixerCustomFixers/phpdoc_type_list' => function (array $o, Translation $t) { + $t->warn('`phpdoc_type_list` turns `array` into `list`, a stronger type, which DressCode does not do.'); + $t->set('phpdoc.types.builtin', 'canonical')->set('phpdoc.types.array', 'generic'); + }, + 'align_multiline_comment' => function (array $o, Translation $t) { + if (($o['comment_type'] ?? 'phpdocs_only') !== 'phpdocs_only') { + $t->warn('`align_multiline_comment` with `comment_type` other than `phpdocs_only` has no equivalent; DressCode aligns the stars of doc comments only.'); + } + $t->set('phpdoc.stars', 'aligned'); + }, + 'array_indentation' => $indentByConfig, + 'array_syntax' => fn(array $o, Translation $t) => $t->set('literals.arraySyntax.array', $o['syntax'] ?? 'short'), + 'assign_null_coalescing_to_coalesce_equal' => ['expressions.assignment.targetInValue' => 'forbidden'], + 'attribute_block_no_spaces' => ['spacing.attribute' => 'compact'], + 'attribute_empty_parentheses' => fn(array $o, Translation $t) => $t->set( + 'classes.emptyParentheses.attribute', + ($o['use_parentheses'] ?? false) ? 'required' : 'forbidden', + ), + 'backtick_to_shell_exec' => ['upgrading.php.backticks' => 'forbidden'], + 'binary_operator_spaces' => function (array $o, Translation $t) { + $default = $o['default'] ?? 'single_space'; + if (str_starts_with((string) $default, 'align')) { + $t->warn('`binary_operator_spaces` aligns operators; DressCode only keeps an alignment that is already there.'); + } + $t->setAll([ + 'spacing.binaryOperator' => 'spaced', + 'spacing.alignment.binaryOperator' => $default === 'single_space' ? 'forbidden' : 'spaces', + ]); + }, + 'blank_line_after_namespace' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.afterNamespace' => 1, + 'blankLines.beforeDeclaration' => 1, + ]), + 'blank_line_after_opening_tag' => fn(array $o, Translation $t) => $t->set('blankLines.afterOpeningTag', 1), + 'blank_line_before_statement' => function (array $o, Translation $t) { + $statements = $o['statements'] ?? ['break', 'continue', 'declare', 'return', 'throw', 'try']; + $supported = ['break', 'continue', 'do', 'for', 'foreach', 'if', 'return', 'switch', 'throw', 'try', 'while', 'yield']; + foreach (array_diff($statements, $supported) as $statement) { + $t->warn("`blank_line_before_statement` with `$statement` has no equivalent; DressCode knows no such statement kind."); + } + $t->set('blankLines.beforeStatement', array_fill_keys(array_intersect($statements, $supported), [1, null])); + }, + 'blank_line_between_import_groups' => fn(array $o, Translation $t) => $t->setAll([ + 'blankLines.betweenImports' => 0, + 'blankLines.betweenImportKinds' => 1, + ]), + 'blank_lines_before_namespace' => fn(array $o, Translation $t) => $t->set( + 'blankLines.beforeNamespace', + max(0, ($o['min_line_breaks'] ?? 2) - 1), + ), + 'braces_position' => function (array $o, Translation $t) { + // a function goes below its signature unless the signature ends with a line break, which a multi-line one does + $t->setAll([ + 'braces.position.function' => ($o['functions_opening_brace'] ?? 'next_line_unless_newline_at_signature_end') === 'same_line' + ? 'sameLine' + : 'nextLine', + 'braces.position.multilineSignature' => 'sameLine', + 'braces.position.class' => ($o['classes_opening_brace'] ?? 'next_line') === 'same_line' ? 'sameLine' : 'nextLine', + 'braces.position.anonymousClass' => ($o['anonymous_classes_opening_brace'] ?? 'same_line') === 'same_line' + ? 'sameLine' + : 'nextLine', + 'braces.position.closure' => ($o['anonymous_functions_opening_brace'] ?? 'same_line') === 'same_line' + ? 'sameLine' + : 'nextLine', + 'braces.position.controlStructure' => ($o['control_structures_opening_brace'] ?? 'same_line') === 'same_line' + ? 'sameLine' + : 'nextLine', + 'braces.singlelineClosure' => ($o['allow_single_line_anonymous_functions'] ?? true) ? 'keep' : 'forbidden', + ]); + // single_line_empty_body, which runs after it, collapses an empty anonymous class all the same + $t->prefer( + 'braces.emptyBody.anonymousClass', + ($o['allow_single_line_empty_anonymous_classes'] ?? true) ? 'keep' : 'expanded', + ); + }, + 'cast_spaces' => fn(array $o, Translation $t) => $t->set( + 'spacing.cast', + ($o['space'] ?? 'single') === 'single' ? 'spaced' : 'compact', + ), + 'class_definition' => fn(array $o, Translation $t) => $t->set( + 'spacing.anonymousClass', + ($o['space_before_parenthesis'] ?? false) ? 'spaced' : 'compact', + ), + 'class_keyword' => ['literals.classNameInString' => 'forbidden'], + 'class_attributes_separation' => function (array $o, Translation $t) { + // the blank lines before an element without and with a doc comment or an attribute + $counts = ['none' => [0, 0], 'one' => [1, 1], 'only_if_meta' => [0, 1]]; + $elements = $o['elements'] ?? [ + 'const' => 'one', + 'method' => 'one', + 'property' => 'one', + 'trait_import' => 'none', + 'case' => 'none', + ]; + if (isset($elements['method'])) { + [$min, $max] = $counts[$elements['method']] ?? [0, 1]; + $t->setBlankLines('blankLines.aroundMethods', $min, $max) + ->setBlankLines('blankLines.aroundInterfaceMethods', $min, $max); + } + + if (isset($elements['trait_import'])) { + [$min, $max] = $counts[$elements['trait_import']] ?? [0, 1]; + $t->setBlankLines('blankLines.betweenTraitUses', $min, $max); + } + + $members = array_intersect_key($elements, array_flip(['case', 'const', 'property'])); + ksort($members); + if ($members !== []) { + $plain = array_map(fn(string $value) => ($counts[$value] ?? [0, 1])[0], $members); + $documented = array_map(fn(string $value) => ($counts[$value] ?? [0, 1])[1], $members); + // only a separation the configuration chose is worth the warning, not the one of the defaults + if (count(array_unique($members)) > 1 && $members !== ['case' => 'none', 'const' => 'one', 'property' => 'one']) { + $t->warn('`class_attributes_separation` separates constants, properties and enum cases differently, while `blankLines` counts the lines between members of different kinds and before a documented one for them together; the translation takes the range of them all there.'); + } + + $places = ['case' => 'enumCase', 'const' => 'constant', 'property' => 'property']; + foreach ($plain as $element => $count) { + $t->setBlankLines("blankLines.betweenMembers.{$places[$element]}", $count, $count); + } + + $t->setBlankLines('blankLines.betweenMemberKinds', min($plain), max($plain)); + $t->setBlankLines('blankLines.beforeDocumentedMember', min($documented), max($documented)); + } + }, + 'class_reference_name_casing' => function (array $o, Translation $t) { + $t->set('casing.class', 'declared'); + $t->warn('`class_reference_name_casing` writes the classes PHP declares, while `casing.class` writes those of the project and its packages too where the configuration sets `typeAnalysis`.'); + }, + 'combine_consecutive_issets' => ['hygiene.separateIssets' => 'forbidden'], + 'combine_nested_dirname' => ['hygiene.dirnameOfFile' => 'forbidden'], + 'combine_consecutive_unsets' => ['hygiene.separateUnsets' => 'forbidden'], + 'compact_nullable_type_declaration' => ['spacing.typeDeclaration' => 'compact'], + 'concat_space' => fn(array $o, Translation $t) => $t->set( + 'spacing.concatenation', + ($o['spacing'] ?? 'none') === 'one' ? 'spaced' : 'compact', + ), + 'constant_case' => fn(array $o, Translation $t) => $t->set( + 'casing.trueFalseNull', + ($o['case'] ?? 'lower') === 'lower' ? 'lowercase' : 'uppercase', + ), + 'control_structure_braces' => ['braces.bracelessBody' => 'forbidden'], + 'control_structure_continuation_position' => fn(array $o, Translation $t) => $t->set( + 'braces.position.continuingKeyword', + ($o['position'] ?? 'same_line') === 'next_line' ? 'nextLine' : 'sameLine', + ), + 'declare_equal_normalize' => fn(array $o, Translation $t) => ($o['space'] ?? 'none') === 'none' + ? $t->set('spacing.declare', 'compact') + : $t->warn('`declare_equal_normalize` with `space=single` has no equivalent; DressCode writes `declare(strict_types=1)` without spaces.'), + 'declare_parentheses' => ['spacing.declare' => 'compact'], + 'declare_strict_types' => fn(array $o, Translation $t) => ($o['strategy'] ?? 'enforce') === 'remove' + ? $t->warn('`declare_strict_types` with `strategy=remove` has no equivalent; DressCode requires the declaration.') + : $t->setAll(['file.strictTypes.declaration' => 'required', 'file.strictTypes.position' => 'nextLine']), + 'dir_constant' => ['hygiene.dirnameOfFile' => 'forbidden'], + 'elseif' => ['controlFlow.elseif' => 'oneWord'], + 'encoding' => ['file.bom' => 'forbidden'], + 'escape_implicit_backslashes' => ['literals.implicitBackslash' => 'forbidden'], + // a tag of include and exclude is an annotation, written with or without its @, and since 3.x an attribute by the + // name the code writes it with, while DressCode names an attribute by its class + 'final_internal_class' => function (array $o, Translation $t) { + $t->set('classes.internalClass.final', 'required'); + $annotations = fn(array $tags) => array_map(fn(string $tag) => '@' . ltrim($tag, '@'), $tags); + $include = $o['include'] ?? $o['annotation_include'] ?? null; + if ($include === []) { + $t->warn('`final_internal_class` with an empty `include` makes every class final; `classes.internalClass.markers` cannot be empty, so it stays `@internal`.'); + } elseif ($include !== null) { + $t->set('classes.internalClass.markers', $annotations($include)); + } + + $exclude = $o['exclude'] ?? $o['annotation_exclude'] ?? null; + if ($exclude !== null) { + $t->set('classes.internalClass.except', $annotations($exclude)); + } + + $tags = [...$o['include'] ?? [], ...$o['exclude'] ?? []]; + if ($tags) { + $t->warn('`final_internal_class` reads `' . implode('`, `', $tags) . '` as attributes too, by the name the code writes them with; ' + . 'add an attribute to `classes.internalClass.markers` or `classes.internalClass.except` by its fully qualified class.'); + } + + if ($o['consider_absent_docblock_as_internal_class'] ?? false) { + $t->warn('`final_internal_class` makes a class without a doc comment final too, which DressCode does not.'); + } + }, + 'full_opening_tag' => ['file.openingTag' => 'required'], + 'fully_qualified_strict_types' => function (array $o, Translation $t) { + if (!($o['import_symbols'] ?? false)) { + $t->warn('`fully_qualified_strict_types` without `import_symbols` shortens a name only to an import the file has; DressCode adds the import it lacks.'); + } + + $t->setQualificationShape(['class' => 'imported', 'globalClass' => 'keep']); + // the tags whose names it shortens are the common ones unless the option names none + if (($o['phpdoc_tags'] ?? null) !== []) { + $t->set('qualification.class.phpdoc', 'imported'); + } + }, + 'function_declaration' => function (array $o, Translation $t) { + if (($o['closure_function_spacing'] ?? 'one') === 'none') { + $t->warn('`function_declaration` with `closure_function_spacing=none` has no equivalent; DressCode writes a space after `function`.'); + } else { + $t->set('spacing.keyword', 'spaced'); + } + + // the comma of parameters on one line is removed by every value but `keep`, a value another fixer gave staying + if ($o['trailing_comma_single_line'] ?? false) { + $t->warn('`function_declaration` with `trailing_comma_single_line=true` has no equivalent; DressCode removes the trailing comma of parameters on one line.'); + } else { + $t->prefer('multiline.trailingComma.parameter', 'optional'); + } + + $t->setAll([ + 'spacing.call' => 'compact', + ...Translation::NoSpaceInParentheses, + 'spacing.arrowFunction' => ($o['closure_fn_spacing'] ?? 'one') === 'one' ? 'spaced' : 'compact', + ]); + }, + 'general_phpdoc_annotation_remove' => function (array $o, Translation $t) { + if (!($o['case_sensitive'] ?? true)) { + $t->warn('`general_phpdoc_annotation_remove` with `case_sensitive=false` has no equivalent; DressCode removes an annotation by the letter case it is written in.'); + } + $annotations = array_map(fn(string $tag) => '@' . ltrim($tag, '@'), $o['annotations'] ?? []); + if ($annotations) { + $t->set('phpdoc.forbiddenAnnotations', $annotations); + } else { + $t->keep('phpdoc.forbiddenAnnotations'); + } + }, + 'get_class_to_class_keyword' => ['hygiene.classNameNotation.object' => 'required'], + 'global_namespace_import' => function (array $o, Translation $t) { + // true imports a name written with the backslash, false writes the backslash, null leaves the name alone, and + // a bare function or constant stands either way + $shape = fn(?bool $import) => match ($import) { + true => 'imported', + false => 'backslashed', + null => 'keep', + }; + $t->setQualificationShape([ + 'globalClass' => $shape(array_key_exists('import_classes', $o) ? $o['import_classes'] : true), + 'globalFunction' => $shape(array_key_exists('import_functions', $o) ? $o['import_functions'] : false), + 'globalConstant' => $shape(array_key_exists('import_constants', $o) ? $o['import_constants'] : false), + ]); + }, + 'group_import' => function (array $o, Translation $t) { + foreach ($o['group_types'] ?? ['classy', 'functions', 'constants'] as $type) { + $t->set('imports.grouping.' . match ($type) { + 'functions' => 'function', + 'constants' => 'constant', + default => 'class', + }, 'grouped'); + } + }, + 'heredoc_indentation' => fn(array $o, Translation $t) => $t->set( + 'indentation.heredoc', + ($o['indentation'] ?? 'start_plus_one') === 'start_plus_one' ? 1 : 0, + ), + 'heredoc_to_nowdoc' => ['literals.heredocWithoutInterpolation' => 'forbidden'], + 'include' => ['expressions.parenthesesAfterConstruct' => 'forbidden'], + 'indentation_type' => $indentByConfig, + 'increment_style' => fn(array $o, Translation $t) => $t->set( + 'expressions.assignment.increment', + ($o['style'] ?? 'pre') === 'pre' ? 'prefix' : 'postfix', + ), + 'integer_literal_case' => ['casing.hexDigit' => 'uppercase'], + 'is_null' => ['hygiene.is_null' => 'forbidden'], + 'linebreak_after_opening_tag' => fn(array $o, Translation $t) => $t->prefer('blankLines.afterOpeningTag', 0), + 'line_ending' => fn(array $o, Translation $t) => match ($t->lineEnding) { + "\n" => $t->set('file.lineEnding', 'LF'), + "\r\n" => $t->set('file.lineEnding', 'CRLF'), + default => $t->warn('`line_ending` writes the `lineEnding` ' . var_export($t->lineEnding, return: true) . ' of the configuration, which has no equivalent; DressCode ends a line with LF or CRLF.'), + }, + 'list_syntax' => fn(array $o, Translation $t) => $t->set('literals.arraySyntax.list', $o['syntax'] ?? 'short'), + 'logical_operators' => ['expressions.wordLogicalOperator' => 'forbidden'], + 'long_to_shorthand_operator' => ['expressions.assignment.targetInValue' => 'forbidden'], + 'lowercase_cast' => ['expressions.cast' => 'canonical'], + 'lowercase_keywords' => ['casing.keyword' => 'lowercase'], + 'lowercase_static_reference' => ['casing.keyword' => 'lowercase'], + 'magic_constant_casing' => ['casing.magicConstant' => 'uppercase'], + 'method_chaining_indentation' => ['indentation.chain' => 'flat'], + 'method_argument_space' => fn(array $o, Translation $t) => match ($o['on_multiline'] ?? 'ensure_fully_multiline') { + 'ensure_fully_multiline' => $t->setAll(['multiline.items.argument' => 'perLine', 'multiline.edges.argument' => 'apart']), + 'ignore' => $t, + default => $t->warn("`method_argument_space` with `on_multiline={$o['on_multiline']}` has no equivalent; DressCode never joins a multi-line call into one line."), + }, + 'modern_serialization_methods' => function (array $o, Translation $t) { + $t->warn('`modern_serialization_methods` renames `__sleep()` and `__wakeup()`; DressCode only reports them.'); + $t->set('upgrading.php.sleepAndWakeup', 'forbidden'); + }, + 'modernize_strpos' => function (array $o, Translation $t) { + if ($o['modernize_stripos'] ?? false) { + $t->warn('`modernize_strpos` with `modernize_stripos` rewrites `stripos()` too, which DressCode leaves alone, because PHP has no case-insensitive `str_contains()`.'); + } + + $t->set('upgrading.functions.substringFunctions', 'required'); + }, + 'modernize_types_casting' => ['hygiene.conversionFunction' => 'forbidden'], + 'modifier_keywords' => $visibility, + 'multiline_promoted_properties' => fn(array $o, Translation $t) => $t->set('multiline.spreadAlways', ['promotedProperty']), + 'multiline_whitespace_before_semicolons' => fn(array $o, Translation $t) => $t->set( + 'multiline.semicolon', + ($o['strategy'] ?? 'no_multi_line') === 'new_line_for_chained_calls' ? 'nextLine' : 'sameLine', + ), + 'native_function_casing' => function (array $o, Translation $t) { + $t->set('casing.function', 'declared'); + $t->warn('`native_function_casing` writes the functions PHP declares, while `casing.function` writes those of the project and its packages too where the configuration sets `typeAnalysis`.'); + }, + 'native_type_declaration_casing' => ['casing.type' => 'lowercase'], + 'new_expression_parentheses' => ['classes.instantiationWrapping' => 'forbidden'], + 'new_with_braces' => fn(array $o, Translation $t) => $t->setAll([ + 'classes.emptyParentheses.instantiation' => ($o['named_class'] ?? true) ? 'required' : 'forbidden', + 'classes.emptyParentheses.anonymousClass' => ($o['anonymous_class'] ?? true) ? 'required' : 'forbidden', + ]), + 'new_with_parentheses' => fn(array $o, Translation $t) => $t->setAll([ + 'classes.emptyParentheses.instantiation' => ($o['named_class'] ?? true) ? 'required' : 'forbidden', + 'classes.emptyParentheses.anonymousClass' => ($o['anonymous_class'] ?? true) ? 'required' : 'forbidden', + ]), + 'native_function_invocation' => function (array $o, Translation $t) { + if (($o['scope'] ?? 'all') === 'all') { + $t->warn('`native_function_invocation` with `scope=all` writes the backslash in the global namespace too, where DressCode writes none.'); + } + + $fallback = []; + $named = []; + foreach ($o['include'] ?? ['@compiler_optimized'] as $name) { + if ($name === '@compiler_optimized') { + $fallback['optimizedFunction'] = 'qualified'; + + } elseif ($name === '@internal' || $name === '@all') { + if ($name === '@internal') { + $t->warn('`native_function_invocation` with `@internal` writes the backslash before a global function of the project too; DressCode has no set of builtin functions.'); + } + + $fallback['function'] = 'qualified'; + + } else { + $named[] = $name; + } + } + + // strict takes the backslash from every function the include does not reach + if ($o['strict'] ?? true) { + $fallback['function'] ??= 'bare'; + } + + $named = [...$named, ...$o['exclude'] ?? []]; + if ($named !== []) { + $t->warn('`native_function_invocation` names functions one by one (`' . implode('`, `', $named) . '`), which DressCode decides by group; a function named follows `qualification.function.global`, or `qualification.function.optimized` where the compiler optimizes it.'); + } + + // the fixer qualifies with the backslash; a function neither the include nor strict reaches stays as it is + $shape = ['optimizedFunction' => 'backslashed']; + if (($fallback['function'] ?? null) === 'qualified') { + $shape['globalFunction'] = 'backslashed'; + } + + $t->setQualificationShape($shape); + $t->setQualificationFallback($fallback); + }, + 'native_constant_invocation' => function (array $o, Translation $t) { + if (($o['scope'] ?? 'all') === 'all') { + $t->warn('`native_constant_invocation` with `scope=all` writes the backslash in the global namespace too, where DressCode writes none.'); + } + + $fallback = []; + if ($o['fix_built_in'] ?? true) { + $t->warn('`native_constant_invocation` with `fix_built_in` writes the backslash before every constant of PHP; DressCode writes it before those the compiler computes with.'); + $fallback['optimizedConstant'] = 'qualified'; + } + + // strict takes the backslash from every constant the fixer does not escape + if ($o['strict'] ?? true) { + $fallback['constant'] = 'bare'; + } + + // true, false and null are left alone by the rule, so the default exclusion names nothing + $named = [...$o['include'] ?? [], ...array_diff($o['exclude'] ?? [], ['null', 'false', 'true'])]; + if ($named !== []) { + $t->warn('`native_constant_invocation` names constants one by one (`' . implode('`, `', $named) . '`), which DressCode decides by group; a constant named follows `qualification.constant.global`, or `qualification.constant.optimized` where the compiler computes with it.'); + } + + // the fixer qualifies with the backslash + $t->setQualificationShape(['optimizedConstant' => 'backslashed']); + $t->setQualificationFallback($fallback); + }, + 'no_alias_functions' => function (array $o, Translation $t) { + $sets = $o['sets'] ?? null; + foreach (array_intersect($sets ?? [], ['@mbreg', '@exif']) as $set) { + $t->warn("`no_alias_functions` with `$set` has no equivalent; PHP 8 has none of the aliases it replaces."); + } + + if (in_array('@snmp', $sets ?? [], true)) { + $t->warn('`no_alias_functions` with `@snmp` has no equivalent; DressCode leaves the aliases of the SNMP functions alone.'); + } + + $sets = array_map( + fn(string $set) => $set === '@internal' ? 'core' : strtolower(ltrim($set, '@')), + array_values(array_diff($sets ?? [], ['@mbreg', '@exif', '@snmp'])), + ); + $t->set('hygiene.aliasFunctions', ($o['sets'] ?? null) === null ? ['core', 'imap', 'pg'] : $sets); + }, + 'no_alternative_syntax' => ['braces.alternativeSyntax' => 'forbidden'], + 'no_blank_lines_after_class_opening' => ['blankLines.bodyStart.class' => 0, 'blankLines.bodyStart.anonymousClass' => 0], + 'no_blank_lines_after_phpdoc' => fn(array $o, Translation $t) => $t->set('blankLines.afterPhpdoc', 0), + 'no_break_comment' => fn(array $o, Translation $t) => $t->set( + 'controlFlow.switch.fallThroughComment', + $o['comment_text'] ?? 'no break', + ), + 'no_closing_tag' => ['file.closingTagAtEnd' => 'forbidden'], + 'no_empty_comment' => ['comments.empty' => 'forbidden'], + 'no_empty_phpdoc' => ['phpdoc.empty' => 'forbidden'], + 'no_empty_statement' => ['controlFlow.emptyStatement' => 'forbidden'], + 'no_extra_blank_lines' => function (array $o, Translation $t) { + $tokens = $o['tokens'] ?? ['extra']; + $jumps = array_values(array_intersect($tokens, ['break', 'continue', 'return', 'throw'])); + $unsupported = []; + foreach ($tokens as $token) { + match ($token) { + 'curly_brace_block' => $t->setAll([ + ...Translation::expandBlocks('blankLines.bodyStart', 0), + ...Translation::expandBlocks('blankLines.bodyEnd', 0), + ]), + 'break', 'continue', 'return', 'throw' => null, + 'attribute' => $t->set('blankLines.afterAttributes', 0), + 'use_trait' => $t->set('blankLines.betweenTraitUses', 0), + // imports of one kind lose the blank lines between them, those of different kinds keep what another + // fixer gives them + 'use' => $t->set('blankLines.betweenImports', 0)->prefer('blankLines.betweenImportKinds', 0), + default => $unsupported[] = $token, + }; + } + + if ($jumps) { + $t->warn('`no_extra_blank_lines` with `' . implode('`, `', $jumps) . '` removes the blank lines after the jump, which stand only before the closing brace or the next `case`, what follows a jump in its block being unreachable; set `blankLines.bodyEnd.controlStructure` and `blankLines.bodyEnd.switchCase` for those.'); + } + + if ($unsupported) { + $t->warn('`no_extra_blank_lines` with `' . implode('`, `', $unsupported) . '` has no equivalent; DressCode counts the blank lines place by place in `blankLines`, with no limit of their own for these.'); + } + }, + 'no_leading_import_slash' => fn(array $o, Translation $t) => $t->set('imports.leadingBackslash', 'forbidden'), + 'no_leading_namespace_whitespace' => $indentByConfig, + 'no_multiple_statements_per_line' => ['file.statementsPerLine' => 1], + 'no_null_property_initialization' => ['classes.untypedPropertyNullDefault' => 'forbidden'], + 'no_redundant_readonly_property' => ['classes.modifiers.implied' => 'forbidden'], + 'no_short_bool_cast' => ['expressions.doubleNegation' => 'forbidden'], + 'no_singleline_whitespace_before_semicolons' => ['spacing.semicolon.before' => 'compact'], + 'no_space_around_double_colon' => ['spacing.doubleColon' => 'compact'], + 'no_spaces_after_function_name' => ['spacing.call' => 'compact'], + 'no_spaces_around_offset' => ['spacing.padding.offset' => 'forbidden'], + 'no_spaces_inside_parenthesis' => Translation::NoSpaceInParentheses, + 'no_trailing_comma_in_singleline' => function (array $o, Translation $t) { + // a place another fixer asked to be required stays so, which removes the one-line comma as well + $places = [ + 'arguments' => 'multiline.trailingComma.argument', + 'array' => 'multiline.trailingComma.array', + 'array_destructuring' => 'multiline.trailingComma.list', + 'group_import' => 'multiline.trailingComma.import', + ]; + foreach ($o['elements'] ?? array_keys($places) as $element) { + if (isset($places[$element])) { + $t->prefer($places[$element], 'optional'); + } + } + }, + 'no_trailing_whitespace' => ['file.trailingWhitespace' => 'forbidden'], + 'no_trailing_whitespace_in_comment' => ['file.trailingWhitespace' => 'forbidden'], + 'no_trailing_whitespace_in_string' => ['mistakes.trailingWhitespaceInString' => 'forbidden'], + 'no_unneeded_braces' => function (array $o, Translation $t) { + if ($o['namespaces'] ?? false) { + $t->warn('`no_unneeded_braces` with `namespaces=true` also unwraps a braced namespace, which DressCode leaves alone.'); + } + $t->set('braces.bareStatementGroup', 'forbidden'); + }, + 'no_unneeded_control_parentheses' => ['expressions.parenthesesAfterConstruct' => 'forbidden'], + 'no_unneeded_final_method' => function (array $o, Translation $t) { + if (($o['private_methods'] ?? true) === false) { + $t->warn('`no_unneeded_final_method` with `private_methods=false` keeps `final` on a private method, which DressCode removes anyway.'); + } + + $t->set('classes.modifiers.implied', 'forbidden'); + }, + 'no_unneeded_curly_braces' => function (array $o, Translation $t) { + if ($o['namespaces'] ?? false) { + $t->warn('`no_unneeded_curly_braces` with `namespaces=true` also unwraps a braced namespace, which DressCode leaves alone.'); + } + $t->set('braces.bareStatementGroup', 'forbidden'); + }, + 'no_unneeded_import_alias' => ['imports.aliasEqualToName' => 'forbidden'], + 'no_unreachable_default_argument_value' => ['functions.unreachableParameterDefault' => 'forbidden'], + 'no_unused_imports' => ['imports.unused' => 'forbidden'], + 'no_superfluous_elseif' => fn(array $o, Translation $t) => $t->set('controlFlow.elseifAfterReturn', 'forbidden'), + 'no_superfluous_phpdoc_tags' => ['phpdoc.annotation.nativeTypeOnly' => 'forbidden'], + 'no_useless_concat_operator' => ['literals.concatenatedLiterals' => 'forbidden'], + 'no_useless_else' => ['controlFlow.elseAfterReturn' => 'forbidden'], + 'no_useless_return' => ['functions.trailingBareReturn' => 'forbidden'], + 'no_whitespace_before_comma_in_array' => fn(array $o, Translation $t) => $t->set('spacing.comma', 'spaced') + ->prefer('spacing.alignment.comma', 'optional'), + 'no_whitespace_in_blank_line' => ['file.trailingWhitespace' => 'forbidden'], + 'non_printable_character' => function (array $o, Translation $t) { + if (!($o['use_escape_sequences_in_strings'] ?? true)) { + $t->warn('`non_printable_character` without escape sequences has no equivalent; DressCode writes the character as an escape sequence to keep the value.'); + } + $t->set('mistakes.invisibleCharacter', 'forbidden'); + }, + 'not_operator_with_successor_space' => ['spacing.logicalNot' => 'spaced'], + 'not_operator_without_successor_space' => ['spacing.logicalNot' => 'compact'], + 'numeric_literal_separator' => function (array $o, Translation $t) { + $word = ($o['strategy'] ?? 'use_separator') === 'no_separator' ? 'forbidden' : 'required'; + $t->setAll(['literals.digitSeparator.integer' => $word, 'literals.digitSeparator.fraction' => $word]); + }, + 'nullable_type_declaration' => fn(array $o, Translation $t) => $t->set( + 'types.nullable', + ($o['syntax'] ?? 'question_mark') === 'union' ? 'union' : 'questionMark', + ), + 'nullable_type_declaration_for_default_null_value' => fn(array $o, Translation $t) => ($o['use_nullable_type_declaration'] ?? true) + ? $t->set('upgrading.php.implicitNullable', 'forbidden') + : $t->warn('`nullable_type_declaration_for_default_null_value` with `use_nullable_type_declaration=false` has no equivalent; DressCode adds the `?` instead of removing it.'), + 'object_operator_without_whitespace' => ['spacing.objectOperator' => 'compact'], + 'octal_notation' => ['upgrading.syntax.octalPrefix' => 'required'], + 'operator_linebreak' => function (array $o, Translation $t) { + $position = ($o['position'] ?? 'beginning') === 'end' ? 'lineEnd' : 'lineStart'; + $places = ($o['only_booleans'] ?? false) + ? ['logical', 'condition'] + : ['logical', 'condition', 'concatenation', 'arithmetic', 'bitwise', 'coalesce', 'pipe', 'comparison', 'ternary']; + $t->setAll(array_fill_keys(array_map(fn(string $place) => "multiline.operatorPosition.$place", $places), $position)); + }, + 'ordered_class_elements' => function (array $o, Translation $t) { + if (($o['sort_algorithm'] ?? 'none') !== 'none') { + $t->warn('`ordered_class_elements` sorts members by name; DressCode orders them by kind only.'); + } + $kinds = [ + 'use_trait' => 'traitUse', 'constant' => 'constant', 'constant_public' => 'publicConstant', + 'constant_protected' => 'protectedConstant', 'constant_private' => 'privateConstant', 'property' => 'property', + 'property_public' => 'publicProperty', 'property_protected' => 'protectedProperty', + 'property_private' => 'privateProperty', 'property_public_static' => 'publicStaticProperty', + 'property_protected_static' => 'protectedStaticProperty', 'property_private_static' => 'privateStaticProperty', + 'method' => 'method', 'method_public' => 'publicMethod', 'method_protected' => 'protectedMethod', + 'method_private' => 'privateMethod', 'method_public_static' => 'publicStaticMethod', + 'method_protected_static' => 'protectedStaticMethod', 'method_private_static' => 'privateStaticMethod', + 'construct' => 'constructor', 'destruct' => 'destructor', 'magic' => 'magicMethod', 'case' => 'enumCase', + // a static member of any visibility is a static one of each, in the place of the kind + 'property_static' => ['publicStaticProperty', 'protectedStaticProperty', 'privateStaticProperty'], + 'method_static' => ['publicStaticMethod', 'protectedStaticMethod', 'privateStaticMethod'], + ]; + $order = $unknown = $approximated = []; + foreach ($o['order'] ?? [] as $kind) { + if (isset($kinds[$kind])) { + array_push($order, ...(array) $kinds[$kind]); + } elseif (preg_match('~^(property|method)_(public|protected|private)_(abstract|readonly)~', $kind, $m)) { + $order[] = $m[2] . (str_ends_with($kind, '_static') ? 'Static' : '') . ucfirst($m[1]); + $approximated[] = $kind; + } else { + $unknown[] = $kind; + } + } + + $order = array_values(array_unique($order)); + if ($approximated) { + $t->warn('DressCode orders an abstract or a readonly member with the others of its visibility, so `' . implode('`, `', $approximated) . '` of `ordered_class_elements` stand for those.'); + } + + if ($unknown) { + $t->warn('The kinds `' . implode('`, `', $unknown) . '` of `ordered_class_elements` have no equivalent in `classes.members.order` and were left out.'); + } + + // a kind its three visibilities leave nothing to name is no kind of the order + foreach (['constant' => 'Constant', 'property' => 'Property', 'method' => 'Method'] as $kind => $word) { + if (!array_diff(["public$word", "protected$word", "private$word"], $order)) { + $order = array_values(array_diff($order, [$kind])); + } + } + + $t->setOrder('classes.members.order', $order ?: [ + 'traitUse', 'enumCase', 'publicConstant', 'protectedConstant', 'privateConstant', + 'publicProperty', 'protectedProperty', 'privateProperty', 'constructor', 'destructor', 'magicMethod', + 'publicMethod', 'protectedMethod', 'privateMethod', + ]); + }, + 'ordered_types' => function (array $o, Translation $t) { + if ($o['case_sensitive'] ?? false) { + $t->warn('`ordered_types` with `case_sensitive` sorts capitals first; `types.unionOrder` sorts regardless of case.'); + } + + $t->setAll([ + 'types.unionOrder' => ($o['sort_algorithm'] ?? 'alpha') === 'alpha' ? 'alphabetical' : 'keep', + 'types.nullPosition' => match ($o['null_adjustment'] ?? 'always_first') { + 'always_last' => 'last', + 'none' => 'keep', + default => 'first', + }, + ]); + }, + 'ordered_imports' => function (array $o, Translation $t) { + $sort = $o['sort_algorithm'] ?? 'alpha'; + if ($sort === 'length') { + $t->warn('`ordered_imports` with `sort_algorithm=length` has no equivalent; DressCode sorts alphabetically.'); + } + $kinds = $o['imports_order'] ?? null; + $t->setAll([ + 'imports.order.names' => $sort === 'none' ? 'keep' : 'alphabetical', + 'imports.order.kinds' => $kinds === null + ? 'keep' + : array_map(fn(string $kind) => $kind === 'const' ? 'constant' : $kind, $kinds), + 'imports.order.caseSensitive' => $o['case_sensitive'] ?? false, + ]); + }, + 'phpdoc_array_type' => fn(array $o, Translation $t) => $t->set('phpdoc.types.array', 'generic'), + 'phpdoc_indent' => ['phpdoc.stars' => 'aligned'], + 'phpdoc_line_span' => function (array $o, Translation $t) { + // a place given null is left as it is, one not given is spread + $spanOf = fn(string $place) => array_key_exists($place, $o) ? $o[$place] : 'multi'; + if ($spanOf('const') !== null || $spanOf('method') !== null) { + $t->warn('`phpdoc_line_span` of a constant or a method has no equivalent; DressCode leaves their doc comments as they are.'); + } + + $span = $spanOf('property'); + if ($span !== null) { + $t->set('phpdoc.property.lineSpan', $span === 'single' ? 'collapsed' : 'expanded'); + } + }, + 'phpdoc_list_type' => function (array $o, Translation $t) { + $t->warn('`phpdoc_list_type` turns `array` into `list`, a stronger type, which DressCode does not do.'); + $t->set('phpdoc.types.builtin', 'canonical')->set('phpdoc.types.array', 'generic'); + }, + 'phpdoc_no_access' => fn(array $o, Translation $t) => $t->set('phpdoc.forbiddenAnnotations', ['@access']), + 'phpdoc_no_duplicate_types' => ['phpdoc.types.duplicate' => 'forbidden'], + 'phpdoc_no_package' => fn(array $o, Translation $t) => $t->set('phpdoc.forbiddenAnnotations', ['@package', '@subpackage']), + 'phpdoc_no_useless_inheritdoc' => function (array $o, Translation $t) { + $t->warn('`phpdoc_no_useless_inheritdoc` removes `@inheritdoc` only from a class that inherits nothing; DressCode removes a doc comment of `@inheritDoc` alone everywhere.'); + $t->set('phpdoc.inheritdocOnly', 'forbidden'); + }, + 'phpdoc_readonly_class_comment_to_keyword' => ['upgrading.phpdoc.readonly' => 'required'], + 'phpdoc_scalar' => fn(array $o, Translation $t) => $t->set('phpdoc.types.builtin', 'canonical'), + 'phpdoc_tag_casing' => function (array $o, Translation $t) { + if (array_diff($o['tags'] ?? ['inheritDoc'], ['inheritDoc'])) { + $t->warn('`phpdoc_tag_casing` with `tags` other than `inheritDoc` has no equivalent; DressCode writes the annotations it knows in their canonical case.'); + } + $t->set('phpdoc.annotation.casing', 'canonical'); + }, + 'phpdoc_to_param_type' => $phpdocToType('phpdoc_to_param_type', 'types.declaration.parameter'), + 'phpdoc_to_property_type' => $phpdocToType('phpdoc_to_property_type', 'types.declaration.property'), + 'phpdoc_to_return_type' => $phpdocToType('phpdoc_to_return_type', 'types.declaration.return'), + 'phpdoc_trim' => ['phpdoc.blankLines' => 'trimmed'], + 'phpdoc_trim_consecutive_blank_line_separation' => ['phpdoc.blankLines' => 'trimmed'], + 'phpdoc_types' => fn(array $o, Translation $t) => $t->set('phpdoc.types.builtin', 'canonical'), + 'phpdoc_types_no_duplicates' => ['phpdoc.types.duplicate' => 'forbidden'], + 'phpdoc_types_order' => function (array $o, Translation $t) { + if ($o['case_sensitive'] ?? false) { + $t->warn('`phpdoc_types_order` with `case_sensitive=true` has no equivalent; DressCode sorts case-insensitively.'); + } + $t->setAll([ + 'phpdoc.types.unionOrder' => ($o['sort_algorithm'] ?? 'alpha') === 'alpha' ? 'alphabetical' : 'keep', + 'phpdoc.types.nullPosition' => match ($o['null_adjustment'] ?? 'always_first') { + 'always_last' => 'last', + 'none' => 'keep', + default => 'first', + }, + ]); + }, + 'protected_to_private' => ['classes.protectedInFinalClass' => 'forbidden', 'classes.protectedInEnum' => 'forbidden'], + 'regular_callable_call' => ['hygiene.call_user_func' => 'forbidden'], + 'return_type_declaration' => fn(array $o, Translation $t) => ($o['space_before'] ?? 'none') === 'none' + ? $t->set('spacing.typeDeclaration', 'compact') + : $t->warn('`return_type_declaration` with `space_before=one` has no equivalent; DressCode writes no space before the colon.'), + 'self_accessor' => ['qualification.currentClass' => 'self'], + 'self_static_accessor' => fn(array $o, Translation $t) => $t->set('qualification.staticInFinalClass', 'required'), + 'set_type_to_cast' => ['hygiene.settype' => 'forbidden'], + 'short_scalar_cast' => ['expressions.cast' => 'canonical'], + 'simple_to_complex_string_variable' => ['upgrading.php.dollarBraceInterpolation' => 'forbidden'], + 'simplified_if_return' => ['controlFlow.ifReturningTrueFalse' => 'forbidden'], + 'single_blank_line_at_eof' => ['file.finalLineEndings' => 1], + 'single_class_element_per_statement' => fn(array $o, Translation $t) => $t->setAll(array_fill_keys( + array_map(fn(string $e) => 'classes.members.grouping.' . match ($e) { + 'const' => 'constant', + 'trait' => 'traitUse', + default => $e, + }, $o['elements'] ?? ['const', 'property']), + 'separated', + )), + 'single_import_per_statement' => function (array $o, Translation $t) { + foreach (['class', 'function', 'constant'] as $kind) { + if ($o['group_to_single_imports'] ?? true) { + $t->set("imports.grouping.$kind", 'separated'); + } else { // a group use it lets stay gives way to a fixer making one + $t->prefer("imports.grouping.$kind", ['separated', 'grouped']); + } + } + }, + 'single_line_after_imports' => ['blankLines.afterImports' => 1, 'blankLines.beforeDeclaration' => 1], + 'single_line_comment_spacing' => ['spacing.comment.marker' => 'spaced'], + 'single_line_comment_style' => fn(array $o, Translation $t) => in_array( + 'hash', + $o['comment_types'] ?? ['asterisk', 'hash'], + true, + ) + ? $t->set('comments.singleline', 'slashes') + : $t->warn('`single_line_comment_style` without `hash` has no equivalent; DressCode only rewrites the hash comment.'), + 'single_line_empty_body' => [ + 'braces.emptyBody.class' => 'collapsed', + 'braces.emptyBody.anonymousClass' => 'collapsed', + 'braces.emptyBody.function' => 'collapsed', + 'braces.emptyBody.closure' => 'collapsed', + ], + 'single_quote' => fn(array $o, Translation $t) => $t->set('literals.quotes', 'single'), + 'single_space_around_construct' => function (array $o, Translation $t) { + // the space of a single construct is that of every keyword, which DressCode decides at once + if ( + ($o['constructs_followed_by_a_single_space'] ?? null) !== [] + || ($o['constructs_preceded_by_a_single_space'] ?? null) !== [] + ) { + $t->set('spacing.keyword', 'spaced'); + } + }, + 'single_trait_insert_per_statement' => ['classes.members.grouping.traitUse' => 'separated'], + 'space_after_semicolon' => function (array $o, Translation $t) { + if ($o['remove_in_empty_for_expressions'] ?? false) { + $t->warn('`space_after_semicolon` with `remove_in_empty_for_expressions=true` removes the space in an empty expression of a `for`, which DressCode does not tell apart.'); + } + $t->set('spacing.semicolon.after', 'spaced'); + }, + 'spaces_inside_parentheses' => fn(array $o, Translation $t) => ($o['space'] ?? 'none') === 'none' + ? $t->setAll(Translation::NoSpaceInParentheses) + : $t->warn('`spaces_inside_parentheses` with `space=single` has no equivalent; DressCode writes no space inside parentheses.'), + 'standardize_increment' => ['expressions.assignment.addingOne' => 'forbidden'], + 'standardize_not_equals' => ['expressions.comparison.notEquals' => 'exclamation'], + 'statement_indentation' => $indentByConfig, + 'static_lambda' => ['functions.staticClosure' => 'required'], + 'static_private_method' => ['classes.staticMethod' => 'required'], + 'strict_comparison' => ['expressions.comparison.loose' => 'forbidden'], + 'strict_param' => ['mistakes.strictArgument' => 'required'], + 'string_implicit_backslashes' => function (array $o, Translation $t) { + $modes = [$o['double_quoted'] ?? 'escape', $o['heredoc'] ?? 'escape', $o['single_quoted'] ?? 'unescape']; + if (in_array('unescape', $modes, true)) { + $t->warn('`string_implicit_backslashes` unescapes some strings; DressCode always escapes the backslash.'); + } + $t->set('literals.implicitBackslash', 'forbidden'); + }, + 'string_length_to_empty' => ['hygiene.strlenEmptyTest' => 'forbidden'], + 'switch_case_semicolon_to_colon' => ['upgrading.php.caseSemicolon' => 'forbidden'], + 'switch_case_space' => ['spacing.switchCase' => 'compact'], + 'switch_continue_to_break' => function (array $o, Translation $t) { + $t->warn('`switch_continue_to_break` rewrites the `continue`; DressCode only reports it.'); + $t->set('mistakes.continueInSwitch', 'forbidden'); + }, + 'ternary_operator_spaces' => [ + 'spacing.ternary' => 'spaced', + 'spacing.alignment.ternary' => 'optional', + ], + 'ternary_to_elvis_operator' => ['expressions.ternary.short' => 'required'], + 'ternary_to_null_coalescing' => ['expressions.coalesce' => 'required'], + 'trailing_comma_in_multiline' => function (array $o, Translation $t) { + $elements = $o['elements'] ?? ['arrays']; + if (in_array('array_destructuring', $elements, true)) { + $t->warn(in_array('arrays', $elements, true) + ? '`trailing_comma_in_multiline` with `array_destructuring` is translated only for the short `[...]`, which `trailingComma` sets under the key `array`; a multi-line `list()` keeps its comma as written.' + : '`trailing_comma_in_multiline` with `array_destructuring` and without `arrays` has no equivalent; DressCode decides the comma of a short `[...]` together with that of an array, under the key `array` of `trailingComma`, and a multi-line `list()` keeps its comma as written.'); + } + $places = [ + 'arrays' => 'multiline.trailingComma.array', + 'arguments' => 'multiline.trailingComma.argument', + 'parameters' => 'multiline.trailingComma.parameter', + 'match' => 'multiline.trailingComma.matchArm', + ]; + $t->setAll(array_fill_keys(array_values(array_intersect_key($places, array_flip($elements))), 'required')); + }, + 'trim_array_spaces' => Translation::NoSpaceInArrays, + 'type_declaration_spaces' => ['spacing.typeDeclaration' => 'compact'], + 'types_spaces' => fn(array $o, Translation $t) => ($o['space'] ?? 'none') === 'none' + ? $t->set('spacing.typeDeclaration', 'compact') + : $t->warn('`types_spaces` with `space=single` has no equivalent; DressCode writes a type without whitespace around `|` and `&`.'), + 'unary_operator_spaces' => fn(array $o, Translation $t) => ($o['only_dec_inc'] ?? false) + ? $t->warn('`unary_operator_spaces` with `only_dec_inc=true` has no equivalent; DressCode decides the space after `-`, `+`, `~` and `@` with the one after `++` and `--`, so it leaves them all alone.') + // the space after `!` is not_operator_with_successor_space's where the configuration has it too + : $t->set('spacing.unaryOperator', 'compact')->prefer('spacing.logicalNot', 'compact'), + 'use_arrow_functions' => ['functions.arrowFunction.oneExpression' => 'required'], + 'visibility_required' => $visibility, + 'void_return' => function (array $o, Translation $t) { + $t->warn('`void_return` adds only `void`; `types.declaration.return` also takes the type of `@return`.'); + $t->set('types.declaration.return', 'required'); + if ($o['fix_lambda'] ?? true) { + $t->set('types.declaration.anonymousFunction', 'required'); + } + }, + 'whitespace_after_comma_in_array' => fn(array $o, Translation $t) => $t->setAll([ + 'spacing.comma' => 'spaced', + 'spacing.alignment.comma' => ($o['ensure_single_space'] ?? false) ? 'forbidden' : 'optional', + ]), + 'yoda_style' => function (array $o, Translation $t) { + $equal = array_key_exists('equal', $o) ? $o['equal'] : true; + $identical = array_key_exists('identical', $o) ? $o['identical'] : true; + if ($equal !== $identical || ($o['less_and_greater'] ?? null) !== null) { + $t->warn('`yoda_style` decides `==`, `===`, `<` and `>` one by one, which DressCode decides together for the equality comparisons alone.'); + } + + $t->set('expressions.comparison.yoda', match ($identical) { + true => 'required', + false => 'forbidden', + default => 'keep', + }); + }, + ]; + } + + + /** + * Rules of a .php-cs-fixer.php, which returns a configuration object, or of a file returning the rules + * themselves, with the indent and the line ending the fixers of whitespace write, those of PHP CS Fixer where the + * object does not say. The file is executed, so php-cs-fixer has to be installed; during a migration from it, it is. + * @return array{array>, string, string} + * @throws ConfigurationException + */ + public static function readConfig(string $file): array + { + if (!is_file($file)) { + throw new ConfigurationException("File `$file` does not exist."); + } + + try { + $config = require $file; + } catch (\Error $e) { + throw new ConfigurationException( + (preg_match('~^Class ".+" not found$~', $e->getMessage()) + ? "File `$file` needs the classes of PHP CS Fixer, which this dresscode cannot load; run the `vendor/bin/dresscode` of the project that has PHP CS Fixer installed: " + : "File `$file` cannot be read: ") + . rtrim($e->getMessage(), '.') . '.', + previous: $e, + ); + } + + if (is_array($config)) { + return [$config, ' ', "\n"]; + } + if (!is_object($config) || !method_exists($config, 'getRules')) { + throw new ConfigurationException("File `$file` returns neither rules nor a PHP CS Fixer configuration."); + } + + return [ + $config->getRules(), + method_exists($config, 'getIndent') ? $config->getIndent() : ' ', + method_exists($config, 'getLineEnding') ? $config->getLineEnding() : "\n", + ]; + } + + + /** @return array rule set => preset */ + public static function getSets(): array + { + return [ + '@PER' => 'dresscode/perCs', + '@PER-CS' => 'dresscode/perCs', + '@PER-CS1.0' => 'dresscode/perCs', + '@PER-CS2.0' => 'dresscode/perCs', + '@PER-CS3.0' => 'dresscode/perCs', + '@PSR1' => 'dresscode/psr12', + '@PSR2' => 'dresscode/psr12', + '@PSR12' => 'dresscode/psr12', + '@Symfony' => 'dresscode/symfony', + ]; + } + + + /** + * The sets that move the code to a version of PHP, by the rules and sets each turns on, as PHP CS Fixer 3.95 defines + * them without `clean_namespace`, `no_unset_cast` and `normalize_index_brace`, which fix what PHP 8.0 no longer + * parses; `@PHP84Migration` is the older name of `@PHP8x4Migration`. + * @return array>> + */ + public static function getMigrationSets(): array + { + $afterHeredoc = ['after_heredoc' => true]; + return [ + '@PHP5x4Migration' => ['array_syntax' => true], + '@PHP5x6Migration:risky' => ['pow_to_exponentiation' => true], + '@PHP7x0Migration' => ['@PHP5x4Migration' => true, 'ternary_to_null_coalescing' => true], + '@PHP7x0Migration:risky' => [ + '@PHP5x6Migration:risky' => true, + 'combine_nested_dirname' => true, + 'declare_strict_types' => true, + 'non_printable_character' => true, + 'random_api_migration' => ['replacements' => [ + 'mt_getrandmax' => 'getrandmax', + 'mt_rand' => 'random_int', + 'mt_srand' => 'srand', + 'rand' => 'random_int', + ]], + ], + '@PHP7x1Migration' => ['@PHP7x0Migration' => true, 'list_syntax' => true, 'modifier_keywords' => true], + '@PHP7x1Migration:risky' => ['@PHP7x0Migration:risky' => true, 'void_return' => true], + '@PHP7x3Migration' => [ + '@PHP7x1Migration' => true, + 'heredoc_indentation' => true, + 'method_argument_space' => $afterHeredoc, + 'no_whitespace_before_comma_in_array' => $afterHeredoc, + 'trailing_comma_in_multiline' => $afterHeredoc, + ], + '@PHP7x4Migration' => [ + '@PHP7x3Migration' => true, + 'assign_null_coalescing_to_coalesce_equal' => true, + 'short_scalar_cast' => true, + ], + '@PHP7x4Migration:risky' => [ + '@PHP7x1Migration:risky' => true, + 'implode_call' => true, + 'no_alias_functions' => true, + 'use_arrow_functions' => true, + ], + '@PHP8x0Migration' => ['@PHP7x4Migration' => true], + '@PHP8x0Migration:risky' => [ + '@PHP7x4Migration:risky' => true, + 'get_class_to_class_keyword' => true, + 'modernize_strpos' => true, + 'no_alias_functions' => ['sets' => ['@all']], + 'no_php4_constructor' => true, + 'no_unneeded_final_method' => true, + 'no_unreachable_default_argument_value' => true, + ], + '@PHP8x1Migration' => ['@PHP8x0Migration' => true, 'octal_notation' => true], + '@PHP8x1Migration:risky' => ['@PHP8x0Migration:risky' => true], + '@PHP8x2Migration' => ['@PHP8x1Migration' => true, 'simple_to_complex_string_variable' => true], + '@PHP8x2Migration:risky' => ['@PHP8x1Migration:risky' => true, 'phpdoc_readonly_class_comment_to_keyword' => true], + '@PHP8x3Migration' => ['@PHP8x2Migration' => true], + '@PHP8x3Migration:risky' => ['@PHP8x2Migration:risky' => true], + '@PHP8x4Migration' => [ + '@PHP8x3Migration' => true, + 'new_expression_parentheses' => true, + 'nullable_type_declaration_for_default_null_value' => true, + ], + '@PHP8x4Migration:risky' => ['@PHP8x3Migration:risky' => true], + '@PHP8x5Migration' => ['@PHP8x4Migration' => true, 'switch_case_semicolon_to_colon' => true], + '@PHP8x5Migration:risky' => ['@PHP8x4Migration:risky' => true, 'modern_serialization_methods' => true], + ]; + } +} diff --git a/src/Interop/Translation.php b/src/Interop/Translation.php new file mode 100644 index 000000000..913421702 --- /dev/null +++ b/src/Interop/Translation.php @@ -0,0 +1,505 @@ + 'forbidden', + 'spacing.padding.parameter' => 'forbidden', + 'spacing.padding.closureUse' => 'forbidden', + 'spacing.padding.controlStructure' => 'forbidden', + 'spacing.padding.grouping' => 'forbidden', + ]; + + /** no whitespace inside the brackets of an array and of a destructuring */ + public const NoSpaceInArrays = ['spacing.padding.array' => 'forbidden', 'spacing.padding.list' => 'forbidden']; + + /** how firmly a foreign rule gives the value of a decision */ + private const Kept = 0; + private const Preferred = 1; + private const Set = 2; + + /** @var array path => the value of the decision */ + public private(set) array $decisions = []; + + /** @var array{shape: array, fallback: array, optimizedCalls: bool} */ + public private(set) array $qualification = ['shape' => [], 'fallback' => [], 'optimizedCalls' => false]; + + /** @var list */ + public private(set) array $presets = []; + + /** @var list */ + public private(set) array $warnings = []; + + public private(set) ?int $lineLength = null; + + /** @var array path => how firmly its value is given */ + private array $firmness = []; + + /** @var array paths the foreign rules give values that contradict each other */ + private array $contradicted = []; + + /** @var array paths whose lists are orders, never united */ + private array $ordered = []; + + /** @var array paths whose lists name what is allowed, narrowed to what every foreign rule allows */ + private array $allowances = []; + + + public function __construct( + /** the indent of the configuration object of PHP CS Fixer, which its fixers of the indentation write */ + public readonly string $indent = ' ', + /** the line ending of the configuration object of PHP CS Fixer, which its `line_ending` writes */ + public readonly string $lineEnding = "\n", + ) { + } + + + public function addPreset(string $name): static + { + if (!in_array($name, $this->presets, true)) { + $this->presets[] = $name; + } + + return $this; + } + + + /** The widest line; of two foreign rules naming different ones the narrower is kept, and said so. */ + public function setLineLength(int $length): static + { + if ($this->lineLength !== null && $this->lineLength !== $length) { + $this->warn(sprintf( + 'The foreign rules name the line lengths `%d` and `%d`; DressCode has one and keeps the narrower.', + min($this->lineLength, $length), + max($this->lineLength, $length), + )); + } + + $this->lineLength = min($this->lineLength ?? $length, $length); + return $this; + } + + + /** The value of a decision, merged with what another foreign rule gave it. */ + public function set(string $path, mixed $value): static + { + $this->place($path, $value, self::Set); + return $this; + } + + + /** The value of a decision unless another foreign rule sets it, which then wins in either order. */ + public function prefer(string $path, mixed $value): static + { + $this->place($path, $value, self::Preferred); + return $this; + } + + + /** + * The values of several decisions. + * @param array $values path => value + */ + public function setAll(array $values): static + { + foreach ($values as $path => $value) { + $this->set($path, $value); + } + + return $this; + } + + + /** + * The value for every place of the kind `blocks` of the group, which a translation writes out place by place. + * @return array path => the value + */ + public static function expandBlocks(string $group, mixed $value): array + { + return array_fill_keys(array_map(fn(string $place) => "$group.$place", Catalogue::PlaceKinds['blocks']), $value); + } + + + /** + * The blank lines of a place, a count or the range from `$min` to `$max`; a count another foreign rule gave the place + * widens it to the range of both, and the translation says so. + */ + public function setBlankLines(string $path, int $min, int $max): static + { + return $this->set($path, $min === $max ? $min : [$min, $max]); + } + + + /** + * An order whose sequence carries meaning; another foreign rule setting a different one contradicts it instead of + * uniting with it. + * @param list $order + */ + public function setOrder(string $path, array $order): static + { + $this->ordered[$path] = true; + return $this->set($path, $order); + } + + + /** + * A list of what is allowed; another foreign rule allowing a different one narrows it to what both allow instead of + * uniting with it. + * @param list $allowed + */ + public function setAllowed(string $path, array $allowed): static + { + $this->allowances[$path] = true; + return $this->set($path, $allowed); + } + + + /** The value of a decision as it is given, whatever the foreign rules gave it before. */ + public function replace(string $path, mixed $value): static + { + $this->decisions[$path] = $value; + $this->firmness[$path] = self::Set; + unset($this->contradicted[$path]); + return $this; + } + + + /** The decision left out of the translation, whatever the foreign rules gave it. */ + public function remove(string $path): static + { + unset($this->decisions[$path], $this->firmness[$path], $this->contradicted[$path]); + return $this; + } + + + /** + * The decisions a foreign rule turned off stands for, or those a foreign rule governs while allowing either form, `keep` + * unless another foreign rule sets them, which then wins in either order. + */ + public function keep(string ...$paths): static + { + foreach ($paths as $path) { + $this->place($path, 'keep', self::Kept); + } + + return $this; + } + + + /** A value merges with one given as firmly, replaces one given less firmly and gives way to one given more. */ + private function place(string $path, mixed $value, int $firmness): void + { + $firmness = $value === 'keep' ? self::Kept : $firmness; + $current = $this->firmness[$path] ?? -1; + if ($firmness > $current) { + $this->firmness[$path] = $firmness; + unset($this->contradicted[$path]); + $this->decisions[$path] = $value; + + } elseif ($firmness === $current && !isset($this->contradicted[$path])) { + $merged = $this->merge($path, $this->decisions[$path] ?? null, $value); + if (isset($this->contradicted[$path])) { + unset($this->decisions[$path]); + } else { + $this->decisions[$path] = $merged; + } + } + } + + + /** The value two foreign rules give a decision together; values that contradict each other mark it and are said so. */ + private function merge(string $path, mixed $old, mixed $value): mixed + { + $isCount = fn(mixed $v) => is_int($v) || (is_array($v) && array_is_list($v) && count($v) === 2 && is_int($v[0])); + if ($old === $value) { + return $old; + + } elseif (str_starts_with($path, 'blankLines.') && $isCount($old) && $isCount($value)) { + $this->warn("The foreign rules count the blank lines of `$path` differently; DressCode has one count there and takes the range of them all."); + [$oldMin, $oldMax] = is_int($old) ? [$old, $old] : $old; + [$min, $max] = is_int($value) ? [$value, $value] : $value; + $range = [min($min, $oldMin), $max === null || $oldMax === null ? null : max($max, $oldMax)]; + return $range[0] === $range[1] ? $range[0] : $range; + + } elseif (is_array($old) && is_array($value) && array_is_list($old) && array_is_list($value) && isset($this->allowances[$path])) { + return array_values(array_intersect($old, $value)); + + } elseif (is_array($old) && is_array($value) && array_is_list($old) && array_is_list($value) && !isset($this->ordered[$path])) { + return array_values(array_unique([...$old, ...$value], SORT_REGULAR)); + + } elseif (is_array($old) && is_array($value)) { + foreach ($value as $key => $item) { + $old[$key] = array_key_exists($key, $old) ? $this->merge($path, $old[$key], $item) : $item; + } + + return $old; + + } elseif (is_bool($old) && is_bool($value)) { + return $old || $value; + } + + $this->contradicted[$path] = true; + $this->warn("The foreign rules give `$path` values that contradict each other; DressCode leaves it out, for the configuration to set by hand."); + return $old; + } + + + /** + * The form a qualified name is written in, `imported`, `backslashed` or `keep`, by the kind of name: `class`, + * `function` and `constant` of every name, `globalClass`, `globalFunction` and `globalConstant` of a global one, + * `optimizedFunction` and `optimizedConstant` of one the compiler works with. + * @param array $shapes + */ + public function setQualificationShape(array $shapes): static + { + $this->qualification['shape'] = $shapes + $this->qualification['shape']; + return $this; + } + + + /** + * Whether a global function or constant stands `qualified`, `bare` or as written (`keep`): `function` and `constant` + * of every one, `optimizedFunction` of a call the compiler optimizes and `optimizedConstant` of a constant it + * computes with. + * @param array $fallbacks + */ + public function setQualificationFallback(array $fallbacks): static + { + $this->qualification['fallback'] = $fallbacks + $this->qualification['fallback']; + return $this; + } + + + /** A call the compiler optimizes is written in the form the optimization takes, its arguments included. */ + public function markCallsOptimized(): static + { + $this->qualification['optimizedCalls'] = true; + return $this; + } + + + public function warn(string $message): static + { + if (!in_array($message, $this->warnings, true)) { + $this->warnings[] = $message; + } + + return $this; + } + + + /** The configuration as the source of a dresscode.php. */ + public function toPhp(): string + { + $arguments = ''; + if ($this->presets) { + $arguments .= "\tuse: " . self::export(array_map(PluginRegistry::abbreviate(...), $this->presets)) . ",\n"; + } + + $decisions = $this->buildTree(); + if ($decisions) { + $arguments .= "\tdecisions: " . self::export($decisions, 1) . ",\n"; + } + + return "presets ? ['use' => array_map(PluginRegistry::abbreviate(...), $this->presets)] : []) + $this->buildTree(); + return $config === [] + ? "use: []\n" + : Neon::encode( + $config, + fn(mixed $value) => is_array($value) && (!array_is_list($value) || array_any($value, fn(mixed $item) => is_array($item))), + ) + . "\n"; + } + + + /** + * The paths of the decisions the translation sets, those of the qualification included. + * @return list + */ + public function getPaths(): array + { + return array_keys($this->collectDecisions()); + } + + + /** + * The tree of the decisions. + * @return array + */ + private function buildTree(): array + { + $tree = []; + foreach ($this->collectDecisions() as $path => $value) { + $tree = Helpers::placeValue($tree, $path, $value); + } + + return $tree; + } + + + /** + * The decisions, those the two axes of the qualification come to among them, what they leave as it is left to the presets. + * @return array path => value + */ + private function collectDecisions(): array + { + $decisions = $this->decisions; + if ($this->qualification !== ['shape' => [], 'fallback' => [], 'optimizedCalls' => false]) { + $defaults = self::translateQualification([], [], optimizedCalls: false); + foreach (self::translateQualification(...array_values($this->qualification)) as $path => $value) { + if ($defaults[$path] !== $value) { + $decisions[$path] = $value; + } + } + } + + if ($this->lineLength !== null) { + $decisions['file.lineLength'] = $this->lineLength; + } + + // a range is kept as its two ends and a shape as its word while the foreign rules meet, and both are written as + // the configuration writes them + static $catalogue; + $catalogue ??= new ConfigResolver(new PluginRegistry)->getCatalogue(); + foreach ($decisions as $path => $value) { + $domain = $catalogue->find($path)?->domain; + if (str_starts_with($path, 'blankLines.')) { + $decisions[$path] = self::writeRanges($value); + } elseif ($domain instanceof Shapes && is_string($value) && isset($domain->shapes[$value])) { + $decisions[$path] = $domain->getExample($value); + } elseif ($domain instanceof Shapes && is_array($value) && array_all($value, fn($word) => isset($domain->shapes[$word]))) { + $decisions[$path] = array_map($domain->getExample(...), $value); + } + } + + return $decisions; + } + + + /** The count as the configuration writes it, `0-1` or `1+` for a range, and so each count of a map. */ + private static function writeRanges(mixed $value): mixed + { + return match (true) { + is_array($value) && array_keys($value) === [0, 1] && is_int($value[0]) => $value[0] . ($value[1] === null ? '+' : "-$value[1]"), + is_array($value) => array_map(self::writeRanges(...), $value), + default => $value, + }; + } + + + /** + * The decisions of the global and the other names the two axes come to: the form of a qualified name, and whether + * a global function or constant stands qualified or bare, which the compiler may decide apart. + * @param array $shape + * @param array $fallback + * @return array path => value + */ + private static function translateQualification(array $shape, array $fallback, bool $optimizedCalls): array + { + $values = [ + 'qualification.class.otherNamespace' => $shape['class'] ?? 'keep', + 'qualification.class.global' => $shape['globalClass'] ?? $shape['class'] ?? 'keep', + 'qualification.function.otherNamespace' => $shape['function'] ?? 'keep', + 'qualification.constant.otherNamespace' => $shape['constant'] ?? 'keep', + ]; + // a name of the namespace of the file is shortened to the bare name, the import of it being no form there + foreach (['class', 'function', 'constant'] as $kind) { + $values["qualification.$kind.currentNamespace"] = match ($shape[$kind] ?? null) { + 'imported' => 'bare', + 'backslashed' => 'backslashed', + default => 'keep', + }; + } + + foreach (['Function', 'Constant'] as $kind) { + $form = $shape['global' . $kind] ?? $shape[lcfirst($kind)] ?? null; + $global = self::combine($fallback[lcfirst($kind)] ?? null, $form); + $optimized = match ($fallback['optimized' . $kind] ?? null) { + 'bare' => 'bare', + 'qualified' => self::combine('qualified', $shape['optimized' . $kind] ?? $form), + default => 'keep', + }; + // the arguments of an optimized call written positionally are an intent of their own, the call then qualified + if ($optimized === 'keep' && $global === 'keep' && $kind === 'Function' && $optimizedCalls) { + $optimized = 'imported'; + } + + $values['qualification.' . lcfirst($kind) . '.global'] = $global; + // the optimized ones the foreign rules do not tell apart follow every global one + $values['qualification.' . lcfirst($kind) . '.optimized'] = $optimized === 'keep' ? $global : $optimized; + } + + return $values; + } + + + /** + * The forms a global name stands in, from whether it stands qualified or bare and the form of a qualified one; a + * bare one takes the backslash away and leaves an import. + * @return string|list + */ + private static function combine(?string $fallback, ?string $shape): string|array + { + $qualified = $shape === 'keep' ? null : $shape; + return match ($fallback) { + 'bare' => $qualified === 'imported' ? ['bare', 'imported'] : 'bare', + 'qualified' => $qualified ?? ['imported', 'backslashed'], + default => $qualified === null ? 'keep' : [$qualified, 'bare'], + }; + } + + + /** The value as PHP writes it: a list of plain values on one line, a map a key per line indented under the level. */ + private static function export(mixed $value, int $level = 0): string + { + if ($value === null) { + return 'null'; + } elseif (!is_array($value)) { + return var_export($value, return: true); + } elseif (array_is_list($value) && !array_any($value, fn($item) => is_array($item))) { + return '[' . implode(', ', array_map(fn($item) => self::export($item), $value)) . ']'; + } + + $indent = str_repeat("\t", $level + 1); + $list = array_is_list($value); + $items = ''; + foreach ($value as $key => $item) { + $items .= $indent . ($list ? '' : var_export($key, return: true) . ' => ') . self::export($item, $level + 1) . ",\n"; + } + + return "[\n$items" . str_repeat("\t", $level) . ']'; + } +} diff --git a/src/Interop/Translator.php b/src/Interop/Translator.php new file mode 100644 index 000000000..400f34980 --- /dev/null +++ b/src/Interop/Translator.php @@ -0,0 +1,310 @@ +|\Closure(array, Translation): mixed> foreign rule => the + * decisions it stands for, or a closure setting them from the foreign options + */ + private readonly array $translations; + + /** @var array foreign rule set => preset */ + private readonly array $sets; + + /** @var array> foreign rule => the decisions it stands for */ + private array $paths = []; + + /** @var ?array> decision => the foreign rules standing for it */ + private ?array $foreignNames = null; + + private ?Catalogue $catalogue = null; + + + /** + * Without tables, those of PHP CS Fixer and PHP_CodeSniffer. + * @param ?array|\Closure(array, Translation): mixed> $translations + * @param ?array $sets + */ + public function __construct(?array $translations = null, ?array $sets = null) + { + $this->translations = $translations ?? PhpCsFixer::getTranslations() + PhpCodeSniffer::getTranslations(); + $this->sets = $sets ?? PhpCsFixer::getSets() + PhpCodeSniffer::getSets(); + } + + + /** + * @param array> $rules foreign rule => whether it is on, or its options + * @param string $indent of the configuration object of PHP CS Fixer + * @param string $lineEnding of the configuration object of PHP CS Fixer + */ + public function translate(array $rules, string $indent = ' ', string $lineEnding = "\n"): Translation + { + $translation = new Translation($indent, $lineEnding); + $rules = self::expandMigrationSets($rules); + foreach ($rules as $name => $options) { + if ($options === false) { + $this->translateDisabled($name, $translation, $rules); + continue; + } + + $options = is_array($options) ? $options : []; + $target = $this->translations[$name] ?? null; + if (isset($this->sets[$name])) { + $translation->addPreset($this->sets[$name]); + } elseif ($target === null) { + $translation->warn($this->explainUncovered($name)); + } elseif (is_array($target)) { + $translation->setAll($target); + if ($options !== []) { + $translation->warn("The options of `$name` were not translated; set them by hand, `dresscode explain " . array_key_first($target) . '` describes them.'); + } + } else { + $target($options, $translation); + } + } + + // the sets of PER Coding Style in PHP CS Fixer leave `(new Foo())->bar()` alone, which the preset rewrites from PHP 8.4 + if (in_array('dresscode/perCs', $translation->presets, true) && !isset($rules['new_expression_parentheses'])) { + $translation->prefer('classes.instantiationWrapping', 'keep'); + } + + $this->fillCommaPlaces( + $translation, + isset($rules['trailing_comma_in_multiline']) && $rules['trailing_comma_in_multiline'] !== false, + ); + $this->leaveOutTyped($translation); + return $translation; + } + + + /** + * The rules a set moving the code to a version of PHP turns on, in its place, a later rule replacing an earlier one + * as PHP CS Fixer resolves them; a set turned off turns its rules off. + * @param array> $rules + * @return array> + */ + private static function expandMigrationSets(array $rules): array + { + $sets = PhpCsFixer::getMigrationSets(); + $expanded = []; + foreach ($rules as $name => $options) { + $set = $sets[preg_replace('~^@PHP(\d)(\d)Migration~', '@PHP$1x$2Migration', $name)] ?? null; + if ($set === null) { + $expanded[$name] = $options; + } else { + $members = self::expandMigrationSets($set); + $expanded = [...$expanded, ...$options === false ? array_map(fn() => false, $members) : $members]; + } + } + + return $expanded; + } + + + /** + * Every place of `multiline.trailingComma` stands in the translation, starting from what a preset of the + * translation gives it, or `keep` where none decides it, not from the defaults of the rule. A + * `trailing_comma_in_multiline` of PHP CS Fixer replaces what its set says of the comma of a list spread over lines, + * so a place it does not name keeps of the preset only the removal on one line, and without it a list the set + * requires the comma of stays required whatever `no_trailing_comma_in_singleline` says. + */ + private function fillCommaPlaces(Translation $translation, bool $multilineReplaced): void + { + $places = ['array', 'argument', 'parameter', 'matchArm', 'closureUse', 'import', 'list']; + if (!array_any($places, fn(string $place) => isset($translation->decisions["multiline.trailingComma.$place"]))) { + return; + } + + $preset = $translation->presets === [] + ? [] + : new ConfigResolver(new PluginRegistry($this)) + ->resolve(new Config(use: $translation->presets), Config::DefaultPhpVersion) + ->decisions; + foreach ($places as $place) { + $path = "multiline.trailingComma.$place"; + $value = ($preset[$path] ?? null)?->value; + $given = $value === null || $value->isKept() ? 'keep' : $value->getWord(); + $strict = $given === 'required' || $given === 'forbidden'; + $translation->replace($path, match (true) { + !isset($translation->decisions[$path]) => $multilineReplaced && $strict ? 'optional' : $given, + $translation->decisions[$path] === 'optional' && !$multilineReplaced && $strict => $given, + default => $translation->decisions[$path], + }); + } + } + + + /** + * A decision of a rule that needs the types of the code is left out and said so: the foreign configuration does not + * tell whether the project has PHPStan for `typeAnalysis`, without which the configuration setting it is refused. + */ + private function leaveOutTyped(Translation $translation): void + { + foreach ($translation->decisions as $path => $value) { + $rules = $this->getCatalogue()->getRulesOf($path); + if ($value !== 'keep' && $rules !== [] && array_all($rules, fn(string $rule) => RuleInfo::fromRule($rule)->typesRequired)) { + $translation->remove($path) + ->warn("`$path` needs the types of the code, so it is left out; set it together with `typeAnalysis: phpstan` where the project has PHPStan."); + } + } + } + + + /** + * A foreign rule switched off makes the requirements it stands for `keep` where no other rule of its tool that the + * configuration may run stands for them; a requirement another one stands for too is left as it is, and so is one an + * exclusion of a single message of its sniff would narrow, and the translation says so. A sniff and a message of it + * standing for the same requirement count as one. + * @param array> $rules + */ + private function translateDisabled(string $name, Translation $translation, array $rules): void + { + $paths = $this->findPaths($name); + $sniff = $name; + while ($paths === [] && substr_count($sniff, '.') > 2) { + $sniff = implode('.', array_slice(explode('.', $sniff), 0, -1)); + if ($narrowed = $this->findPaths($sniff)) { + $translation->warn("`$name` is excluded, but `" . implode('`, `', $narrowed) . '` cannot leave out that check alone, so ' + . (count($narrowed) > 1 ? 'they stay' : 'it stays') . ' as the rest of the configuration sets it.'); + return; + } + } + + $staying = $others = []; + foreach ($paths as $path) { + if ($this->getCatalogue()->find($path)?->isRequirement() !== true) { + continue; + } + + $covering = array_filter( + $this->findForeignNames([$path]), + fn(string $other) => $other !== $name + && !str_starts_with($name, "$other.") + && !str_starts_with($other, "$name.") + && str_contains($other, '.') === str_contains($name, '.') + && $this->canRun($other, $rules), + ); + if ($covering) { + $staying[] = $path; + $others = [...$others, ...$covering]; + } else { + $translation->keep($path); + } + } + + if ($staying) { + $list = implode('`, `', $staying); + $plural = count($staying) > 1; + $translation->warn("`$name` is turned off, but `$list` also " . ($plural ? 'stand' : 'stands') + . ' for `' . implode('`, `', array_unique($others)) . '`, so ' . ($plural ? 'they stay' : 'it stays') . '; set ' . ($plural ? 'them' : 'it') . ' to `keep` if none of them applies.'); + } + } + + + /** + * Whether the configuration may run a foreign rule: one it names is on unless turned off, and one it does not name + * may come from a set or a standard of its tool, whose members the translation does not know. + * @param array> $rules + */ + private function canRun(string $name, array $rules): bool + { + if (isset($rules[$name])) { + return $rules[$name] !== false; + } + + $fixer = !str_contains($name, '.'); + return array_any( + array_keys($rules), + fn(string $rule) => $rules[$rule] !== false + && ($fixer ? str_starts_with($rule, '@') : (bool) preg_match('~^[A-Z]\w*$~', $rule)), + ); + } + + + /** Why no rule stands for a foreign name: a rule set or a standard has no preset, a message of a sniff is not the sniff. */ + private function explainUncovered(string $name): string + { + if (str_starts_with($name, '@')) { + $start = str_starts_with($name, '@PhpCsFixer') ? 'the preset `symfony`, the nearest one' : 'the preset `perCs` or `psr12`'; + return "The rule set `$name` has no DressCode preset; start from $start."; + + } elseif (preg_match('~^[A-Z]\w*$~', $name)) { + return "The standard `$name` has no DressCode preset; start from the preset `perCs` or `psr12`."; + + } elseif (substr_count($name, '.') === 3 && $this->findPaths($sniff = substr($name, 0, (int) strrpos($name, '.')))) { + return "`$name` is one message of `$sniff`, which DressCode cannot enable alone."; + } + + return "No DressCode rule covers `$name`."; + } + + + /** + * The decisions a foreign name stands for; empty when none does. + * @return list + */ + public function findPaths(string $name): array + { + if (isset($this->paths[$name])) { + return $this->paths[$name]; + } + + $value = $this->translations[$name] ?? null; + if (is_array($value)) { + return $this->paths[$name] = array_keys($value); + } elseif ($value === null) { + return $this->paths[$name] = []; + } + + $value([], $translation = new Translation); + return $this->paths[$name] = $translation->getPaths(); + } + + + /** + * Names of other tools standing for any of the decisions, for the reference and the listing of the rules. + * @param list $paths + * @return list + */ + public function findForeignNames(array $paths): array + { + if ($this->foreignNames === null) { + $map = []; + foreach (array_keys($this->translations) as $name) { + foreach ($this->findPaths($name) as $path) { + $map[$path][] = $name; + } + } + + $this->foreignNames = $map; + } + + $names = array_flip(array_merge(...array_map(fn(string $path) => $this->foreignNames[$path] ?? [], $paths))); + return array_values(array_filter(array_keys($this->translations), fn(string $name) => isset($names[$name]))); + } + + + private function getCatalogue(): Catalogue + { + $core = (new CorePlugin)->getManifest(); + return $this->catalogue ??= new Catalogue(array_keys($core->rules), coreDecisions: $core->decisions); + } +} diff --git a/src/Profile.php b/src/Profile.php index 093fb1b36..fd0ab79f6 100644 --- a/src/Profile.php +++ b/src/Profile.php @@ -39,9 +39,9 @@ public ?string $nameResolution; /** - * a comment the project already writes to say a line is meant as it is: pattern => the rules or decisions it - * silences where it stands, as `dresscode:ignore` does, and the example of such a comment, `` standing for - * the reason (`// @ `) + * a comment the project already writes to say a line is meant as it is: pattern => the rules, decisions or names + * of other tools it silences where it stands, as `dresscode:ignore` does, and the example of such a comment, + * `` standing for the reason (`// @ `) * @var array, example: ?string}> */ public array $suppressionComments; diff --git a/tests/DressCode/Config/ConfigResolver.phpt b/tests/DressCode/Config/ConfigResolver.phpt index 26a1dc783..368631933 100644 --- a/tests/DressCode/Config/ConfigResolver.phpt +++ b/tests/DressCode/Config/ConfigResolver.phpt @@ -613,6 +613,19 @@ test('a severity holds for a value said by its layer or one below it, the most s }); +test('an unknown key is looked up in the tables of the translator of the run, as an unknown name is', function () { + $resolver = new ConfigResolver(new PluginRegistry(new DressCode\Interop\Translator( + ['acme_spacing' => ['spacing.call' => 'compact']], + [], + ))); + Assert::exception( + fn() => $resolver->resolve(new Config(decisions: ['acme_spacing' => true]), '8.3'), + ConfigurationException::class, + 'Key `acme_spacing` is unknown. It is the name of a rule of another tool, covered by `spacing.call`;%a%', + ); +}); + + test('a rule an override turns on runs somewhere, however the override decides it', function () { // a word on a structure decides every decision under it $config = new Config( diff --git a/tests/DressCode/Config/DecisionResolver.phpt b/tests/DressCode/Config/DecisionResolver.phpt index b19a73c05..b3f56d443 100644 --- a/tests/DressCode/Config/DecisionResolver.phpt +++ b/tests/DressCode/Config/DecisionResolver.phpt @@ -125,6 +125,11 @@ test('a path the catalogue does not know is named with the nearest known one', f ConfigurationException::class, 'Key `spacng` is unknown; write `spacing`.', ); + Assert::exception( + fn() => $resolver->resolve([[new Layer(LayerKind::Configuration), ['native_function_invocation' => true]]]), + ConfigurationException::class, + 'Key `native_function_invocation` is unknown. It is the name of a rule of another tool, covered by %a%; `dresscode import` translates a configuration of another tool.', + ); Assert::exception( fn() => $resolver->resolve([[new Layer(LayerKind::Configuration), ['qualification' => 'bare']]]), ConfigurationException::class, diff --git a/tests/DressCode/Config/PluginRegistry.phpt b/tests/DressCode/Config/PluginRegistry.phpt index 507310837..9f4d0f2ad 100644 --- a/tests/DressCode/Config/PluginRegistry.phpt +++ b/tests/DressCode/Config/PluginRegistry.phpt @@ -106,6 +106,11 @@ test('names of a suppression comment: a decision or a section for itself, a rule Assert::same(['imports.order.names'], $registry->expandSuppressedName('imports.order.names')); Assert::same(['imports'], $registry->expandSuppressedName('imports')); Assert::same([], $registry->expandSuppressedName('importOrder')); + Assert::same(['imports.order.names', 'imports.order.kinds'], $registry->expandSuppressedName('ordered_imports')); + Assert::same( + ['imports.order.names', 'imports.order.kinds'], + $registry->expandSuppressedName('SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses'), + ); Assert::same([], $registry->expandSuppressedName('test/unknown')); Assert::same([], $registry->expandSuppressedName('imports.ord')); }); @@ -136,6 +141,10 @@ test('errors', function () { ConfigurationException::class, 'Unknown preset `dresscode/nete`. Did you mean `dresscode/nette`?', ); + Assert::same( + 'Unknown decision, preset or rule `cast_spaces`. It is covered by `spacing.cast`; `dresscode import` translates a configuration of another tool.', + $registry->createUnknownNameException('cast_spaces')->getMessage(), + ); Assert::exception( fn() => RuleInfo::fromRule(NoInfo::class), ConfigurationException::class, diff --git a/tests/DressCode/Console/Application.phpt b/tests/DressCode/Console/Application.phpt index 0eeee0626..30024b6c7 100644 --- a/tests/DressCode/Console/Application.phpt +++ b/tests/DressCode/Console/Application.phpt @@ -627,6 +627,98 @@ test('catalogue lists every decision, those the configuration makes marked, and $data = json_decode($out, associative: true); Assert::same(DressCode\Config\Catalogue::Version, $data['version']); Assert::same('foo($a, $b)', $data['decisions']['spacing.call']['standards']['nette']); + Assert::contains('no_spaces_after_function_name', $data['decisions']['spacing.call']['covers']); +}); + + +test('import translates a foreign configuration and says what it could not', function () use ($root) { + file_put_contents("$root/phpcs.xml", <<<'XX' + + + + + + + + + + + + + + + XX); + [$code, $out, $err] = runApp($root, ['import', "$root/phpcs.xml"]); + Assert::same(0, $code); + Assert::same( + " [\n" + . "\t\t\t'trailingComma' => [\n" + . "\t\t\t\t'array' => 'required',\n" + . "\t\t\t\t'argument' => 'required',\n" + . "\t\t\t\t'parameter' => 'keep',\n" + . "\t\t\t\t'matchArm' => 'keep',\n" + . "\t\t\t\t'closureUse' => 'keep',\n" + . "\t\t\t\t'import' => 'keep',\n" + . "\t\t\t\t'list' => 'keep',\n" + . "\t\t\t],\n" + . "\t\t],\n" + . "\t\t'blankLines' => [\n" + . "\t\t\t'aroundDeclarations' => 1,\n" + . "\t\t\t'aroundMethods' => 1,\n" + . "\t\t\t'aroundInterfaceMethods' => 1,\n" + . "\t\t\t'bodyStart' => [\n" + . "\t\t\t\t'class' => 0,\n" + . "\t\t\t],\n" + . "\t\t\t'bodyEnd' => [\n" + . "\t\t\t\t'class' => 0,\n" + . "\t\t\t],\n" + . "\t\t],\n" + . "\t],\n" + . ");\n", + $out, + ); + Assert::same( + "\nRead 5 rules; set 12 decisions and 1 preset.\n" + . " `Squiz.WhiteSpace.FunctionSpacing` leaves the blank lines around classes alone, while `blankLines` sets them together with those around functions.\n" + . " No DressCode rule covers `Squiz.Nonsense.DoesNotExist`.\n", + $err, + ); + + file_put_contents( + "$root/fixer.php", + " ['space' => 'none']];\n\t}\n};\n", + ); + [$code, $out] = runApp($root, ['import', 'fixer.php']); // relative to the working directory of the run + Assert::same(0, $code); + Assert::contains("\t\t'spacing' => [\n\t\t\t'cast' => '(int)\$x',\n\t\t],\n", $out); + + [$code, , $err] = runApp($root, ['import']); + Assert::same(3, $code); + Assert::match('Error: Missing required argument ``.%A%', $err); + + // a file named is written in the format of its extension, as a configuration the run reads + $dir = createTempDir('import-neon'); + [$code, $out, $err] = runApp($root, ['import', "$root/phpcs.xml", "$dir/dresscode.neon"]); + Assert::same([0, ''], [$code, $out]); + Assert::contains("Written `$dir/dresscode.neon`.", str_replace('\\', '/', $err)); + $neon = (string) file_get_contents("$dir/dresscode.neon"); + Assert::match("use: [psr12]\nmultiline:\n\ttrailingComma:\n\t\tarray: required\n%A%", $neon); + Assert::same( + 'required', + DressCode\Config\NeonReader::read("$dir/dresscode.neon")->decisions['multiline']['trailingComma']['array'] ?? null, + ); + + [$code, , $err] = runApp($root, ['import', "$root/phpcs.xml", "$dir/dresscode.neon"]); + Assert::same(3, $code); + Assert::contains('exists already', $err); + [$code, , $err] = runApp($root, ['import', "$root/phpcs.xml", "$dir/dresscode.yaml"]); + Assert::same(3, $code); + Assert::contains('its extension is `.neon` or `.php`', $err); }); diff --git a/tests/DressCode/Engine/Suppression.phpt b/tests/DressCode/Engine/Suppression.phpt index c593ae0cc..c21ea1974 100644 --- a/tests/DressCode/Engine/Suppression.phpt +++ b/tests/DressCode/Engine/Suppression.phpt @@ -7,7 +7,8 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; -$resolve = fn(string $name) => str_starts_with($name, 'dresscode/') ? [$name] : []; +$foreign = ['Generic.Files.LineLength' => ['dresscode/lineLength']]; +$resolve = fn(string $name) => $foreign[$name] ?? (str_starts_with($name, 'dresscode/') ? [$name] : []); /** @param Closure(string): list $resolve */ @@ -191,15 +192,98 @@ test('disable and enable, also without a matching enable', function () use ($res }); -test('ignoreFile silences the whole file', function () use ($resolve) { +test('ignoreFile and phpcs forms with alias translation', function () use ($resolve) { $s = suppression("isSilenced('dresscode/x', 3)); + + $s = suppression(<<<'XX' + isSilenced('dresscode/lineLength', 2)); + Assert::true($s->isSilenced('dresscode/lineLength', 4)); + Assert::false($s->isSilenced('dresscode/lineLength', 5)); + Assert::true($s->isSilenced('dresscode/lineLength', 10)); + Assert::false($s->isSilenced('dresscode/lineLength', 12)); + + $s = suppression("isSilenced('dresscode/x', 3)); }); test('the hyphenated ignore-file is no directive', function () use ($resolve) { - $s = suppression("isSilenced('dresscode/x', 3)); + foreach (['dresscode', 'phpcs'] as $prefix) { + $s = suppression("isSilenced('dresscode/x', 3)); + } +}); + + +test('@phpcsSuppress with our names, several in one tag', function () use ($resolve) { + $s = suppression(<<<'XX' + isSilenced('dresscode/a', 7)); + Assert::true($s->isSilenced('dresscode/b', 7)); + Assert::true($s->isSilenced('dresscode/c', 7)); + Assert::false($s->isSilenced('dresscode/d', 7)); +}); + + +test('@phpcsSuppress on the first item of a list covers that item, not the list', function () use ($resolve) { + $s = suppression(<<<'XX' + isSilenced('dresscode/a', 4)); + Assert::false($s->isSilenced('dresscode/a', 5)); + Assert::true($s->isSilenced('dresscode/b', 9)); + Assert::false($s->isSilenced('dresscode/b', 10)); + Assert::true($s->isSilenced('dresscode/c', 14)); + Assert::false($s->isSilenced('dresscode/c', 12)); +}); + + +test('@phpcsSuppress after a parameter covers the parameter', function () use ($resolve) { + $s = suppression(<<<'XX' + isSilenced('dresscode/a', 3)); + Assert::false($s->isSilenced('dresscode/a', 4)); }); diff --git a/tests/DressCode/Interop/Translator.phpt b/tests/DressCode/Interop/Translator.phpt new file mode 100644 index 000000000..8c5aab4e5 --- /dev/null +++ b/tests/DressCode/Interop/Translator.phpt @@ -0,0 +1,1296 @@ +|Closure(array, Translation): mixed> */ +function allTranslations(): array +{ + return PhpCsFixer::getTranslations() + PhpCodeSniffer::getTranslations(); +} + + +/** Resolves the configuration the translation prints, which throws on a decision or a value the catalogue does not accept. */ +function resolveTranslation(Translation $translation): DressCode\Config\ResolvedConfig +{ + $file = createTempDir('translator') . '/dresscode.php'; + file_put_contents($file, $translation->toPhp()); + return new ConfigResolver(new PluginRegistry)->resolve(Loader::loadFile($file), DressCode\Config::DefaultPhpVersion); +} + + +/** + * @param array $places place => value + * @return array path => value + */ +function commaPlaces(array $places): array +{ + $paths = []; + foreach ($places as $place => $value) { + $paths["multiline.trailingComma.$place"] = $value; + } + + return $paths; +} + + +test('a translation names a decision that exists and values it accepts', function () { + $catalogue = (new PluginRegistry)->getCatalogue(); + $translator = new Translator; + foreach (array_keys(allTranslations()) as $foreign) { + $paths = $translator->findPaths($foreign); + Assert::true( + $paths !== [] || $translator->translate([$foreign => true])->warnings !== [], + "$foreign translates to nothing and says nothing", + ); + foreach ($paths as $path) { + Assert::notNull($catalogue->find($path), "$foreign names the unknown decision $path"); + } + + Assert::noError(fn() => resolveTranslation($translator->translate([$foreign => true]))); + } +}); + + +test('the two name spaces do not overlap', function () { + Assert::same([], array_intersect_key(PhpCsFixer::getTranslations(), PhpCodeSniffer::getTranslations())); + Assert::same([], array_intersect_key(PhpCsFixer::getSets(), PhpCodeSniffer::getSets())); + foreach (array_keys(PhpCsFixer::getTranslations()) as $fixer) { + Assert::false(str_contains($fixer, '.'), "$fixer is not a fixer name"); + } + + foreach (array_keys(PhpCodeSniffer::getTranslations()) as $sniff) { + Assert::true((bool) preg_match('~^\w+(\.\w+)+$~D', $sniff), "$sniff is not a sniff code"); + } +}); + + +test('a set becomes a preset, an unknown rule a warning', function () { + $translation = (new Translator)->translate([ + '@PSR12' => true, 'PSR12' => true, '@Symfony' => true, '@PhpCsFixer' => true, 'no_such_fixer' => true, + 'Squiz' => true, 'Generic.Files.LineLength.TooLong' => true, 'Acme.Cat.Name.Code' => true, + ]); + Assert::same(['dresscode/psr12', 'dresscode/symfony'], $translation->presets); + Assert::same([], $translation->decisions); + Assert::same([ + 'The rule set `@PhpCsFixer` has no DressCode preset; start from the preset `symfony`, the nearest one.', + 'No DressCode rule covers `no_such_fixer`.', + 'The standard `Squiz` has no DressCode preset; start from the preset `perCs` or `psr12`.', + '`Generic.Files.LineLength.TooLong` is one message of `Generic.Files.LineLength`, which DressCode cannot enable alone.', + 'No DressCode rule covers `Acme.Cat.Name.Code`.', + ], $translation->warnings); +}); + + +test('a set moving the code to a version of PHP stands for its rules, which a later rule overrides', function () { + $translation = (new Translator)->translate(['@PHP84Migration' => true, 'array_syntax' => false]); + Assert::same([], $translation->presets); + Assert::same('keep', $translation->decisions['literals.arraySyntax.array']); + Assert::same('short', $translation->decisions['literals.arraySyntax.list']); + Assert::same('forbidden', $translation->decisions['classes.instantiationWrapping']); + Assert::same('forbidden', $translation->decisions['upgrading.php.implicitNullable']); + // what PHP 8.0 no longer parses has nothing to translate + Assert::false(array_any($translation->warnings, fn(string $warning) => str_contains($warning, 'normalize_index_brace'))); + + // a set turned off turns its rules off + Assert::same('keep', (new Translator)->translate(['@PHP8x4Migration' => false])->decisions['classes.instantiationWrapping']); +}); + + +test('the tags of final_internal_class are annotations, and the translation says they may be attributes', function () { + $translation = (new Translator)->translate(['final_internal_class' => ['include' => ['internal', '@Custom'], 'exclude' => ['Entity']]]); + Assert::same(['@internal', '@Custom'], $translation->decisions['classes.internalClass.markers']); + Assert::same(['@Entity'], $translation->decisions['classes.internalClass.except']); + Assert::contains('reads `internal`, `@Custom`, `Entity` as attributes too', implode("\n", $translation->warnings)); + + // an empty include makes every class final, which the markers cannot say + $translation = (new Translator)->translate( + ['final_internal_class' => ['include' => [], 'consider_absent_docblock_as_internal_class' => true]], + ); + Assert::false(isset($translation->decisions['classes.internalClass.markers'])); + Assert::same([ + '`final_internal_class` with an empty `include` makes every class final; `classes.internalClass.markers` cannot be empty, so it stays `@internal`.', + '`final_internal_class` makes a class without a doc comment final too, which DressCode does not.', + ], $translation->warnings); +}); + + +test('ordered_class_elements takes the kinds of any visibility, and stands an abstract or readonly one for its visibility', function () { + $translation = (new Translator)->translate(['ordered_class_elements' => ['order' => [ + 'use_trait', 'constant', 'property_static', 'property', 'method_public_abstract', 'method_public', 'method_abstract', 'phpunit', + ]]]); + Assert::same( + ['traitUse', 'constant', 'publicStaticProperty', 'protectedStaticProperty', 'privateStaticProperty', 'property', 'publicMethod'], + $translation->decisions['classes.members.order'], + ); + Assert::same([ + 'DressCode orders an abstract or a readonly member with the others of its visibility, so `method_public_abstract` of `ordered_class_elements` stand for those.', + 'The kinds `method_abstract`, `phpunit` of `ordered_class_elements` have no equivalent in `classes.members.order` and were left out.', + ], $translation->warnings); +}); + + +test('a format of type hints told to check the doc comments sets their notation too', function () { + $sniff = 'SlevomatCodingStandard.TypeHints.UnionTypeHintFormat'; + $translation = (new Translator)->translate( + [$sniff => ['shortNullable' => 'no', 'nullPosition' => 'last', 'enableForDocComments' => true]], + ); + Assert::same( + [ + 'types.nullable' => 'union', + 'types.nullPosition' => 'last', + 'phpdoc.types.nullable' => 'union', + 'phpdoc.types.nullPosition' => 'last', + ], + $translation->decisions, + ); + Assert::same(['types.nullable' => 'questionMark'], (new Translator)->translate([$sniff => ['shortNullable' => 'yes']])->decisions); + + Assert::contains( + '`ordered_types` with `case_sensitive`', + implode("\n", (new Translator)->translate(['ordered_types' => ['case_sensitive' => true]])->warnings), + ); +}); + + +test('single_line_empty_body collapses an empty anonymous class braces_position would expand, in either order, and a closure', function () { + $expanded = ['braces_position' => ['allow_single_line_empty_anonymous_classes' => false]]; + Assert::same('expanded', (new Translator)->translate($expanded)->decisions['braces.emptyBody.anonymousClass']); + foreach ([[...$expanded, 'single_line_empty_body' => true], ['single_line_empty_body' => true, ...$expanded]] as $rules) { + Assert::same('collapsed', (new Translator)->translate($rules)->decisions['braces.emptyBody.anonymousClass']); + } + + // a closure is a function too + Assert::same('collapsed', (new Translator)->translate(['single_line_empty_body' => true])->decisions['braces.emptyBody.closure']); +}); + + +test('the space after `!` is not_operator_with_successor_space beside unary_operator_spaces, in either order', function () { + foreach ([ + ['unary_operator_spaces', 'not_operator_with_successor_space'], + ['not_operator_with_successor_space', 'unary_operator_spaces'], + ] as $order) { + $decisions = (new Translator)->translate(array_fill_keys($order, true))->decisions; + Assert::same('spaced', $decisions['spacing.logicalNot']); + Assert::same('compact', $decisions['spacing.unaryOperator']); + } + + Assert::same('compact', (new Translator)->translate(['unary_operator_spaces' => true])->decisions['spacing.logicalNot']); +}); + + +test('PER Coding Style of PHP CS Fixer leaves the parentheses around `new` alone unless a rule removes them', function () { + Assert::same('keep', (new Translator)->translate(['@PER-CS' => true])->decisions['classes.instantiationWrapping']); + Assert::same( + 'forbidden', + (new Translator)->translate(['@PER-CS' => true, '@PHP84Migration' => true])->decisions['classes.instantiationWrapping'], + ); + Assert::false(isset((new Translator)->translate(['@PSR12' => true])->decisions['classes.instantiationWrapping'])); +}); + + +test('an element with no rule of its own is dropped from the list, not passed on', function () { + // @Symfony asks for array_destructuring, which dresscode/trailingComma does not know + $translation = (new Translator)->translate([ + 'trailing_comma_in_multiline' => ['elements' => ['array_destructuring', 'arrays', 'match', 'parameters']], + ]); + Assert::same( + commaPlaces([ + 'array' => 'required', 'parameter' => 'required', 'matchArm' => 'required', 'argument' => 'keep', 'closureUse' => 'keep', + 'import' => 'keep', 'list' => 'keep', + ]), + $translation->decisions, + ); + Assert::same( + ['`trailing_comma_in_multiline` with `array_destructuring` is translated only for the short `[...]`, which `trailingComma` sets under the key `array`; a multi-line `list()` keeps its comma as written.'], + $translation->warnings, + ); +}); + + +test('a set of aliases PHP 8 no longer has is dropped from the list', function () { + $translation = (new Translator)->translate([ + 'no_alias_functions' => ['sets' => ['@internal', '@mbreg', '@exif']], + ]); + Assert::same(['hygiene.aliasFunctions' => ['core']], $translation->decisions); + Assert::same([ + '`no_alias_functions` with `@mbreg` has no equivalent; PHP 8 has none of the aliases it replaces.', + '`no_alias_functions` with `@exif` has no equivalent; PHP 8 has none of the aliases it replaces.', + ], $translation->warnings); +}); + + +test('options are translated, a rule switched off is turned off', function () { + $translation = (new Translator)->translate([ + 'cast_spaces' => ['space' => 'none'], + 'concat_space' => ['spacing' => 'one'], + 'yoda_style' => ['less_and_greater' => true], + 'elseif' => false, + 'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'], + ]); + Assert::same([ + 'spacing.cast' => 'compact', + 'spacing.concatenation' => 'spaced', + 'expressions.comparison.yoda' => 'required', + 'controlFlow.elseif' => 'keep', + 'multiline.semicolon' => 'nextLine', + ], $translation->decisions); + Assert::same( + ['`yoda_style` decides `==`, `===`, `<` and `>` one by one, which DressCode decides together for the equality comparisons alone.'], + $translation->warnings, + ); + Assert::same( + ['literals.arraySyntax.array' => 'long', 'literals.arraySyntax.list' => 'long', 'expressions.assignment.increment' => 'postfix'], + (new Translator)->translate([ + 'array_syntax' => ['syntax' => 'long'], + 'list_syntax' => ['syntax' => 'long'], + 'increment_style' => ['style' => 'post'], + ])->decisions, + ); + // the visibility of the elements the fixer names alone, as Pint names methods and properties + Assert::same( + [ + 'classes.visibility.property' => 'required', + 'classes.visibility.method' => 'required', + 'classes.visibility.interfaceMethod' => 'required', + 'classes.modifiers.order' => 'canonical', + ], + (new Translator)->translate(['modifier_keywords' => ['elements' => ['method', 'property']]])->decisions, + ); +}); + + +test('a decision that needs the types of the code is left out and said so, a keep of it is not', function () { + $translation = (new Translator)->translate(['class_keyword' => true, 'array_syntax' => true]); + Assert::same(['literals.arraySyntax.array' => 'short'], $translation->decisions); + Assert::same( + ['`literals.classNameInString` needs the types of the code, so it is left out; set it together with `typeAnalysis: phpstan` where the project has PHPStan.'], + $translation->warnings, + ); + Assert::noError(fn() => resolveTranslation($translation)); + Assert::same(['literals.classNameInString' => 'keep'], (new Translator)->translate(['class_keyword' => false])->decisions); +}); + + +test('the kinds of ordered_class_elements are translated to the kinds of memberOrder', function () { + $translation = (new Translator)->translate( + ['ordered_class_elements' => ['order' => ['use_trait', 'constant_public', 'method_public_static', 'construct', 'magic']]], + ); + Assert::same( + ['classes.members.order' => ['traitUse', 'publicConstant', 'publicStaticMethod', 'constructor', 'magicMethod']], + $translation->decisions, + ); +}); + + +test('the kinds of ordered_class_elements without an equivalent are left out and named', function () { + $translation = (new Translator)->translate( + ['ordered_class_elements' => ['order' => [ + 'use_trait', 'case', 'method:__construct', 'public', 'method_public', + ]]], + ); + Assert::same( + ['classes.members.order' => ['traitUse', 'enumCase', 'publicMethod']], + $translation->decisions, + ); + Assert::same( + ['The kinds `method:__construct`, `public` of `ordered_class_elements` have no equivalent in `classes.members.order` and were left out.'], + $translation->warnings, + ); +}); + + +test('foreign rules covering one rule are merged, not overwritten', function () { + $translation = (new Translator)->translate([ + 'SlevomatCodingStandard.Arrays.TrailingArrayComma' => true, + 'SlevomatCodingStandard.Functions.RequireTrailingCommaInCall' => true, + 'no_trailing_comma_in_singleline' => true, + 'SlevomatCodingStandard.TypeHints.ParameterTypeHint' => true, + 'SlevomatCodingStandard.TypeHints.ReturnTypeHint' => true, + ]); + Assert::same( + commaPlaces([ + 'array' => 'required', 'argument' => 'required', 'list' => 'optional', 'import' => 'optional', 'parameter' => 'keep', + 'matchArm' => 'keep', 'closureUse' => 'keep', + ]), + array_filter($translation->decisions, fn(string $path) => str_starts_with($path, 'multiline.trailingComma.'), ARRAY_FILTER_USE_KEY), + ); + Assert::same( + [ + 'types.declaration.parameter' => 'required', + 'types.declaration.return' => 'required', + 'types.declaration.anonymousFunction' => 'required', + ], + array_filter($translation->decisions, fn(string $path) => str_starts_with($path, 'types.'), ARRAY_FILTER_USE_KEY), + ); + + // a place one of them requires stays required whichever comes first + $translation = (new Translator)->translate([ + 'no_trailing_comma_in_singleline' => true, + 'SlevomatCodingStandard.Arrays.TrailingArrayComma' => true, + ]); + Assert::same( + commaPlaces([ + 'argument' => 'optional', 'array' => 'required', 'list' => 'optional', 'import' => 'optional', 'parameter' => 'keep', + 'matchArm' => 'keep', 'closureUse' => 'keep', + ]), + $translation->decisions, + ); +}); + + +test('foreign rules deciding one thing come to the same translation in either order', function () { + $translate = fn(array $rules) => [ + (new Translator)->translate($rules), + (new Translator)->translate(array_reverse($rules, preserve_keys: true)), + ]; + + // a fixer of the space before a comma leaves its alignment to the one of the space after it + foreach ($translate(['no_whitespace_before_comma_in_array' => true, 'whitespace_after_comma_in_array' => true]) as $translation) { + Assert::same(['spacing.comma' => 'spaced', 'spacing.alignment.comma' => 'optional'], $translation->decisions); + } + + // an order set in full wins over the one place another rule only prefers, and two different orders contradict + foreach ($translate([ + 'ordered_class_elements' => ['order' => ['constant_public', 'use_trait']], + 'PSR12.Traits.UseDeclaration' => true, + ]) as $translation) { + Assert::same(['publicConstant', 'traitUse'], $translation->decisions['classes.members.order']); + } + + $translator = new Translator([ + 'a_order' => fn(array $o, Translation $t) => $t->setOrder('acme.order', ['a', 'b']), + 'b_order' => fn(array $o, Translation $t) => $t->setOrder('acme.order', ['b', 'a']), + ]); + foreach ([['a_order', 'b_order'], ['b_order', 'a_order']] as $order) { + Assert::same([], $translator->translate(array_fill_keys($order, true))->decisions); + } + + // what each rule separates adds up + foreach ($translate([ + 'SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition' => true, + 'SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition' => true, + ]) as $translation) { + Assert::equal( + ['classes.members.grouping.constant' => 'separated', 'classes.members.grouping.property' => 'separated'], + array_intersect_key( + $translation->decisions, + [ + 'classes.members.grouping.constant' => 1, + 'classes.members.grouping.property' => 1, + 'classes.members.grouping.traitUse' => 1, + ], + ), + ); + } + + // counts of blank lines widen to the range of both, a count a map gives included + foreach ($translate(['class_attributes_separation' => true, 'Squiz.WhiteSpace.FunctionSpacing' => true]) as $translation) { + Assert::same([1, 2], $translation->decisions['blankLines.aroundMethods']); + } + + foreach ($translate([ + 'blank_line_before_statement' => true, + 'SlevomatCodingStandard.ControlStructures.JumpStatementsSpacing' => true, + ]) as $translation) { + Assert::same([1, null], $translation->decisions['blankLines.beforeStatement']['return']); + Assert::same(1, $translation->decisions['blankLines.beforeStatement']['yield']); + } + + // values that contradict each other leave the decision out, and a keep a table writes gives way + foreach ($translate(['group_import' => true, 'single_import_per_statement' => true]) as $translation) { + Assert::false(isset($translation->decisions['imports.grouping.class'])); + Assert::contains( + 'The foreign rules give `imports.grouping.class` values that contradict each other; DressCode leaves it out, for the configuration to set by hand.', + $translation->warnings, + ); + } + + $translator = new Translator([ + 'a_keep' => ['acme.foo' => 'keep'], + 'a_set' => ['acme.foo' => 'x'], + 'a_other' => ['acme.foo' => 'y'], + ]); + Assert::same(['acme.foo' => 'x'], $translator->translate(['a_keep' => true, 'a_set' => true])->decisions); + Assert::same(['acme.foo' => 'x'], $translator->translate(['a_set' => true, 'a_keep' => true])->decisions); + foreach ([['a_set', 'a_keep', 'a_other'], ['a_other', 'a_set', 'a_keep'], ['a_keep', 'a_other', 'a_set']] as $order) { + Assert::same([], $translator->translate(array_fill_keys($order, true))->decisions); + } +}); + + +test('an option with no value of DressCode to write is left out and said so, not written to be refused', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules); + + $translation = $translate( + ['SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing' => ['controlStructures' => ['if', 'case', 'default']]], + ); + Assert::same(['blankLines.beforeStatement' => ['if' => 1], 'blankLines.afterStatement' => ['if' => 1]], $translation->decisions); + Assert::same( + ['`SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing` with `case`, `default` has no equivalent; DressCode counts the blank lines before a `case` in `blankLines.betweenCases`.'], + $translation->warnings, + ); + Assert::noError(fn() => resolveTranslation($translation)); + + $translation = $translate( + ['SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing' => ['controlStructures' => ['case', 'default']]], + ); + Assert::same([], $translation->decisions); + Assert::count(1, $translation->warnings); + + foreach ([ + 'SlevomatCodingStandard.Classes.RequireMultiLineMethodSignature', + 'SlevomatCodingStandard.ControlStructures.RequireMultiLineCondition', + ] as $sniff) { + $translation = $translate([$sniff => ['minLineLength' => 0]]); + Assert::null($translation->lineLength); + Assert::match("`$sniff` with `minLineLength=0` has no equivalent; %a%", $translation->warnings[0] ?? ''); + Assert::noError(fn() => resolveTranslation($translation)); + } + + $translation = $translate(['no_alias_functions' => ['sets' => ['@snmp', '@IMAP']]]); + Assert::same(['hygiene.aliasFunctions' => ['imap']], $translation->decisions); + Assert::same( + ['`no_alias_functions` with `@snmp` has no equivalent; DressCode leaves the aliases of the SNMP functions alone.'], + $translation->warnings, + ); + Assert::noError(fn() => resolveTranslation($translation)); + + $translation = $translate(['numeric_literal_separator' => ['strategy' => 'no_separator']]); + Assert::same( + ['literals.digitSeparator.integer' => 'forbidden', 'literals.digitSeparator.fraction' => 'forbidden'], + $translation->decisions, + ); + Assert::same([], $translation->warnings); + + // a ruleset writes no value as the text `null` + $translation = $translate(['Generic.PHP.ForbiddenFunctions' => ['forbiddenFunctions' => ['delete' => 'null', 'sizeof' => 'count']]]); + Assert::same(['upgrading.api.forbiddenFunctions' => ['delete' => null, 'sizeof' => 'use `count()`']], $translation->decisions); +}); + + +test('a translation of the trailing comma leaves the places it does not name as they are', function () { + $places = ['array', 'argument', 'parameter', 'matchArm', 'closureUse', 'import', 'list']; + $translation = (new Translator)->translate(['SlevomatCodingStandard.Functions.RequireTrailingCommaInDeclaration' => true]); + Assert::same(commaPlaces(['parameter' => 'required'] + array_fill_keys($places, 'keep')), $translation->decisions); + + $rules = [ + 'no_trailing_comma_in_singleline' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arguments']], + 'SlevomatCodingStandard.Arrays.TrailingArrayComma' => true, + ]; + $expected = ['array' => 'required', 'argument' => 'required', 'list' => 'optional', 'import' => 'optional'] + array_fill_keys( + $places, + 'keep', + ); + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::equal(commaPlaces($expected), (new Translator)->translate($order)->decisions); + } +}); + + +test('a translation of empty parentheses leaves the places it does not name as they are', function () { + $translation = (new Translator)->translate(['attribute_empty_parentheses' => true]); + Assert::equal(['classes.emptyParentheses.attribute' => 'forbidden'], $translation->decisions); + + $rules = ['new_with_parentheses' => ['anonymous_class' => false], 'attribute_empty_parentheses' => ['use_parentheses' => true]]; + $expected = [ + 'classes.emptyParentheses.instantiation' => 'required', + 'classes.emptyParentheses.anonymousClass' => 'forbidden', + 'classes.emptyParentheses.attribute' => 'required', + ]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::equal($expected, (new Translator)->translate($order)->decisions); + } + + // the sniff removes the parentheses of a named class and passes an anonymous one by + $translation = (new Translator)->translate(['SlevomatCodingStandard.ControlStructures.NewWithoutParentheses' => true]); + Assert::equal(['classes.emptyParentheses.instantiation' => 'forbidden'], $translation->decisions); +}); + + +test('a sniff of the class declaration puts each interface of a multiline list on its own line', function () { + foreach (['PSR2.Classes.ClassDeclaration', 'PSR12.Classes.AnonClassDeclaration'] as $sniff) { + $translation = (new Translator)->translate([$sniff => true]); + Assert::same('perLine', $translation->decisions['multiline.items.implements']); + Assert::same('apart', $translation->decisions['multiline.edges.implements']); + Assert::same([], $translation->warnings); + } +}); + + +test('a fixer of the case of one kind of native names leaves the other kinds as they are', function () { + Assert::equal( + ['casing.type' => 'lowercase'], + (new Translator)->translate(['native_type_declaration_casing' => true])->decisions, + ); + + $rules = ['native_function_casing' => true, 'class_reference_name_casing' => true]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::equal( + ['casing.class' => 'declared', 'casing.function' => 'declared'], + (new Translator)->translate($order)->decisions, + ); + } +}); + + +test('a fixer of the imports leaves the names of the global namespace to a fixer of those', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules)->decisions; + Assert::same(['imports.leadingBackslash' => 'forbidden'], $translate(['no_leading_import_slash' => true])); + + $rules = ['no_leading_import_slash' => true, 'PhpCsFixerCustomFixers/no_leading_slash_in_global_namespace' => true]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::equal( + ['imports.leadingBackslash' => 'forbidden', 'qualification.leadingBackslash' => 'forbidden'], + $translate($order), + ); + } +}); + + +test('a fixer of the case of constants leaves the keywords alone', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules)->decisions; + Assert::equal(['casing.trueFalseNull' => 'uppercase'], $translate(['constant_case' => ['case' => 'upper']])); + Assert::equal( + ['casing.keyword' => 'lowercase', 'casing.magicConstant' => 'uppercase'], + $translate(['magic_constant_casing' => true, 'lowercase_keywords' => true]), + ); +}); + + +test('the order of the types of a doc comment translates to the options typeNotation has too', function () { + Assert::equal( + ['phpdoc.types.unionOrder' => 'keep', 'phpdoc.types.nullPosition' => 'last'], + (new Translator)->translate(['phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none']])->decisions, + ); + Assert::equal( + ['types.unionOrder' => 'alphabetical', 'types.nullPosition' => 'keep'], + (new Translator)->translate(['ordered_types' => ['null_adjustment' => 'none']])->decisions, + ); +}); + + +test('a fixer of the names of doc types leaves the notation of an array alone, unless another one sets it', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules)->decisions['phpdoc.types.array'] ?? null; + Assert::null($translate(['phpdoc_scalar' => true])); + + $rules = ['phpdoc_types' => true, 'phpdoc_array_type' => true]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::same('generic', $translate($order)); + } +}); + + +test('a fixer keeping group uses gives way to one making them', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules)->decisions['imports.grouping.class'] ?? null; + Assert::same(['separated', 'grouped'], $translate(['single_import_per_statement' => ['group_to_single_imports' => false]])); + Assert::same('separated', $translate(['single_import_per_statement' => true])); + + // the sniffs expand a group use, or forbid the comma between its names + Assert::same('separated', $translate(['PSR2.Namespaces.UseDeclaration' => true])); + Assert::same('separated', $translate(['SlevomatCodingStandard.Namespaces.MultipleUsesPerLine' => true])); + Assert::same(['separated', 'combined'], $translate(['SlevomatCodingStandard.Namespaces.DisallowGroupUse' => true])); + + $rules = ['group_import' => true, 'single_import_per_statement' => ['group_to_single_imports' => false]]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::same('grouped', (new Translator)->translate($order)->decisions['imports.grouping.class']); + } +}); + + +test('a place the translation does not name keeps what the preset of the translation gives it', function () { + $preset = [ + 'array' => 'required', 'argument' => 'required', 'parameter' => 'required', 'matchArm' => 'required', 'closureUse' => 'required', + 'import' => 'optional', 'list' => 'optional', + ]; + $translate = fn(array $rules) => array_diff_key( + (new Translator)->translate($rules)->decisions, + ['classes.instantiationWrapping' => true], + ); + + // the set still asks for the comma of a multi-line list, the rule of one line adds nothing to that + Assert::equal(commaPlaces($preset), $translate(['@PER-CS' => true, 'no_trailing_comma_in_singleline' => true])); + + // the fixer named replaces what the set says of a multi-line list, and the set still removes the comma on one line + Assert::equal( + commaPlaces(['array' => 'required'] + array_fill_keys(array_keys($preset), 'optional')), + $translate(['@PER-CS' => true, 'trailing_comma_in_multiline' => ['elements' => ['arrays']]]), + ); + + // a standard that does not run the rule leaves the places alone + Assert::equal( + commaPlaces(['argument' => 'required'] + array_fill_keys(array_keys($preset), 'keep')), + $translate(['PSR12' => true, 'SlevomatCodingStandard.Functions.RequireTrailingCommaInCall' => true]), + ); +}); + + +test('options of a rule that takes none are reported, not dropped in silence', function () { + $translation = (new Translator)->translate(['no_spaces_around_offset' => ['positions' => ['inside']]]); + Assert::same(['spacing.padding.offset' => 'forbidden'], $translation->decisions); + Assert::same( + ['The options of `no_spaces_around_offset` were not translated; set them by hand, `dresscode explain spacing.padding.offset` describes them.'], + $translation->warnings, + ); +}); + + +test('a rule of blank lines translates to the places it counts, with the counts its options give', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules); + Assert::same( + ['blankLines.afterNamespace' => 1, 'blankLines.beforeDeclaration' => 1], + $translate(['PSR2.Namespaces.NamespaceDeclaration' => true])->decisions, + ); + Assert::same( + ['blankLines.afterImports' => 1, 'blankLines.beforeDeclaration' => 1], + $translate(['single_line_after_imports' => true])->decisions, + ); + Assert::same( + ['blankLines.aroundMethods' => [1, 2], 'blankLines.aroundInterfaceMethods' => [1, 2]], + $translate(['SlevomatCodingStandard.Classes.MethodSpacing' => ['maxLinesCount' => 2]])->decisions, + ); + Assert::same( + [ + 'blankLines.betweenMembers.constant' => [0, 1], + 'blankLines.betweenMemberKinds' => [0, 1], + 'blankLines.beforeDocumentedMember' => 2, + ], + $translate( + ['SlevomatCodingStandard.Classes.ConstantSpacing' => [ + 'minLinesCountBeforeWithComment' => 2, + 'maxLinesCountBeforeWithComment' => 2, + ]], + )->decisions, + ); + + $translation = $translate( + ['class_attributes_separation' => ['elements' => ['method' => 'one', 'property' => 'only_if_meta', 'trait_import' => 'none']]], + ); + Assert::same([ + 'blankLines.aroundMethods' => 1, + 'blankLines.aroundInterfaceMethods' => 1, + 'blankLines.betweenTraitUses' => 0, + 'blankLines.betweenMembers.property' => 0, 'blankLines.betweenMemberKinds' => 0, + 'blankLines.beforeDocumentedMember' => 1, + ], $translation->decisions); + Assert::same([], $translation->warnings); + + $translation = $translate(['no_extra_blank_lines' => ['tokens' => ['curly_brace_block', 'return', 'use_trait', 'extra']]]); + Assert::same([ + 'blankLines.bodyStart.function' => 0, + 'blankLines.bodyStart.closure' => 0, + 'blankLines.bodyStart.propertyHook' => 0, + 'blankLines.bodyStart.controlStructure' => 0, + 'blankLines.bodyEnd.function' => 0, + 'blankLines.bodyEnd.closure' => 0, + 'blankLines.bodyEnd.propertyHook' => 0, + 'blankLines.bodyEnd.controlStructure' => 0, + 'blankLines.betweenTraitUses' => 0, + ], $translation->decisions); + Assert::same([ + '`no_extra_blank_lines` with `return` removes the blank lines after the jump, which stand only before the closing brace or the next `case`, what follows a jump in its block being unreachable; set `blankLines.bodyEnd.controlStructure` and `blankLines.bodyEnd.switchCase` for those.', + '`no_extra_blank_lines` with `extra` has no equivalent; DressCode counts the blank lines place by place in `blankLines`, with no limit of their own for these.', + ], $translation->warnings); + + $translation = $translate(['no_extra_blank_lines' => ['tokens' => ['attribute']]]); + Assert::same(['blankLines.afterAttributes' => 0], $translation->decisions); + Assert::same([], $translation->warnings); + + // a sniff of control structures asks for a blank line after one at least, and checks only their own braces and parentheses + $translation = $translate(['Squiz.WhiteSpace.ControlStructureSpacing' => true]); + Assert::same([1, null], $translation->decisions['blankLines.afterStatement']['if']); + Assert::same([0, 'forbidden'], [ + $translation->decisions['blankLines.bodyStart.controlStructure'], + $translation->decisions['spacing.padding.controlStructure'], + ]); + Assert::same([], $translation->warnings); + + // the defaults of the fixer keep enum cases together, which is not worth a warning, a separation chosen is + $translation = $translate(['class_attributes_separation' => true]); + $members = [ + 'blankLines.betweenMembers.enumCase' => 0, 'blankLines.betweenMembers.constant' => 1, 'blankLines.betweenMembers.property' => 1, + 'blankLines.betweenMemberKinds' => [0, 1], 'blankLines.beforeDocumentedMember' => [0, 1], + ]; + Assert::same($members, array_intersect_key($translation->decisions, $members)); + Assert::same([], $translation->warnings); + $translation = $translate(['class_attributes_separation' => ['elements' => ['const' => 'none', 'property' => 'one']]]); + Assert::same([ + 'blankLines.betweenMembers.constant' => 0, 'blankLines.betweenMembers.property' => 1, + 'blankLines.betweenMemberKinds' => [0, 1], 'blankLines.beforeDocumentedMember' => [0, 1], + ], $translation->decisions); + Assert::match( + '`class_attributes_separation` separates constants, properties and enum cases differently, %a%', + $translation->warnings[0] ?? '', + ); + + // two sniffs counting the members differently keep a count each, and widen the one they share to the range of both, + // in either order + $rules = [ + 'SlevomatCodingStandard.Classes.ConstantSpacing' => true, + 'SlevomatCodingStandard.Classes.PropertySpacing' => [ + 'minLinesCountBeforeWithoutComment' => 1, + 'maxLinesCountBeforeWithoutComment' => 2, + ], + ]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + $translation = $translate($order); + $decisions = $translation->decisions; + ksort($decisions); + Assert::same([ + 'blankLines.beforeDocumentedMember' => 1, 'blankLines.betweenMemberKinds' => [0, 2], + 'blankLines.betweenMembers.constant' => [0, 1], 'blankLines.betweenMembers.property' => [1, 2], + ], $decisions); + Assert::same( + ['The foreign rules count the blank lines of `blankLines.betweenMemberKinds` differently; DressCode has one count there and takes the range of them all.'], + $translation->warnings, + ); + } + + $translation = $translate([ + 'SlevomatCodingStandard.Classes.ConstantSpacing' => true, + 'SlevomatCodingStandard.Classes.PropertySpacing' => true, + ]); + Assert::same([ + 'blankLines.betweenMembers.constant' => [0, 1], 'blankLines.betweenMemberKinds' => [0, 1], + 'blankLines.beforeDocumentedMember' => 1, 'blankLines.betweenMembers.property' => [0, 1], + ], $translation->decisions); + Assert::same([], $translation->warnings); + + // imports of different kinds keep the blank line another fixer asks for, in either order + $rules = ['no_extra_blank_lines' => ['tokens' => ['use']], 'blank_line_between_import_groups' => true]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::same(1, $translate($order)->decisions['blankLines.betweenImportKinds']); + } +}); + + +test('a fixer of a declaration translates the parentheses, the comma and the brace of a function', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules); + $translation = $translate(['function_declaration' => true]); + Assert::same('forbidden', $translation->decisions['spacing.padding.argument']); + Assert::same('optional', $translation->decisions['multiline.trailingComma.parameter']); + Assert::same([], $translation->warnings); + + // a comma another fixer requires stays, in either order + $rules = ['function_declaration' => true, 'trailing_comma_in_multiline' => ['elements' => ['parameters']]]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::same('required', $translate($order)->decisions['multiline.trailingComma.parameter']); + } + + $translation = $translate(['function_declaration' => ['trailing_comma_single_line' => true]]); + Assert::false(isset($translation->decisions['multiline.trailingComma.parameter'])); + Assert::same( + ['`function_declaration` with `trailing_comma_single_line=true` has no equivalent; DressCode removes the trailing comma of parameters on one line.'], + $translation->warnings, + ); + + $translation = $translate(['braces_position' => true]); + Assert::same( + ['nextLine', 'sameLine'], + [$translation->decisions['braces.position.function'], $translation->decisions['braces.position.multilineSignature']], + ); + $translation = $translate(['braces_position' => ['functions_opening_brace' => 'same_line']]); + Assert::same( + ['sameLine', 'sameLine'], + [$translation->decisions['braces.position.function'], $translation->decisions['braces.position.multilineSignature']], + ); + + // the destructuring of the fixer alone has no place of its own + $translation = $translate(['trailing_comma_in_multiline' => ['elements' => ['array_destructuring']]]); + Assert::same([], $translation->decisions); + Assert::match( + '`trailing_comma_in_multiline` with `array_destructuring` and without `arrays` has no equivalent; %a%', + $translation->warnings[0] ?? '', + ); + + Assert::match( + '`Squiz.ControlStructures.ForEachLoopDeclaration` also puts a single space around the `=>` of a `foreach`, %a%', + $translate(['Squiz.ControlStructures.ForEachLoopDeclaration' => true])->warnings[0] ?? '', + ); +}); + + +test('the fixers of the indentation and the line ending write those of the configuration object', function () { + $translation = (new Translator)->translate(['indentation_type' => true, 'statement_indentation' => true, 'line_ending' => true]); + Assert::same(['indentation.unit' => '4 spaces', 'file.lineEnding' => 'LF'], $translation->decisions); + Assert::same([], $translation->warnings); + + $translation = (new Translator)->translate(['indentation_type' => true, 'line_ending' => true], "\t", "\r\n"); + Assert::same(['indentation.unit' => 'tab', 'file.lineEnding' => 'CRLF'], $translation->decisions); + + $translation = (new Translator)->translate(['indentation_type' => true], ' '); + Assert::same([], $translation->decisions); + Assert::same( + ["The fixers of the indentation indent by the `indent` ' ' of the configuration, which has no equivalent; DressCode indents by a tab, four spaces or two."], + $translation->warnings, + ); + + $dir = createTempDir('translator'); + file_put_contents( + "$dir/fixer.php", + " true]; }\n\tpublic function getIndent(): string { return \"\\t\"; }\n\tpublic function getLineEnding(): string { return \"\\r\\n\"; }\n};\n", + ); + Assert::same([['indentation_type' => true], "\t", "\r\n"], PhpCsFixer::readConfig("$dir/fixer.php")); + file_put_contents("$dir/fixer.php", " true];\n"); + Assert::same([['indentation_type' => true], ' ', "\n"], PhpCsFixer::readConfig("$dir/fixer.php")); +}); + + +test('a sniff of the indentation translates to the unit it names, in either order', function () { + $translate = fn(array $rules) => (new Translator)->translate($rules)->decisions; + Assert::same(['indentation.unit' => 'tab'], $translate(['Generic.WhiteSpace.DisallowSpaceIndent' => true])); + Assert::same(['indentation.unit' => '4 spaces'], $translate(['Generic.WhiteSpace.DisallowTabIndent' => true])); + $rules = ['Generic.WhiteSpace.DisallowTabIndent' => true, 'Generic.WhiteSpace.ScopeIndent' => ['indent' => 2]]; + foreach ([$rules, array_reverse($rules, preserve_keys: true)] as $order) { + Assert::same(['indentation.unit' => '2 spaces'], $translate($order)); + } +}); + + +test('a name of another tool stands for the decisions it sets', function () { + $translator = new Translator; + Assert::same(['spacing.cast'], $translator->findPaths('cast_spaces')); + Assert::same(['literals.arraySyntax.array'], $translator->findPaths('array_syntax')); + Assert::same([], $translator->findPaths('no_such_fixer')); + Assert::contains('Squiz.Scope.MethodScope', $translator->findForeignNames($translator->findPaths('Squiz.Scope.MethodScope'))); + Assert::contains('cast_spaces', $translator->findForeignNames(['spacing.cast'])); + Assert::same([], $translator->findForeignNames(['no.such.decision'])); +}); + + +test('a translator over tables of its own', function () { + $translator = new Translator( + [ + 'foo_bar' => ['acme.foo' => 'x'], + 'Acme.Bar' => fn(array $options, Translation $t) => $t + ->set('acme.bar', $options['x'] ?? 1) + ->set('acme.foo', 'x'), + ], + ['@Acme' => 'acme/preset'], + ); + $translation = $translator->translate(['@Acme' => true, 'Acme.Bar' => ['x' => 2], 'cast_spaces' => true]); + Assert::same(['acme/preset'], $translation->presets); + Assert::same(['acme.bar' => 2, 'acme.foo' => 'x'], $translation->decisions); + Assert::same(['No DressCode rule covers `cast_spaces`.'], $translation->warnings); + Assert::same(['acme.bar', 'acme.foo'], $translator->findPaths('Acme.Bar')); + Assert::same(['foo_bar', 'Acme.Bar'], $translator->findForeignNames(['acme.foo'])); +}); + + +test('a rule switched off makes what only it stands for keep, and leaves what another rule of the tool stands for too', function () { + $translator = new Translator( + [ + 'a_one' => ['controlFlow.elseif' => 'oneWord'], + 'a_two' => ['controlFlow.elseif' => 'oneWord'], + 'b_only' => ['file.closingTagAtEnd' => 'forbidden'], + 'Acme.Cat.A' => ['controlFlow.elseif' => 'oneWord'], + ], + ['@Acme' => 'acme/preset'], + ); + $translation = $translator->translate(['@Acme' => true, 'a_one' => false, 'b_only' => false]); + Assert::same(['file.closingTagAtEnd' => 'keep'], $translation->decisions); + Assert::same( + ['`a_one` is turned off, but `controlFlow.elseif` also stands for `a_two`, so it stays; set it to `keep` if none of them applies.'], + $translation->warnings, + ); + Assert::contains('closingTagAtEnd', $translation->toPhp()); + Assert::notContains('elseif', $translation->toPhp()); + + // a sniff shares nothing with a fixer, and what some foreign rule sets stays set in either order + Assert::same(['controlFlow.elseif' => 'keep'], $translator->translate(['Acme.Cat.A' => false])->decisions); + Assert::same(['controlFlow.elseif' => 'oneWord'], $translator->translate(['a_one' => true, 'Acme.Cat.A' => false])->decisions); + Assert::same(['controlFlow.elseif' => 'oneWord'], $translator->translate(['Acme.Cat.A' => false, 'a_one' => true])->decisions); + + // an exclusion of one message of a sniff cannot narrow what the sniff stands for + $translation = $translator->translate(['Acme.Cat.A.Message' => false]); + Assert::same([], $translation->decisions); + Assert::same( + ['`Acme.Cat.A.Message` is excluded, but `controlFlow.elseif` cannot leave out that check alone, so it stays as the rest of the configuration sets it.'], + $translation->warnings, + ); +}); + + +test('a rule switched off under a set stays off in the configuration the translation writes', function () { + $resolved = resolveTranslation((new Translator)->translate(['@PSR12' => true, 'no_closing_tag' => false])); + Assert::true($resolved->decisions['file.closingTagAtEnd']->value->isKept()); + Assert::false(resolveTranslation((new Translator)->translate(['@PSR12' => true]))->decisions['file.closingTagAtEnd']->value->isKept()); + + // declare_equal_normalize shares its decision with declare_parentheses, so it is said, not done + $translation = (new Translator)->translate(['@PSR12' => true, 'declare_equal_normalize' => false]); + Assert::same([], $translation->decisions); + Assert::match( + '`declare_equal_normalize` is turned off, but `spacing.declare` also stands for `declare_parentheses%a%', + $translation->warnings[0] ?? '', + ); +}); + + +test('a ruleset switches off a sniff it excludes inside a rule, and says it leaves the excluded paths out', function () { + $dir = createTempDir('translator'); + file_put_contents("$dir/phpcs.xml", <<<'XX' + + + tests/* + + + legacy/* + + + XX); + [$rules, $warnings] = PhpCodeSniffer::readConfig("$dir/phpcs.xml"); + Assert::same(['PSR12' => true, 'PSR2.Classes.ClassDeclaration' => false], $rules); + Assert::same([ + 'The paths the ruleset excludes are not carried over; set them with the key `excludePaths`.', + 'The paths excluded from `PSR12` are not carried over; turn its rules off there with an override.', + ], $warnings); + + file_put_contents("$dir/phpcs.xml", '0'); + Assert::same([['Squiz.Scope.MethodScope' => false], []], PhpCodeSniffer::readConfig("$dir/phpcs.xml")); +}); + + +test('a ruleset that is missing or broken is said so', function () { + $dir = createTempDir('translator'); + Assert::exception( + fn() => PhpCodeSniffer::readConfig("$dir/phpcs.xml"), + ConfigurationException::class, + 'File `%a%phpcs.xml` does not exist.', + ); + + file_put_contents("$dir/phpcs.xml", "\n\n"); + Assert::exception( + fn() => PhpCodeSniffer::readConfig("$dir/phpcs.xml"), + ConfigurationException::class, + 'File `%a%phpcs.xml` is not a PHP_CodeSniffer ruleset: Opening and ending tag mismatch: rule line 2 and ruleset on line 3.', + ); +}); + + +test('the sniffs of PSR12 listed one by one translate, all but four that no rule covers', function () { + $sniffs = [ + 'Generic.ControlStructures.InlineControlStructure', 'Generic.Files.ByteOrderMark', 'Generic.Files.LineEndings', + 'Generic.Files.LineLength', 'Generic.Formatting.DisallowMultipleStatements', 'Generic.Functions.FunctionCallArgumentSpacing', + 'Generic.NamingConventions.UpperCaseConstantName', 'Generic.PHP.DisallowAlternativePHPTags', 'Generic.PHP.DisallowShortOpenTag', + 'Generic.PHP.LowerCaseConstant', 'Generic.PHP.LowerCaseKeyword', 'Generic.PHP.LowerCaseType', + 'Generic.WhiteSpace.DisallowTabIndent', 'Generic.WhiteSpace.IncrementDecrementSpacing', 'Generic.WhiteSpace.ScopeIndent', + 'PEAR.Functions.ValidDefaultValue', + 'PSR1.Classes.ClassDeclaration', 'PSR1.Files.SideEffects', 'PSR1.Methods.CamelCapsMethodName', + 'PSR2.Classes.ClassDeclaration', 'PSR2.Classes.PropertyDeclaration', 'PSR2.ControlStructures.ElseIfDeclaration', + 'PSR2.ControlStructures.SwitchDeclaration', 'PSR2.Files.ClosingTag', 'PSR2.Files.EndFileNewline', + 'PSR2.Methods.FunctionCallSignature', 'PSR2.Methods.FunctionClosingBrace', 'PSR2.Methods.MethodDeclaration', + 'PSR12.Classes.AnonClassDeclaration', 'PSR12.Classes.ClassInstantiation', 'PSR12.Classes.ClosingBrace', + 'PSR12.Classes.OpeningBraceSpace', 'PSR12.ControlStructures.BooleanOperatorPlacement', + 'PSR12.ControlStructures.ControlStructureSpacing', 'PSR12.Files.DeclareStatement', 'PSR12.Files.FileHeader', + 'PSR12.Files.ImportStatement', 'PSR12.Files.OpenTag', 'PSR12.Functions.NullableTypeDeclaration', + 'PSR12.Functions.ReturnTypeDeclaration', 'PSR12.Keywords.ShortFormTypeKeywords', 'PSR12.Namespaces.CompoundNamespaceDepth', + 'PSR12.Operators.OperatorSpacing', 'PSR12.Properties.ConstantVisibility', 'PSR12.Traits.UseDeclaration', + 'Squiz.Classes.ValidClassName', 'Squiz.ControlStructures.ControlSignature', 'Squiz.ControlStructures.ForEachLoopDeclaration', + 'Squiz.ControlStructures.ForLoopDeclaration', 'Squiz.ControlStructures.LowercaseDeclaration', 'Squiz.Functions.FunctionDeclaration', + 'Squiz.Functions.FunctionDeclarationArgumentSpacing', 'Squiz.Functions.LowercaseFunctionKeywords', + 'Squiz.Functions.MultiLineFunctionDeclaration', 'Squiz.Scope.MethodScope', 'Squiz.WhiteSpace.CastSpacing', + 'Squiz.WhiteSpace.ControlStructureSpacing', 'Squiz.WhiteSpace.ScopeClosingBrace', 'Squiz.WhiteSpace.ScopeKeywordSpacing', + 'Squiz.WhiteSpace.SuperfluousWhitespace', + ]; + Assert::count(60, $sniffs); + $translation = (new Translator)->translate(array_fill_keys($sniffs, true)); + $uncovered = [ + 'Generic.PHP.DisallowAlternativePHPTags', + 'PSR1.Classes.ClassDeclaration', + 'PSR1.Files.SideEffects', + 'PSR12.Namespaces.CompoundNamespaceDepth', + ]; + Assert::same( + $uncovered, + array_values(array_filter($sniffs, fn($sniff) => in_array("No DressCode rule covers `$sniff`.", $translation->warnings, true))), + ); + + // what this one checks DressCode does by decisions that say more, so it says why instead + $explained = ['Squiz.WhiteSpace.ScopeClosingBrace']; + $translator = new Translator; + foreach ($sniffs as $sniff) { + Assert::same(in_array($sniff, [...$uncovered, ...$explained], true), $translator->findPaths($sniff) === [], $sniff); + } + + Assert::noError(fn() => resolveTranslation($translation)); + Assert::same('required', $translation->decisions['classes.emptyParentheses.instantiation']); + Assert::false(isset($translation->decisions['classes.emptyParentheses.anonymousClass'])); + Assert::same(['traitUse'], $translation->decisions['classes.members.order']); + Assert::same('separated', $translation->decisions['classes.members.grouping.traitUse']); + Assert::same('spaced', $translation->decisions['spacing.concatenation']); +}); + + +test('the properties of the sniffs of PSR12 translate, or say they have no equivalent', function () { + Assert::same( + ['multiline.operatorPosition.condition' => 'lineStart'], + (new Translator)->translate(['PSR12.ControlStructures.BooleanOperatorPlacement' => ['allowOnly' => 'first']])->decisions, + ); + + // without `allowOnly` either place is allowed, which a preset requiring one does not override + $translation = (new Translator)->translate(['PSR12' => true, 'PSR12.ControlStructures.BooleanOperatorPlacement' => true]); + Assert::same(['multiline.operatorPosition.condition' => 'keep'], $translation->decisions); + Assert::true(resolveTranslation($translation)->decisions['multiline.operatorPosition.condition']->value->isKept()); + + Assert::same( + ['multiline.operatorPosition.condition' => 'lineEnd'], + (new Translator)->translate(['PSR12.ControlStructures.BooleanOperatorPlacement' => ['allowOnly' => 'last']])->decisions, + ); + + $translation = (new Translator)->translate([ + 'PSR2.Methods.FunctionCallSignature' => ['allowMultipleArguments' => true, 'requiredSpacesAfterOpen' => 1], + 'Squiz.ControlStructures.ForLoopDeclaration' => ['requiredSpacesBeforeClose' => 1], + ]); + Assert::same([ + '`PSR2.Methods.FunctionCallSignature` with spaces inside the parentheses has no equivalent; DressCode writes none.', + '`Squiz.ControlStructures.ForLoopDeclaration` with spaces inside the parentheses has no equivalent; DressCode writes none.', + ], $translation->warnings); + Assert::same('fill', $translation->decisions['multiline.items.argument']); + + $translation = (new Translator)->translate(['method_argument_space' => ['on_multiline' => 'ignore']]); + Assert::same([], $translation->warnings); + Assert::false(isset($translation->decisions['multiline.items.argument'])); +}); + + +test('a message excluded from a sniff turns off its rule, which the sniff covers through that message', function () { + $translation = (new Translator)->translate(['PSR12' => true, 'PSR2.ControlStructures.SwitchDeclaration.SpaceBeforeColonCASE' => false]); + Assert::same(['spacing.switchCase' => 'keep'], $translation->decisions); + Assert::same([], $translation->warnings); + + // the sniff excluded whole turns off what only it and its messages cover, and says what another sniff covers too + $translation = (new Translator)->translate(['PSR12' => true, 'PSR2.ControlStructures.SwitchDeclaration' => false]); + Assert::same( + [ + 'upgrading.php.caseSemicolon' => 'keep', + 'spacing.switchCase' => 'keep', + 'controlFlow.switch.fallThroughComment' => 'keep', + 'indentation.switchCase' => 'keep', + ], + $translation->decisions, + ); + Assert::match( + '`PSR2.ControlStructures.SwitchDeclaration` is turned off, but `casing.keyword`, %a% also stand for `Generic.PHP.LowerCaseKeyword`, `Generic.PHP.LowerCaseType`%a%', + $translation->warnings[0] ?? '', + ); + + // a sniff turned off in the configuration stands for nothing there + $translation = (new Translator)->translate([ + 'PSR12' => true, + 'Generic.WhiteSpace.DisallowTabIndent' => false, + 'Generic.WhiteSpace.DisallowSpaceIndent' => false, + ]); + Assert::same([ + '`Generic.WhiteSpace.DisallowTabIndent` is turned off, but `indentation.unit` also stands for `Generic.WhiteSpace.ScopeIndent`, so it stays; set it to `keep` if none of them applies.', + '`Generic.WhiteSpace.DisallowSpaceIndent` is turned off, but `indentation.unit` also stands for `Generic.WhiteSpace.ScopeIndent`, so it stays; set it to `keep` if none of them applies.', + ], $translation->warnings); + + // and without a standard only the sniffs the configuration names run + $translation = (new Translator)->translate(['Generic.WhiteSpace.DisallowTabIndent' => false, 'Squiz.Scope.MethodScope' => true]); + Assert::same(['indentation.unit' => 'keep'], array_intersect_key($translation->decisions, ['indentation.unit' => 1])); + Assert::same([], $translation->warnings); +}); + + +test('a .php-cs-fixer.php that cannot be run is an error of the configuration, not the end of the run', function () { + $dir = createTempDir('translator'); + + // a class it misses is most likely PHP CS Fixer, which only the dresscode of the project sees + file_put_contents("$dir/.php-cs-fixer.php", "setRules(['@PSR12' => true]);\n"); + Assert::exception( + fn() => PhpCsFixer::readConfig("$dir/.php-cs-fixer.php"), + ConfigurationException::class, + 'File `%a%.php-cs-fixer.php` needs the classes of PHP CS Fixer, which this dresscode cannot load; run the `vendor/bin/dresscode` of the project that has PHP CS Fixer installed: Class "Acme\\Missing\\Config" not found.', + ); + + // any other error of the file is said as it is + file_put_contents("$dir/.php-cs-fixer.php", " PhpCsFixer::readConfig("$dir/.php-cs-fixer.php"), + ConfigurationException::class, + "File `%a%.php-cs-fixer.php` cannot be read: Unclosed '[' on line 2.", + ); +}); + + +test('the configuration is written as a dresscode.php, a rule written as its decisions by them', function () { + $translation = (new Translator)->translate(['@PSR12' => true, 'cast_spaces' => ['space' => 'none'], 'elseif' => true]); + Assert::same( + " [\n" + . "\t\t\t'cast' => '(int)\$x',\n" + . "\t\t],\n" + . "\t\t'controlFlow' => [\n" + . "\t\t\t'elseif' => 'elseif',\n" + . "\t\t],\n" + . "\t],\n" + . ");\n", + $translation->toPhp(), + ); + + // a list of plain values stays on its line, an empty map too + $translation = (new Translator)->translate(['blank_line_before_statement' => ['statements' => ['return']], 'phpdoc_trim' => true]); + Assert::contains("\t\t\t'beforeStatement' => [\n\t\t\t\t'return' => '1+',\n\t\t\t],\n", $translation->toPhp()); + $translation->set('blankLines.afterStatement', []); + Assert::contains("\t\t\t'afterStatement' => [],\n", $translation->toPhp()); + + // a map with numbers for keys keeps them + $translation = new Translation; + $translation->set('acme.map', [2 => 'a', 5 => 'b']); + Assert::contains("\t\t\t'map' => [\n\t\t\t\t2 => 'a',\n\t\t\t\t5 => 'b',\n\t\t\t],\n", $translation->toPhp()); +}); + + +test('two foreign rules writing names merge into the decisions of the qualification they mean together', function () { + $translation = (new Translator)->translate(['global_namespace_import' => true, 'native_function_invocation' => true]); + Assert::equal([ + 'globalClass' => 'imported', + 'globalFunction' => 'backslashed', + 'globalConstant' => 'backslashed', // the fixer imports no constant by default + 'optimizedFunction' => 'backslashed', + ], $translation->qualification['shape']); + Assert::equal(['optimizedFunction' => 'qualified', 'function' => 'bare'], $translation->qualification['fallback']); + Assert::same([], $translation->decisions); + + Assert::same( + " [\n" + . "\t\t\t'class' => [\n" + . "\t\t\t\t'global' => 'imported',\n" + . "\t\t\t],\n" + . "\t\t\t'function' => [\n" + . "\t\t\t\t'global' => 'bare',\n" + . "\t\t\t\t'optimized' => 'backslashed',\n" + . "\t\t\t],\n" + . "\t\t\t'constant' => [\n" + . "\t\t\t\t'global' => ['backslashed', 'bare'],\n" + . "\t\t\t\t'optimized' => ['backslashed', 'bare'],\n" + . "\t\t\t],\n" + . "\t\t],\n" + . "\t],\n" + . ");\n", + $translation->toPhp(), + ); + Assert::contains('qualification.function.global', $translation->getPaths()); + Assert::noError(fn() => resolveTranslation($translation)); +}); + + +test('the casing of the classes and the functions of PHP says the decision covers those of the project too with the types', function () { + $translation = (new Translator)->translate(['class_reference_name_casing' => true, 'native_function_casing' => true]); + Assert::same(['casing.class' => 'declared', 'casing.function' => 'declared'], $translation->decisions); + Assert::same([ + '`class_reference_name_casing` writes the classes PHP declares, while `casing.class` writes those of the project and its packages too where the configuration sets `typeAnalysis`.', + '`native_function_casing` writes the functions PHP declares, while `casing.function` writes those of the project and its packages too where the configuration sets `typeAnalysis`.', + ], $translation->warnings); +}); + + +test('fully_qualified_strict_types writes a class of the namespace of the file bare and one of another namespace imported', function () { + $decisions = resolveTranslation((new Translator)->translate(['fully_qualified_strict_types' => ['import_symbols' => true]]))->decisions; + Assert::same('imported', $decisions['qualification.class.otherNamespace']->value->toWrittenData()); + Assert::same('bare', $decisions['qualification.class.currentNamespace']->value->toWrittenData()); + Assert::same('imported', $decisions['qualification.class.phpdoc']->value->toWrittenData()); + + // a doc comment only where the option leaves it some tags + $translation = (new Translator)->translate(['fully_qualified_strict_types' => ['phpdoc_tags' => []]]); + Assert::false(isset($translation->decisions['qualification.class.phpdoc'])); +}); + + +test('native_function_invocation takes the backslash from a function it does not write it before only with strict', function () { + $qualification = function (array $options): array { + $decisions = resolveTranslation((new Translator)->translate( + ['native_function_invocation' => $options + ['scope' => 'namespaced']], + ))->decisions; + return [ + $decisions['qualification.function.global']->value->toWrittenData(), + $decisions['qualification.function.optimized']->value->toWrittenData(), + ]; + }; + Assert::same(['bare', 'backslashed'], $qualification([])); + Assert::same(['keep', 'backslashed'], $qualification(['strict' => false])); + Assert::same(['backslashed', 'backslashed'], $qualification(['include' => ['@all']])); +}); + + +test('a function or a constant the fixers and the sniffs name one by one follows its group, which the translation says', function () { + $cases = [ + [['native_function_invocation' => ['include' => ['@all'], 'exclude' => ['dump'], 'scope' => 'namespaced']], + ['optimizedFunction' => 'backslashed', 'globalFunction' => 'backslashed'], + ['function' => 'qualified'], + ['`native_function_invocation` names functions one by one (`dump`), which DressCode decides by group; a function named follows `qualification.function.global`, or `qualification.function.optimized` where the compiler optimizes it.']], + [['native_function_invocation' => [ + 'include' => ['@compiler_optimized', 'dump'], + 'exclude' => ['var_dump'], + 'strict' => false, + 'scope' => 'namespaced', + ]], + ['optimizedFunction' => 'backslashed'], + ['optimizedFunction' => 'qualified'], + ['`native_function_invocation` names functions one by one (`dump`, `var_dump`), which DressCode decides by group; a function named follows `qualification.function.global`, or `qualification.function.optimized` where the compiler optimizes it.']], + // fix_built_in asks for the constants PHP declares, qualified where the compiler computes with them, and strict + // takes the backslash from the rest + [['native_constant_invocation' => ['scope' => 'namespaced']], + ['optimizedConstant' => 'backslashed'], + ['optimizedConstant' => 'qualified', 'constant' => 'bare'], + ['`native_constant_invocation` with `fix_built_in` writes the backslash before every constant of PHP; DressCode writes it before those the compiler computes with.']], + [['native_constant_invocation' => [ + 'fix_built_in' => false, + 'include' => ['PHP_EOL'], + 'exclude' => ['DEBUG', 'null'], + 'scope' => 'namespaced', + ]], + ['optimizedConstant' => 'backslashed'], + ['constant' => 'bare'], + ['`native_constant_invocation` names constants one by one (`PHP_EOL`, `DEBUG`), which DressCode decides by group; a constant named follows `qualification.constant.global`, or `qualification.constant.optimized` where the compiler computes with it.']], + // the special functions join an empty include instead of naming every function + [['SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions' => ['includeSpecialFunctions' => true]], + ['optimizedFunction' => 'backslashed'], + ['optimizedFunction' => 'qualified'], + []], + // an include names functions one by one, so nothing is translated but the warning + [['SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions' => ['include' => ['dump'], 'exclude' => ['var_dump']]], + [], + [], + ['`SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions` names functions one by one (`dump`, `var_dump`), which DressCode decides by group; a function named follows `qualification.function.global`, or `qualification.function.optimized` where the compiler optimizes it.']], + [['SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalConstants' => ['include' => ['PHP_EOL'], 'exclude' => ['DEBUG']]], + [], + [], + ['`SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalConstants` names constants one by one (`PHP_EOL`, `DEBUG`), which DressCode decides by group; a constant named follows `qualification.constant.global`.']], + [['SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly' => [ + 'allowFullyQualifiedGlobalFunctions' => true, + 'allowFallbackGlobalConstants' => false, + ]], + [ + 'class' => 'imported', + 'globalClass' => 'imported', + 'function' => 'imported', + 'globalFunction' => 'keep', + 'constant' => 'imported', + 'globalConstant' => 'imported', + ], + ['constant' => 'qualified'], + []], + ]; + foreach ($cases as [$rules, $notation, $fallback, $warnings]) { + $translation = (new Translator)->translate($rules); + Assert::equal($notation, $translation->qualification['shape']); + Assert::equal($fallback, $translation->qualification['fallback']); + Assert::same($warnings, $translation->warnings); + Assert::noError(fn() => resolveTranslation($translation)); + } +}); diff --git a/tests/DressCode/Rules/fuzz-whitespace.phpt b/tests/DressCode/Rules/fuzz-whitespace.phpt index d923da677..cff4690d2 100644 --- a/tests/DressCode/Rules/fuzz-whitespace.phpt +++ b/tests/DressCode/Rules/fuzz-whitespace.phpt @@ -416,7 +416,10 @@ function findFiles(string $dir): array $files = []; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)); foreach ($iterator as $file) { - if (preg_match('~\.(php|phpt|inc)$~', $file->getFilename())) { + if ( + preg_match('~\.(php|phpt|inc)$~', $file->getFilename()) + && !preg_match('~(dresscode:ignoreFile|phpcs:ignoreFile)~', (string) file_get_contents($file->getPathname())) // silenced, it measures nothing + ) { $files[] = str_replace('\\', '/', $file->getPathname()); } } From faad58dcd0b643950f9008c2ec52202ec14f2d4b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 15 Oct 2026 16:30:30 +0200 Subject: [PATCH 09/13] cli: migrate-suppressions --- docs/internals.md | 2 +- readme.md | 5 +- src/Console/Application.php | 1 + .../Commands/MigrateSuppressionsCommand.php | 86 +++++++++++++++ src/Engine/Suppression.php | 2 +- src/Engine/SuppressionMigration.php | 104 ++++++++++++++++++ tests/DressCode/Console/Application.phpt | 46 +++++++- 7 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/Console/Commands/MigrateSuppressionsCommand.php create mode 100644 src/Engine/SuppressionMigration.php diff --git a/docs/internals.md b/docs/internals.md index bd52483e8..77f0674fa 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -201,7 +201,7 @@ Its format follows the extension of its file, so it is written in whichever of t ### Suppression -Suppression is read once from the original comments before any mutation: `dresscode:ignore [paths]` on a line silences that line, a section or a structure standing for every decision under it, on its own line the nearest node starting on the next line, `disable`/`enable` a range, `ignoreFile` everything; what follows ` -- ` says why, as in ESLint and phpcs, and names no rule. The forms of PHP_CodeSniffer (`phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, `phpcs:ignoreFile` and the `@phpcsSuppress` annotation) name the rules of another tool, which `Interop\Translator` translates, one name possibly standing for several decisions, or the decisions of DressCode by their paths; a name no decision stands for is a warning of the run. A project that already marks its intended exceptions with a comment of its own says so in `suppressionComments`, a map of a pattern to the decisions it silences (`'~\bintentionally\s+[!=]=~': expressions.comparison.loose`, which the `nette` preset carries): a comment the pattern matches silences those decisions as `dresscode:ignore` would at its place, on its line or, alone on a line, on the next node; an entry written as `{decisions: …, example: '// @ '}` carries the example of such a comment too, which must match the pattern with a word put for ``, and a layer saying the pattern again replaces the entry, the example with it (`ResolvedConfig::$suppressionExamples`, what the output for an agent offers); a pattern is matched against the text of one comment, so `^` stands at its start (`'~^//\s*@\s+\S~'`), and where a configuration has any, every file has its comments walked. +Suppression is read once from the original comments before any mutation: `dresscode:ignore [paths]` on a line silences that line, a section or a structure standing for every decision under it, on its own line the nearest node starting on the next line, `disable`/`enable` a range, `ignoreFile` everything; what follows ` -- ` says why, as in ESLint and phpcs, and names no rule. The forms of PHP_CodeSniffer (`phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, `phpcs:ignoreFile` and the `@phpcsSuppress` annotation) name the rules of another tool, which `Interop\Translator` translates, one name possibly standing for several decisions, or the decisions of DressCode by their paths; a name no decision stands for is a warning of the run. `dresscode migrate-suppressions` rewrites them to the dresscode form with the paths, the annotation keeping its tag, which has no form of ours, and lists the names no decision covers. A project that already marks its intended exceptions with a comment of its own says so in `suppressionComments`, a map of a pattern to the decisions it silences (`'~\bintentionally\s+[!=]=~': expressions.comparison.loose`, which the `nette` preset carries): a comment the pattern matches silences those decisions as `dresscode:ignore` would at its place, on its line or, alone on a line, on the next node; an entry written as `{decisions: …, example: '// @ '}` carries the example of such a comment too, which must match the pattern with a word put for ``, and a layer saying the pattern again replaces the entry, the example with it (`ResolvedConfig::$suppressionExamples`, what the output for an agent offers); a pattern is matched against the text of one comment, so `^` stands at its start (`'~^//\s*@\s+\S~'`), and where a configuration has any, every file has its comments walked. ### Processing a file diff --git a/readme.md b/readme.md index 08d312a6e..4b4c7d0f3 100644 --- a/readme.md +++ b/readme.md @@ -472,7 +472,7 @@ Migrating from PHP CS Fixer, PHP_CodeSniffer and Laravel Pint ============================================================= DressCode knows not only its own decisions, but also the names their rules have in PHP CS Fixer, -PHP_CodeSniffer and Slevomat. So the move takes two steps, and you can stop after the first. +PHP_CodeSniffer and Slevomat. So the move takes three steps, and you can stop after any of them. First, leave the code as it is. Comments `// phpcs:ignore`, `phpcs:disable`, `phpcs:enable`, `phpcs:ignoreFile` and the annotation `@phpcsSuppress` keep working: DressCode translates the foreign rule @@ -494,6 +494,9 @@ Read 4 rules; set 4 decisions and 1 preset. `.php-cs-fixer.dist.php` is translated the same way, as long as PHP CS Fixer is still installed in the project, because that file is PHP which has to run. +Third, rewrite the comments when you like: `dresscode migrate-suppressions src tests` turns +`phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses` into `dresscode:ignore imports.unused`. + **Laravel Pint** runs PHP CS Fixer, with a command line of its own and presets of its own, `laravel` the default one. A project on that preset has nothing to translate: DressCode has the style as the standard `laravel`, and `dresscode init` in a project with Pint writes the standard of its preset itself. Over 1,615 diff --git a/src/Console/Application.php b/src/Console/Application.php index 3e3f95b69..6c2cfba00 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -139,6 +139,7 @@ private function defineCommandLine(): Command new Commands\CatalogueCommand($context), new Commands\InitCommand($context), new Commands\ImportCommand($context), + new Commands\MigrateSuppressionsCommand($context), ] as $handlers) { $program->addCommandsFrom($handlers); } diff --git a/src/Console/Commands/MigrateSuppressionsCommand.php b/src/Console/Commands/MigrateSuppressionsCommand.php new file mode 100644 index 000000000..619ec90e3 --- /dev/null +++ b/src/Console/Commands/MigrateSuppressionsCommand.php @@ -0,0 +1,86 @@ + $paths + */ + #[Cli\Command('Rewrite the suppression comments of PHP_CodeSniffer to the DressCode form')] + public function migrateSuppressions( + ParseResult $args, + #[Cli\Argument('Files or directories; the configured paths when omitted', optional: true, repeatable: true)] + array $paths, + ConfigOptions $configuration, + ): int + { + [$factory, $resolution] = $this->context->resolveProject($args, $configuration); + $config = $resolution->config; + $runner = $factory->createRunner($resolution, baseline: false); + $named = array_map($this->context->resolvePath(...), $paths); + $paths = $named ? $runner->narrowPaths($named, $config->paths) : $config->paths; + if (!$paths) { + throw new UsageException('No paths given and none configured.', $args->command); + } + + $migration = new SuppressionMigration($factory->registry->expandSuppressedName(...)); + $parser = new Parser; + $files = 0; + foreach ($runner->findFiles($paths) as $path) { + $absolute = $runner->toAbsolute($path); + $code = @file_get_contents($absolute); // @ is escalated to exception + if ($code === false) { + throw new \RuntimeException("Cannot read file `$path`."); + } + + try { + $file = $parser->parse($code); + } catch (ParseException $e) { + $this->context->write("`$path`: skipped, {$e->getMessage()}\n"); + continue; + } + + if ($migration->migrate($file)) { + if (!Helpers::writeFile($absolute, Printer::print($file))) { + throw new \RuntimeException("Cannot write file `$path`."); + } + + $files++; + } + } + + $this->context->write('Migrated ' . Markup::formatCount($migration->count, 'suppression comment') . ' in ' . Markup::formatCount($files, 'file') . ".\n"); + if ($migration->unknownNames) { + $this->context->write('Warning: The names DressCode does not know are kept as they are: `' . implode('`, `', array_keys($migration->unknownNames)) . "`.\n"); + } + + if ($migration->ownLineIgnore) { + $this->context->write("Note: A `dresscode:ignore` on a line of its own covers the whole statement below it, not just the next line; review the migrated ones.\n"); + } + + return 0; + } +} diff --git a/src/Engine/Suppression.php b/src/Engine/Suppression.php index 955a62dc4..824bf48cd 100644 --- a/src/Engine/Suppression.php +++ b/src/Engine/Suppression.php @@ -15,7 +15,7 @@ /** * Which rules are silenced on which original lines, read once from the comments of the file before any mutation: * the dresscode:ignore, disable, enable and ignoreFile comments, and the forms of phpcs, whose names Interop - * translates, or which name ours. What follows ` -- ` says why, for the reader alone. + * translates, or which name ours, as the migration writes them. What follows ` -- ` says why, for the reader alone. * @internal */ final class Suppression diff --git a/src/Engine/SuppressionMigration.php b/src/Engine/SuppressionMigration.php new file mode 100644 index 000000000..7a9e284f9 --- /dev/null +++ b/src/Engine/SuppressionMigration.php @@ -0,0 +1,104 @@ + */ + public private(set) array $unknownNames = []; + + /** an ignore on a line of its own was migrated; its scope grows from the next line to the whole next statement */ + public private(set) bool $ownLineIgnore = false; + + + public function __construct( + /** @var \Closure(string): list */ + private readonly \Closure $expandName, + ) { + } + + + /** Whether the file changed. */ + public function migrate(FileNode $file): bool + { + $changed = false; + foreach ($file->getTokens() as $token) { + // whether a comment has a line of its own is read before the first of them is rewritten + $comments = []; + foreach ([$token->leadingTrivia, $token->trailingTrivia] as $trivias) { + foreach ($trivias as $index => $trivia) { + if ($trivia->isComment()) { + $comments[] = [$trivia, Suppression::isAlone($trivias, $index, $token)]; + } + } + } + + foreach ($comments as [$trivia, $ownLine]) { + $text = $this->rewrite($trivia->text, $ownLine); + if ($text !== $trivia->text) { + $token->replaceTrivia($trivia, $trivia->withText($text)); + $changed = true; + } + } + } + + return $changed; + } + + + private function rewrite(string $text, bool $ownLine): string + { + $text = (string) preg_replace_callback( + '~phpcs:(ignoreFile|ignore|disable|enable)((?:[ \t]+[\w/][\w/.-]*(?:[ \t]*,[ \t]*[\w/][\w/.-]*)*)?)~', + function (array $m) use ($ownLine): string { + $this->count++; + $kind = $m[1]; + $this->ownLineIgnore = $this->ownLineIgnore || ($kind === 'ignore' && $ownLine); + $names = preg_split('~[,\s]+~', trim($m[2]), -1, PREG_SPLIT_NO_EMPTY) ?: []; + return "dresscode:$kind" . ($names ? ' ' . implode(', ', array_map($this->getCanonicalName(...), $names)) : ''); + }, + $text, + ); + return (string) preg_replace_callback( + '~@phpcsSuppress[ \t]+([\w/][\w/.-]*)~', + function (array $m): string { + $name = $this->getCanonicalName($m[1]); + if ($name !== $m[1]) { + $this->count++; + } + + return "@phpcsSuppress $name"; + }, + $text, + ); + } + + + private function getCanonicalName(string $name): string + { + $resolved = ($this->expandName)($name); + if (!$resolved) { + $this->unknownNames[$name] = true; + return $name; + } + + return implode(', ', array_map(PluginRegistry::abbreviate(...), $resolved)); + } +} diff --git a/tests/DressCode/Console/Application.phpt b/tests/DressCode/Console/Application.phpt index 30024b6c7..3faf7f730 100644 --- a/tests/DressCode/Console/Application.phpt +++ b/tests/DressCode/Console/Application.phpt @@ -872,8 +872,9 @@ test('a baseline is generated by the baseline command over what a fix leaves and Assert::match("Error: %a%`$option`%a%\n%A%", $err); } - // a baseline that cannot be read is replaced + // a baseline that cannot be read is replaced, and it is no obstacle to the migration of the comments file_put_contents("$root/baseline.neon", "a: [b\n"); + Assert::same(0, runApp($root, ['migrate-suppressions', '--config', "$root/baseline.php"])[0]); [$code, $out, $err] = runApp($root, ['baseline', '--config', "$root/baseline.php", '--format', 'json']); Assert::same(0, $code); Assert::same('', $out); // a machine-readable format keeps its stream to itself @@ -1074,6 +1075,49 @@ test('a baseline generated by workers is the one the single process writes, and }); +test('migrate-suppressions rewrites phpcs comments to the dresscode form', function () { + $root = createConsoleProject(); + file_put_contents( + "$root/src/s.php", + " Date: Fri, 16 Oct 2026 06:19:38 +0200 Subject: [PATCH 10/13] docs: the procedure an agent writes a rule or ports one from another tool by --- AGENTS.md | 2 +- docs/skills/writing-rules.md | 119 +++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 docs/skills/writing-rules.md diff --git a/AGENTS.md b/AGENTS.md index fcf1d6ea6..3e2d49ec3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ It is my fervent wish that this file guide every AI coding agent working with co ## Documentation -`docs/internals.md` is the source of truth for how DressCode works: the tree as a rule sees it, the mutation API, the engine of gaps, the rules and the interop. Read it before any non-trivial change. +`docs/internals.md` is the source of truth for how DressCode works: the tree as a rule sees it, the mutation API, the engine of gaps, the rules and the interop. Read it before any non-trivial change. `docs/skills/writing-rules.md` is the procedure of writing a rule or porting one from another tool, the one an agent outside this repository follows too. ## Project overview diff --git a/docs/skills/writing-rules.md b/docs/skills/writing-rules.md new file mode 100644 index 000000000..d0404f957 --- /dev/null +++ b/docs/skills/writing-rules.md @@ -0,0 +1,119 @@ +# Writing a rule, or porting one from another tool + +A procedure for an agent that writes a rule for DressCode, new or ported from a sniff of PHP_CodeSniffer or a fixer +of PHP CS Fixer. It decides and orders; what a rule may and must do is the contract the manual states once, and this +guide links to it rather than repeat it: + +- [custom-rule](https://dresscode.run/custom-rule), a rule from its fixture to a project that uses it; +- [rule-contract](https://dresscode.run/rule-contract), the contract: decisions, values, report before mutation, + risk, analyses; +- [whitespace-rules](https://dresscode.run/whitespace-rules), a rule of the whitespace, which claims and does not + write; +- [testing-rules](https://dresscode.run/testing-rules), fixtures and `RuleTester`; +- [porting-rules](https://dresscode.run/porting-rules), the specification taken from the tests of the foreign rule. + +The engine itself is `docs/internals.md`; read the part a rule touches before writing one. + + +## 0. Is there anything to write? + +`dresscode catalogue --format json` lists for every decision the foreign names it covers (`covers`). A sniff or +a fixer named there is covered: write nothing, translate the configuration with `dresscode import`. + + +## 1. The fixture first + +Write `fixtures//basic.code` before any code (`tests/DressCode/Rules/fixtures//` for a rule of the core): +the code before the fix, every case the foreign rule's tests have, and the ones a tree sees that a token stream does not +(a comment inside the construct, the construct over several lines, the alternative syntax, markup around it). Then +`basic.expected` (omit it where the fix changes nothing) and `basic.violations`, `line: message` per report, two +reports on one line being two lines. `values.neon` beside them gives the decision its value, written as a configuration +nests it (`project:` and under it `key: required`), or the rule does nothing; a comment opening a fixture can say more +(`// risky` for a run that makes risky fixes, `// php 8.4`, `// lineLength 80`). The slug is the class without `Rule`, +its first letter in lower case. + + +## 2. Choose the shape + +Ask in this order and stop at the first yes: + +1. **Is it only the whitespace between two tokens** (a space, a line break, blank lines)? A `GapRule`, in + `Stage::Formatting`, made of claims alone: `getClaims()` returns node class => slot => `[before, after]`, each a + `Claim` or null. It never writes a token. Archetype: `src/Rules/ControlFlow/SwitchCaseSpacingRule.php`. +2. **Is it a comment or a doc comment?** A `NodeRule` visiting `Token::class`, in `Stage::Finishing`, with + `modifiesComments: true` where it changes one. Archetype: `src/Rules/Comments/NoHashCommentsRule.php`. +3. **Does it rewrite code?** A `NodeRule` in `Stage::Structure` visiting the node classes it rewrites. Archetype: + `src/Rules/Expressions/NoDoubleNegationsRule.php`. +4. **Does it only report?** The same, every report with `fixable: false`, and its description says "which is only + reported". Archetype: `src/Rules/Variables/NoGlobalStatementsRule.php`. + +A rule that would both claim whitespace and change tokens is two rules. + + +## 3. Name the decision + +A user never names a rule, only the decision it enforces. In a project it is `project.`, in a plugin +`
.`; the key names the thing in the code and its value says how it is written (`required`, +`forbidden`, two shapes by their examples). A foreign option that chose between two forms is the value, one that +turned the rule on and off is `keep` against the value, a list of exceptions is a parameter. An option that only +worked around a weakness of tokens is dropped. The vocabulary is in `AGENTS.md` (Conventions) and on the page +[extending](https://dresscode.run/extending) of the manual. + + +## 4. Write the rule in this order + +1. `getDecisions()`: `new Decision($path, $domain, $description)`, the domain one of `DressCode\Domains\*` or + `Domain::state('forbidden')` and `Domain::state('required')` for a thing that must not or must be there; the + description English, capitalized, no trailing period. +2. `getVisitedNodes()`, or `getClaims()` for a gap rule. The node classes are in `PhpSyntax\Nodes`, `Nodes\Expression`, + `Nodes\Statement`, `Nodes\Member` and `Nodes\Scalar` of the PhpSyntax package, each with its slots in its `Slots` + constant, and the page `nodes` of its manual describes them. What a foreign rule tells by the next token is often + the class of the node: `static::A`, `static::a()` and `static::$a` are `ClassConstantFetchNode`, + `StaticMethodCallNode` and `StaticPropertyFetchNode`, so a foreign loop over tokens is a question about a class or + a slot. +3. `enter()`: one `if` of questions to the tree, shortest first, ending with `!$context->report($at, $message)`, + which returns false where a comment silenced the place or the run does not take the fix. `$at` is the node or token + the problem is at: its line is the line of the report, which a `dresscode:ignore` and the baseline match. +4. Ask whether the fix changes what the code does where the file does not show it: a child class that declares the + constant again (`static::A` is not `self::A` there), a closure bound later, a function of the namespace. Where it + may, report with `risk: Risk::BehaviorChanges` (or the `Risk` that would decide it) and a `because:`; where the tree + tells it cannot (a final class), the fix is safe. A foreign rule that called its fix safe is no answer. +5. The fix after the condition, never before it: a slot assigned, `replaceWith()`, `setValue()`, a `Builder` for new + code. Writing `$name->text` or `setValue()` keeps the comments around the node; a node put into a slot is taken + with `withoutEdgeTrivia()`. +6. `configure(Values $values)` reads the values once, where the rule has more than its one requirement. + +Every line answers a question about the code. A line asking what may stand between two tokens is a translated line, +not a ported one: delete it. A rule much over a hundred lines does several things or still thinks in tokens. + + +## 5. Run it + +```php +Assert::noError(fn() => DressCode\Testing\RuleTester::run(AcmeRule::class, __DIR__ . '/fixtures/acme')); +``` + +`RuleTester::run()` throws `DressCode\Testing\TestFailure` with a diff where a fixture fails. It compares the output and +the reports and checks the contract: the output is a fixpoint, no comment is +lost, `dresscode:ignoreFile` is honoured, nothing changed without a report. Then PHPStan at level 8. Read a failure by +its symptom: + +| what you see | what is wrong | +|---|---| +| `It changed the file although report() returned false` | the fix stands outside the condition ending with `report()` | +| `It changed the file after a report with fixable: false` | a report-only occurrence is fixed after all | +| `It reported an occurrence it then left unfixed without saying fixable: false` | the rule reports what it cannot fix | +| `It asks for analysis X, which it does not name` | `analyses: [X::class]` missing in `#[RuleInfo]`, a helper's included | +| `does not converge` | the rule rewrites its own output: it is not idempotent | +| the output loses a space or a line ending | a node written into a slot kept the trivia of its edges: take it with `withoutEdgeTrivia()` | +| `The node does not belong to the file of the analysis` | the rule removed or replaced an ancestor of the node it was given; visit the ancestor instead | + + +## 6. Porting: what does not carry over + +- **The code.** Only the behaviour and the test cases; mark fixtures taken from a foreign project with their origin and + licence in their header. +- **The foreign name.** `phpcs:ignore Acme.Sniff.Name` silences a decision of the core, never a rule of a plugin; the + user rewrites such comments with the path of the new decision, which `dresscode migrate-suppressions` lists. +- **A risky fix of the foreign rule.** Asked of the tree (`ExpressionNode::isRepeatableRead()`, the types), the fix + may be safe; where the code cannot tell, it is reported with `risk:` and the `Risk` that would decide it. From b05db6c40ca3c028c44de16c3700f2fe17c824ac Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 19 Oct 2026 07:47:36 +0200 Subject: [PATCH 11/13] cli: the hidden option --profile writes where the time of a run went, the workers included `--profile ` writes as JSON where the time of a run went, in nanoseconds: the phases (the start of the process, the start of the container of PHPStan or of a derived one and its scopes, the parse, the passes, the gaps, the print, the cycle collection, the run as a whole), every rule by the time of its callbacks, how often they ran and how often they changed the tree, every gap rule by the time of its claims, every file with its size, rounds and passes, and the peak memory of every process. A worker measures its own files and hands that over with every result and the parent adds it up, so the totals do not depend on the number of workers. A run with the option leaves the result cache out, a file served from the cache having nothing to measure. It says where the time of a run goes, not only how much of it: a comparison of two revisions then names the rule or the claim that moved. Without the option the engine only asks whether there is a profiler, and wraps a claim in a measurement only when there is one. --- docs/internals.md | 4 +- src/Analyses/PhpStan.php | 11 +- src/Config/RunnerFactory.php | 8 +- src/Console/Commands/CheckCommand.php | 27 +++- src/Console/Commands/RunOptions.php | 3 + src/Engine/CycleCollector.php | 7 +- src/Engine/Gaps/Claims.php | 23 +++ src/Engine/PassLoop.php | 12 ++ src/Engine/PhpFileProcessor.php | 39 ++++- src/Engine/Profiler.php | 146 ++++++++++++++++++ src/Engine/RulePlan.php | 3 +- src/Engine/Runner.php | 4 +- src/Engine/Worker.php | 11 +- src/Engine/WorkerCodec.php | 19 ++- src/Engine/WorkerPool.php | 47 +++++- tests/DressCode/Console/Application.phpt | 55 +++++++ tests/DressCode/Engine/Profiler.phpt | 53 +++++++ tests/DressCode/Engine/WorkerCodec.phpt | 22 +-- .../DressCode/Engine/fixtures/fake-worker.php | 1 + 19 files changed, 455 insertions(+), 40 deletions(-) create mode 100644 src/Engine/Profiler.php create mode 100644 tests/DressCode/Engine/Profiler.phpt diff --git a/docs/internals.md b/docs/internals.md index 77f0674fa..0c523693a 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -221,12 +221,14 @@ A rule exception or a cycle fails that one file (nothing is written, the run goe A `ResultCache` (one JSON file per project root in `cacheDir` or the system temp) remembers every content that came out clean under the effective configuration: the rules and the values of the decisions, the selection of `only`, style, PHP version, the same of every override resolved on its own, so that a preset only an override uses counts too, the installed version of each package of the project with the reference of the source it came from (`ProjectPackages::getIdentity()`), the same of each package of the running process, which is where the tool, its PHPStan and the extensions of it come from wherever they are not installed in the project, its root and the roots inside a phar left out (`RunnerFactory::getProcessIdentity()`), the times and sizes of the files of the rule, preset and analysis classes neither a package of the process nor a phar holds, which a rule of the project itself changes under the same version (a process without Composer knows no package, so every file outside a phar counts), and so of every PHP and NEON file of the source trees of DressCode and PhpSyntax, whose helpers and data the rules stand on, where they are a working tree rather than a package, and the text of the configuration file, hashed by `RunnerFactory::hashConfiguration()`. That text is all that describes a rule or an analysis a closure builds, so such a closure must build from nothing but what the text says, and a run with a closure and no file is not cached. A content is remembered by its path together with its text, because an override or a rule reading the path gives the same text in two files two verdicts; `Runner::run()` skips such a file with a `cached` result, its text read again when it is reported and processed after all where it is no longer the text the cache knew, and, after a fix whose text has nothing remaining, remembers the written content too, unless there is a baseline, whose entries no run has yet held against the fixed lines. With the types a result also depends on what other files of the project declare and on the `phpstan.neon` of the project, so the identity then holds the time and size of every file under the analysed paths, the configuration of PHPStan and `installed.json` (`RunnerFactory::fingerprintProject()`): a change anywhere throws the cache away, which is the price of a clean verdict that stays true after a pull or a rebase. What that configuration includes, its stubs and bootstrap files outside those paths, only the container of PHPStan knows, and a run served whole by the cache builds none, so the run that builds one records them with their stamps in the cache (`PhpStan::getConfigurationFiles()`, a worker sending them with its first result after it knows them, `ResultCache::recordDependencies()`), and the next run drops the cache where any of them changed; those of PHPStan itself and those in its temporary directory are left out. A file modified in the second before the run began or later, where a change in the same second would leave its time and size as they were, adds the hash of its text, as git does with the files it calls racy, which holds for the files of the rules too; a file restored with an older time and the same size is the one limit left. `--no-cache` serves no file from it but remembers what the run finds, forgetting a content it found not clean; `processCode()` (stdin) never uses it. -With `--jobs` above 1 (by default the processor count, capped at one worker per four files, because a worker costs about that much to start; an explicit `--jobs` is taken as given) the files not served by the cache go to a `WorkerPool`. Worker processes are started with the same command line plus a hidden `--worker
`, under the `php.ini` of the parent (`-c`, or `-n` where it loaded none) and with the settings the parent has beyond it, those of `-d` among them, which PHP tells no process, so the parent asks the same PHP for its settings and passes on the difference as `-d`, and connect to a TCP server on the loopback: `stream_select()` handles no pipes on Windows before PHP 8.5, and where it does, a message over a pipe costs there hundreds of times what it costs over the loopback, so the server stays whatever the minimum version. With the types, a worker fills the cache of PHPStan before it connects (`PhpStan::warmUp()`), and the pool starts the others only once the first has connected: workers started together would all write the same cold cache, which on Windows makes every write slow and fails the renames over a file another worker holds. A worker that has not connected within the connect timeout of the pool fails the run, which with the types bounds that warm-up. A worker receives one path at a time and answers with the `FileResult` as a line of JSON (`Engine\WorkerCodec`), the path both ways and the output base64-encoded, since neither need be UTF-8. The parent reads the connections without blocking and puts a line together from its parts, because waiting for the rest of one would stop it from hearing the other workers and from noticing a stuck one; it takes an answer only about the path it gave that worker, and a worker spending longer than the timeout of the pool on one file fails the run, since a rule has most likely looped. The listening socket is closed as soon as the last worker is in, because on Windows closing it later costs every run a wait, and it comes from ext/sockets where there is one, because closing one of the stream layer there waits half a second. +With `--jobs` above 1 (by default the processor count, capped at one worker per four files, because a worker costs about that much to start; an explicit `--jobs` is taken as given) the files not served by the cache go to a `WorkerPool`. Worker processes are started with the same command line plus a hidden `--worker
`, under the `php.ini` of the parent (`-c`, or `-n` where it loaded none) and with the settings the parent has beyond it, those of `-d` among them, which PHP tells no process, so the parent asks the same PHP for its settings and passes on the difference as `-d`, and connect to a TCP server on the loopback: `stream_select()` handles no pipes on Windows before PHP 8.5, and where it does, a message over a pipe costs there hundreds of times what it costs over the loopback, so the server stays whatever the minimum version. With the types, a worker fills the cache of PHPStan before it connects (`PhpStan::warmUp()`), and the pool starts the others only once the first has connected: workers started together would all write the same cold cache, which on Windows makes every write slow and fails the renames over a file another worker holds. A worker that has not connected within the connect timeout of the pool fails the run, which with the types bounds that warm-up. A worker receives one path at a time and answers with the `FileResult` and what it measured for `--profile` as a line of JSON (`Engine\WorkerCodec`), the path both ways and the output base64-encoded, since neither need be UTF-8. The parent reads the connections without blocking and puts a line together from its parts, because waiting for the rest of one would stop it from hearing the other workers and from noticing a stuck one; it takes an answer only about the path it gave that worker, and a worker spending longer than the timeout of the pool on one file fails the run, since a rule has most likely looped. The listening socket is closed as soon as the last worker is in, because on Windows closing it later costs every run a wait, and it comes from ext/sockets where there is one, because closing one of the stream layer there waits half a second. A process working through files, the parent without workers and every worker, keeps the cycle collector of PHP off and collects between two files once the memory has doubled since what the last collection left, never above three quarters of `memory_limit` (`Engine\CycleCollector`). The collector PHP runs by itself scans again and again what stays alive, the tree being processed and above all the reflection of PHPStan, and frees little: with the types it took a third of the run. A worker only processes and, when fixing, writes; the parent keeps the cache and the baseline, reads a file again when it hands it out, reports every file in the order of the input as soon as the files before it are done and keeps of its result what the run needs without the code and the output (`Engine\FileSummary`), so that only the files in progress and those waiting for an earlier one are held as text, and turns a worker that dies or exits with an error into a failure of the run. +The hidden option `--profile ` of `check`, `fix` and `baseline` writes where the time of the run went as JSON (`Engine\Profiler`): the phases of processing a file (parse, passes, print, each round counted, and the gaps the engine decides along the traversal, the claims of the rules included), the callbacks of every rule with the calls that changed the tree, the time of the closures of every gap rule's claims, the start of PHPStan, its derived containers and scopes, the collections of cycles, the fifty slowest files with the phases of each, the size and the time of every file, and the peak memory of every process. A worker hands over what it measured with every file, beside its result, and, once nothing is left, what it measured since in an answer to `finish`, which the pool waits for from every worker, one that started too late to get a file among them; the parent adds it up, so the counts do not depend on `--jobs`; the result cache is off, since a file it serves has nothing to measure. Off, the profiler costs a null check around each callback of a rule; on, it costs the clock read twice around each, so its times say where the time goes and the times of a run without it how much there is. + ## Configuration ### The file diff --git a/src/Analyses/PhpStan.php b/src/Analyses/PhpStan.php index 618a27fda..cf4cff369 100644 --- a/src/Analyses/PhpStan.php +++ b/src/Analyses/PhpStan.php @@ -10,7 +10,7 @@ use Composer\Autoload\ClassLoader; use Composer\InstalledVersions; use DressCode\Config\ComposerJson; -use DressCode\Engine\Helpers; +use DressCode\Engine\{Helpers, Profiler}; use DressCode\Violation; use Nette\Utils\FileSystem; use PhpParser\{Node, NodeFinder}; @@ -73,6 +73,7 @@ public function __construct( private readonly string $tempDir, /** the version of PHP the code is written for, which PHPStan parses it as where the running PHP is older */ private readonly ?string $phpVersion = null, + private readonly ?Profiler $profiler = null, /** * @var list the directories of the packages installed in a newer version than the oldest one the project * supports, whose declarations may say what that one does not @@ -239,6 +240,7 @@ public function resolveScopes(string $path, array $ast, callable $callback): voi $resolver = $container->getByType(NodeScopeResolver::class); $resolver->setAnalysedFiles([$path]); $scope = $container->getByType(ScopeFactory::class)->create(ScopeContext::create($path)); + $start = $this->profiler ? hrtime(as_number: true) : 0; $inCallback = false; // a warning of PHPStan is the first sign of its failure on the file, and printed it would spoil the output; one it // silences is not, a rename into its cache another process has just written being one @@ -262,6 +264,8 @@ public function resolveScopes(string $path, array $ast, callable $callback): voi } finally { restore_error_handler(); } + + $this->profiler?->addPhase('phpstan: scopes', hrtime(as_number: true) - $start); } @@ -499,6 +503,7 @@ private function getContainer(): Container if (ini_get('opcache.file_cache_only')) { ini_set('opcache.enable', '0'); } + $start = $this->profiler ? hrtime(as_number: true) : 0; try { $container = new ContainerFactory($this->root)->create( $this->tempDir, @@ -519,6 +524,10 @@ private function getContainer(): Container ); } + $this->profiler?->addPhase( + $this->replacement === null ? 'phpstan: container' : 'phpstan: derived container build', + hrtime(as_number: true) - $start, + ); $this->configurationFiles ??= $this->collectConfigurationFiles($container); return $this->container = $container; } diff --git a/src/Config/RunnerFactory.php b/src/Config/RunnerFactory.php index 2af5fa990..5fcab98f5 100644 --- a/src/Config/RunnerFactory.php +++ b/src/Config/RunnerFactory.php @@ -10,7 +10,7 @@ use Composer\InstalledVersions; use DressCode\{Analyses, Config, ConfigurationException, Override, Plugin, PluginManifest, Profile, Rule}; use DressCode\Engine\{ - Baseline, FileProcessor, FileProcessors, Helpers, PhpFileProcessor, ReportPolicy, ResultCache, Runner, TypeAnalysisStatus, + Baseline, FileProcessor, FileProcessors, Helpers, PhpFileProcessor, Profiler, ReportPolicy, ResultCache, Runner, TypeAnalysisStatus, }; use Nette\Utils\Finder; use PhpSyntax\Node; @@ -129,6 +129,7 @@ public function createRunner( ?string $configFile = null, bool $fixRisky = false, bool $baseline = true, + ?Profiler $profiler = null, bool $refreshCache = false, array $leftAsWritten = [], ): Runner @@ -150,6 +151,7 @@ public function createRunner( self::resolveAnalysedPaths($config, $root), self::resolveCacheDir($config, $root) . '/phpstan', $phpVersion, + $profiler, $resolution->projectPackages->findNewerInstalledPaths(), $resolution->projectPackages->findUnboundedInstalledPaths(), self::findForeignPaths($root), @@ -163,7 +165,7 @@ public function createRunner( $processors = new FileProcessors( array_map(fn(Override $override) => $override->paths, $config->overrides), function (array $overrides) use ( - $resolution, $registry, $analyses, $strict, $baselineFile, $fixRisky, $leftAsWritten, + $resolution, $registry, $analyses, $strict, $baselineFile, $fixRisky, $profiler, $leftAsWritten, ): FileProcessor { $variant = $resolution->resolveFor($overrides); $style = $variant->createStyle(); @@ -188,6 +190,7 @@ function (array $overrides) use ( leftAsWritten: array_fill_keys($leftAsWritten, true), strict: $strict, ), + profiler: $profiler, gates: $variant->getGates(), ); }, @@ -228,6 +231,7 @@ function (array $overrides) use ( $baselineFile, $resultCache, narrowed: (bool) $resolution->only, + profiler: $profiler, warmUp: isset($phpstan) ? $phpstan->warmUp(...) : null, findDependencies: isset($phpstan) ? $phpstan->getConfigurationFiles(...) : null, typeAnalysis: match (true) { diff --git a/src/Console/Commands/CheckCommand.php b/src/Console/Commands/CheckCommand.php index 2bda75b14..95a9aa850 100644 --- a/src/Console/Commands/CheckCommand.php +++ b/src/Console/Commands/CheckCommand.php @@ -13,10 +13,11 @@ Versions, }; use DressCode\Console\{CommandContext, Markup, ProgressBar, RiskReview}; -use DressCode\Engine\{Baseline, FileSummary, Helpers, LineRanges, Runner, RunResult, Worker, WorkerPool}; +use DressCode\Engine\{Baseline, FileSummary, Helpers, LineRanges, Profiler, Runner, RunResult, Worker, WorkerPool}; use Nette\CommandLine\{Attributes as Cli, Console, ParseResult, UsageException}; use Nette\Utils\FileSystem; use function count, in_array, is_string, sprintf, strlen; +use const JSON_PRETTY_PRINT, JSON_THROW_ON_ERROR, JSON_UNESCAPED_SLASHES; /** @@ -150,6 +151,7 @@ private function run( ? $this->context->resolveProject($args, $configuration, $check->only ?? []) : $this->resolveUpgrade($args, $configuration, $modernize, $upgrade); [$config, $root] = [$resolution->config, $resolution->root]; + $profiler = $run->profile === null ? null : new Profiler; $fixRisky = in_array(true, $check->fixRisky ?? [], true); // a bare `--fix-risky`, the names given go to the configuration if ($run->worker !== null) { // the parent keeps the cache; the baseline decides what is reported $runner = $factory->createRunner( @@ -158,18 +160,20 @@ private function run( cache: false, fixRisky: $fixRisky, baseline: !$generate, + profiler: $profiler, ); ($runner->warmUp)?->__invoke(); // before connecting, which is what starts the other workers - return Worker::run($run->worker, $runner, $fix); + return Worker::run($run->worker, $runner, $fix, $profiler); } $options = [ 'strict' => (bool) $run->strictRules, - // the counts of a file served from the cache are those the baseline left - 'cache' => !$generate, + // a file served from the cache has nothing to measure, and its counts are those the baseline left + 'cache' => !$generate && !$profiler, 'configFile' => $configFile, 'fixRisky' => $fixRisky, 'baseline' => !$generate, + 'profiler' => $profiler, 'refreshCache' => (bool) $run->noCache, ]; $runner = $factory->createRunner($resolution, ...$options); @@ -292,13 +296,15 @@ private function run( ]) : null; $workers = $command - ? new WorkerPool($command, $jobs, $this->context->workingDirectory, warmFirst: $runner->warmUp !== null) + ? new WorkerPool($command, $jobs, $this->context->workingDirectory, profiler: $profiler, warmFirst: $runner->warmUp !== null) : null; $progress = !$generate && $format === 'console' && count($files) > 1 && $out->isTerminal() ? new ProgressBar($out, count($files)) : null; $onProgress = $progress === null ? null : $progress->advance(...); + $profiler?->addPhase('start of the process', (int) ((microtime(as_float: true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1e9)); + $start = hrtime(as_number: true); try { if ($generate) { return $this->generateBaseline($runner, $config, $root, $configFile, $files, $workers, $format); @@ -327,6 +333,13 @@ private function run( } finally { $progress?->clear(); // an error must not be written into the bar + if ($profiler && $run->profile !== null) { + $profiler->addPhase('run', hrtime(as_number: true) - $start); + FileSystem::write( + $this->context->resolvePath($run->profile), + json_encode($profiler->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n", + ); + } } } @@ -713,6 +726,10 @@ private function buildWorkerCommand( $command[] = is_string($value) ? "--fix-risky=$value" : '--fix-risky'; } + if ($run->profile !== null) { // a worker measures and hands it over with every file, the parent writes + $command[] = "--profile=$run->profile"; + } + return $command; } diff --git a/src/Console/Commands/RunOptions.php b/src/Console/Commands/RunOptions.php index a4174b174..560525bff 100644 --- a/src/Console/Commands/RunOptions.php +++ b/src/Console/Commands/RunOptions.php @@ -41,6 +41,9 @@ enum: ['console', 'agent', 'lines', 'github', 'json', 'checkstyle'], /** the address of the parent; a worker started by WorkerPool */ #[Cli\Option(hidden: true)] public ?string $worker = null, + /** a file to write where the time of the run went, see Profiler */ + #[Cli\Option(hidden: true)] + public ?string $profile = null, ) { } diff --git a/src/Engine/CycleCollector.php b/src/Engine/CycleCollector.php index bbe3852a1..fdf62b337 100644 --- a/src/Engine/CycleCollector.php +++ b/src/Engine/CycleCollector.php @@ -24,8 +24,9 @@ final class CycleCollector private int $threshold = self::Floor; - public function __construct() - { + public function __construct( + private readonly ?Profiler $profiler = null, + ) { $this->enabled = gc_enabled(); gc_disable(); } @@ -38,7 +39,9 @@ public function collect(): void return; } + $start = $this->profiler ? hrtime(as_number: true) : 0; gc_collect_cycles(); + $this->profiler?->addPhase('gc', hrtime(as_number: true) - $start); $this->threshold = max(self::Floor, min(2 * memory_get_usage(), self::findCeiling())); } diff --git a/src/Engine/Gaps/Claims.php b/src/Engine/Gaps/Claims.php index 0a3064da4..0f02d3c9d 100644 --- a/src/Engine/Gaps/Claims.php +++ b/src/Engine/Gaps/Claims.php @@ -8,6 +8,7 @@ namespace DressCode\Engine\Gaps; use DressCode\{Claim, ConfigurationException, Gap, GapRule, Rule}; +use DressCode\Engine\Profiler; use PhpSyntax\{LayoutData, Node}; use PhpSyntax\Nodes\{PlainNodeList, SeparatedNodeList}; use function sprintf, strlen; @@ -49,6 +50,7 @@ final class Claims */ public function __construct( array $rules, + private readonly ?Profiler $profiler = null, ) { foreach ($rules as $rule) { if (!$rule instanceof GapRule) { @@ -172,10 +174,31 @@ private function claim(array &$side, string $name, string $key, Rule $rule, Clai } } + if ($claim instanceof \Closure && $this->profiler) { + $claim = self::measureClaim($claim, $rule::class, $this->profiler); + } + $side[$key][] = [$rule, $claim, $key]; } + /** + * @param \Closure(Gap): ?Claim $claim + * @return \Closure(Gap): ?Claim + */ + private static function measureClaim(\Closure $claim, string $rule, Profiler $profiler): \Closure + { + return static function (Gap $gap) use ($claim, $rule, $profiler): ?Claim { + $start = hrtime(as_number: true); + try { + return $claim($gap); + } finally { + $profiler->addClaim($rule, hrtime(as_number: true) - $start); + } + }; + } + + /** @throws ConfigurationException */ public static function refuseSecond(Rule $first, Rule $second, string $side, string $key): never { diff --git a/src/Engine/PassLoop.php b/src/Engine/PassLoop.php index f2b9fcec7..b144e5333 100644 --- a/src/Engine/PassLoop.php +++ b/src/Engine/PassLoop.php @@ -76,6 +76,7 @@ public function __construct( private readonly RulePlan $plan, private readonly Analyses\Registry $analyses, private readonly ReportPolicy $policy = new ReportPolicy, + private readonly ?Profiler $profiler = null, ) { $this->gaps = new Resolver($plan->claims); } @@ -139,7 +140,9 @@ public function run(FileNode $file, string $code, string $path, Style $style, st $context->passStorage = []; } + $start = $this->profiler ? hrtime(as_number: true) : 0; $this->analyses->beginPass($file); + $this->profiler?->addPhase('analyses: begin pass', hrtime(as_number: true) - $start); $revision = $file->revision; foreach ($this->plan->stages as $stage => $rules) { $this->runStage($stage, $rules, $style); @@ -158,7 +161,9 @@ public function run(FileNode $file, string $code, string $path, Style $style, st $mutated = true; $mutatedRules += $this->mutatedRules; + $start = $this->profiler ? hrtime(as_number: true) : 0; $output = Printer::print($file); + $this->profiler?->addPhase('print', hrtime(as_number: true) - $start); $hash = hash('xxh3', $output); if (isset($seen[$hash])) { // a state seen before: the rules cycle throw new ConvergenceException($path, array_keys($this->mutatedRules), Diff::unified($last, $output, $path)); @@ -353,8 +358,10 @@ static function (string $class, Rule $rule) use ($analyses, $file, $path, $stric return; } + $start = $this->profiler ? hrtime(as_number: true) : 0; try { $node instanceof Token ? $gaps->enterToken($node) : $gaps->enterNode($node); + $this->profiler?->addPhase('gaps', hrtime(as_number: true) - $start); } catch (ConfigurationException|RuleException $e) { // two claims deciding one gap, or a claim of the rule named failing throw $e; } catch (\Throwable $e) { @@ -394,14 +401,17 @@ private function visit(string $stage, Node|Token $node, bool $enter): void $parent = $node->parent; $file = $this->file; + $profiler = $this->profiler; foreach ($rules as [$rule, $context, $ruleClass]) { $before = $file->revision; + $start = $profiler ? hrtime(as_number: true) : 0; try { $enter ? $rule->enter($node, $context) : $rule->leave($node, $context); } catch (\Throwable $e) { throw new RuleException($ruleClass, $this->path, $e); } + $profiler?->addRule($ruleClass, hrtime(as_number: true) - $start, $file->revision !== $before); if ($file->revision !== $before || $context->hasReports()) { $this->account($ruleClass, $context, $before); } @@ -444,12 +454,14 @@ private function invoke(NodeRule $rule, \Closure $callback): void $ruleClass = $rule::class; $context = $this->contexts[$ruleClass]; $before = $this->file->revision; + $start = $this->profiler ? hrtime(as_number: true) : 0; try { $callback($context); } catch (\Throwable $e) { throw new RuleException($ruleClass, $this->path, $e); } + $this->profiler?->addRule($ruleClass, hrtime(as_number: true) - $start, $this->file->revision !== $before); $this->account($ruleClass, $context, $before); } diff --git a/src/Engine/PhpFileProcessor.php b/src/Engine/PhpFileProcessor.php index c7ddd4c17..c1f5a7e55 100644 --- a/src/Engine/PhpFileProcessor.php +++ b/src/Engine/PhpFileProcessor.php @@ -11,6 +11,7 @@ use PhpSyntax\Nodes\FileNode; use PhpSyntax\Nodes\Statement\{NamespaceNode, UseNode}; use PhpSyntax\{ParseException, Parser, SymbolKind}; +use function strlen; /** @@ -36,11 +37,12 @@ public function __construct( /** the line ending of the style follows the prevailing one of each file */ private bool $detectLineEnding = true, private ReportPolicy $policy = new ReportPolicy, + private ?Profiler $profiler = null, /** @var array the class of a rule => the gate of its reports */ private array $gates = [], ) { $this->parser = new Parser; - $this->plan = new RulePlan($this->rules, $this->gates); + $this->plan = new RulePlan($this->rules, $this->profiler, $this->gates); } @@ -74,15 +76,18 @@ public function process(string $path, string $code, array $acceptedRisks = []): $settled = false; $remaining = null; $removedImports = $mutatedRules = []; + $start = $this->profiler ? hrtime(as_number: true) : 0; + $times = ['parse' => 0, 'passes' => 0]; // a mutated tree is not the tree the parser would build from the printed text, so a rule can miss what another // one has just written; the strict run makes the text settle in rounds, the others take what the last pass // of the tree left for what a round over the text would report, which spares a pass; they only parse the printed // text, so that a broken rule never writes code PHP refuses - $passLoop = new PassLoop($this->plan, $this->analyses, $this->policy); + $passLoop = new PassLoop($this->plan, $this->analyses, $this->policy, $this->profiler); $rounds = 0; while (!$settled) { $rounds++; + $lap = $this->profiler ? hrtime(as_number: true) : 0; try { $file = $this->parser->parse($text); self::checkImports($file); @@ -90,6 +95,11 @@ public function process(string $path, string $code, array $acceptedRisks = []): return self::createSyntaxError($path, $code, $e, $rounds); } + if ($this->profiler) { + $times['parse'] += hrtime(as_number: true) - $lap; + $lap = hrtime(as_number: true); + } + try { $result = $passLoop->run($file, $text, $path, $style, $this->phpVersion, $acceptedRisks); } catch (RuleException $e) { @@ -104,6 +114,10 @@ public function process(string $path, string $code, array $acceptedRisks = []): $passes += $result->passes; array_push($removedImports, ...$result->removedImports); array_push($mutatedRules, ...$result->mutatedRules); + if ($this->profiler) { + $times['passes'] += hrtime(as_number: true) - $lap; + } + $printed = $result->output ?? $text; $settled = $printed === $text; $seen[hash('xxh3', $text)] = true; @@ -114,17 +128,38 @@ public function process(string $path, string $code, array $acceptedRisks = []): $text = $printed; if (!$settled && !$this->policy->strict && $result->remaining !== null) { + $lap = $this->profiler ? hrtime(as_number: true) : 0; try { self::checkImports($this->parser->parse($text)); } catch (ParseException $e) { return self::createParseFailure($path, $code, $e); } + if ($this->profiler) { + $times['parse'] += hrtime(as_number: true) - $lap; + } + $remaining = $result->remaining; + $rounds++; // the parse of the printed text stands for the round over it break; } } + if ($this->profiler) { + foreach ($times as $phase => $time) { + $this->profiler->addPhase($phase, $time, $rounds); + } + + $this->profiler->addFile([ + 'path' => $path, + 'size' => strlen($code), + 'time' => hrtime(as_number: true) - $start, + ...$times, + 'rounds' => $rounds, + 'passCount' => $passes, + ]); + } + // the last round is a check of the text the fix writes; a round after the first parsed that text, not the file as read $remaining ??= $result === $first ? $result->violations diff --git a/src/Engine/Profiler.php b/src/Engine/Profiler.php new file mode 100644 index 000000000..cd0d8883d --- /dev/null +++ b/src/Engine/Profiler.php @@ -0,0 +1,146 @@ +> phase => nanoseconds, calls */ + private array $phases = []; + + /** @var array> rule => nanoseconds, calls, calls that changed the tree */ + private array $rules = []; + + /** @var array> gap rule => nanoseconds, calls of its claims */ + private array $claims = []; + + /** @var list> every file with the phases of its processing */ + private array $files = []; + + /** @var array process id => its peak memory */ + private array $memory = []; + + + public function addPhase(string $phase, int $time, int $calls = 1): void + { + self::add($this->phases, $phase, [$time, $calls]); + } + + + public function addRule(string $rule, int $time, bool $mutated): void + { + self::add($this->rules, $rule, [$time, 1, (int) $mutated]); + } + + + public function addClaim(string $rule, int $time): void + { + self::add($this->claims, $rule, [$time, 1]); + } + + + /** @param array $file path, size, time and the phases of its processing */ + public function addFile(array $file): void + { + $this->files[] = $file; + } + + + /** + * What was measured since the last call, as data for another process, and forgets it. + * @return array + */ + public function takeRecords(): array + { + $records = [ + 'phases' => $this->phases, + 'rules' => $this->rules, + 'claims' => $this->claims, + 'files' => $this->files, + 'memory' => [getmypid() => memory_get_peak_usage()], + ]; + $this->phases = $this->rules = $this->claims = $this->files = []; + return $records; + } + + + /** @param array $records as `takeRecords()` of another process made them */ + public function merge(array $records): void + { + foreach (['phases', 'rules', 'claims'] as $table) { + foreach (is_array($records[$table] ?? null) ? $records[$table] : [] as $name => $values) { + self::add($this->$table, (string) $name, array_values(array_map(intval(...), (array) $values))); + } + } + + foreach (is_array($records['files'] ?? null) ? $records['files'] : [] as $file) { + $this->files[] = (array) $file; + } + + foreach (is_array($records['memory'] ?? null) ? $records['memory'] : [] as $pid => $peak) { + $this->memory[(int) $pid] = max($this->memory[(int) $pid] ?? 0, (int) $peak); + } + } + + + /** + * The profile of the whole run, the most expensive first; every file with its size and time, in the order + * of the paths, for a sample to be drawn from. + * @return array + */ + public function toArray(): array + { + $files = $this->files; + usort($files, fn(array $a, array $b) => $b['time'] <=> $a['time']); + $times = []; + foreach ($files as $file) { + $times[(string) $file['path']] = [(int) $file['size'], (int) $file['time']]; + } + + ksort($times); + $pid = (int) getmypid(); + $this->memory[$pid] = max($this->memory[$pid] ?? 0, memory_get_peak_usage()); + $byTime = static function (array $table, array $names): array { + uasort($table, fn(array $a, array $b) => $b[0] <=> $a[0]); + return array_map(fn(array $values) => array_combine($names, $values), $table); + }; + return [ + 'schema' => self::Schema, + 'files' => count($files), + 'phases' => $byTime($this->phases, ['time', 'calls']), + 'rules' => $byTime($this->rules, ['time', 'calls', 'mutating']), + 'claims' => $byTime($this->claims, ['time', 'calls']), + 'slowestFiles' => array_slice($files, 0, self::SlowestFiles), + 'fileTimes' => $times, + 'peakMemory' => $this->memory, + ]; + } + + + /** + * @param array> $table + * @param list $values + */ + private static function add(array &$table, string $name, array $values): void + { + $table[$name] = isset($table[$name]) + ? array_map(fn(int $a, int $b) => $a + $b, $table[$name], $values) + : $values; + } +} diff --git a/src/Engine/RulePlan.php b/src/Engine/RulePlan.php index ff4791df7..cc613f7e6 100644 --- a/src/Engine/RulePlan.php +++ b/src/Engine/RulePlan.php @@ -48,6 +48,7 @@ final class RulePlan public function __construct( /** @var list in configuration order */ public readonly array $rules, + ?Profiler $profiler = null, array $gates = [], ) { $stages = $leaves = []; @@ -79,7 +80,7 @@ public function __construct( $this->stages = $stages; $this->leaves = $leaves; $this->gates = $gates; - $this->claims = new Claims($rules); + $this->claims = new Claims($rules, $profiler); } diff --git a/src/Engine/Runner.php b/src/Engine/Runner.php index b49fa2b05..6a7ffde64 100644 --- a/src/Engine/Runner.php +++ b/src/Engine/Runner.php @@ -37,6 +37,8 @@ public function __construct( private ?ResultCache $cache = null, /** the run is narrowed to some of the decisions, so it says nothing about the baseline entries of the others */ private bool $narrowed = false, + /** measures the cycle collection of the files processed in this process */ + private ?Profiler $profiler = null, /** @var ?\Closure(): void fills the cache the workers share, which those started together would all write at once */ public ?\Closure $warmUp = null, /** @var ?\Closure(): ?list the files the results depend on beyond the configuration, once the run knows them */ @@ -222,7 +224,7 @@ private function processPending( return; } - $collector = new CycleCollector; + $collector = new CycleCollector($this->profiler); try { foreach ($paths as $path) { if ($onProgress !== null) { diff --git a/src/Engine/Worker.php b/src/Engine/Worker.php index 91f2c443c..2924f2109 100644 --- a/src/Engine/Worker.php +++ b/src/Engine/Worker.php @@ -19,7 +19,7 @@ final class Worker { /** @return int exit code */ - public static function run(string $address, Runner $runner, bool $fix): int + public static function run(string $address, Runner $runner, bool $fix, ?Profiler $profiler = null): int { $context = stream_context_create(['socket' => ['tcp_nodelay' => true]]); // Nagle would delay every small message by an ACK $socket = @stream_socket_client("tcp://$address", $errno, $error, timeout: 10, context: $context); // @ reported below @@ -28,10 +28,15 @@ public static function run(string $address, Runner $runner, bool $fix): int return 2; } - $collector = new CycleCollector; + $collector = new CycleCollector($profiler); $sent = false; while (($line = fgets($socket)) !== false) { $job = json_decode($line, associative: true); + if (is_array($job) && ($job['finish'] ?? null) === true) { // nothing is left; the parent waits for what was measured since the last file + fwrite($socket, json_encode(['profile' => $profiler?->takeRecords() ?? []], JSON_THROW_ON_ERROR) . "\n"); + continue; + } + $path = is_array($job) && is_string($job['path'] ?? null) ? base64_decode($job['path'], strict: true) : false; if ($path === false) { fwrite(STDERR, "Unexpected message from the parent: $line"); @@ -39,7 +44,7 @@ public static function run(string $address, Runner $runner, bool $fix): int } $result = $runner->processPath($path, $fix); - $message = WorkerCodec::encode($result); + $message = WorkerCodec::encode($result, $profiler?->takeRecords()); // the files the results depend on, with the first result after they are known if (!$sent && ($dependencies = ($runner->findDependencies)?->__invoke()) !== null) { $message['dependencies'] = $dependencies; diff --git a/src/Engine/WorkerCodec.php b/src/Engine/WorkerCodec.php index 10420b22f..598567251 100644 --- a/src/Engine/WorkerCodec.php +++ b/src/Engine/WorkerCodec.php @@ -12,14 +12,18 @@ /** - * The result of a file as a worker sends it to the parent: the output only where it differs from the code, which - * the parent has, the output and the path base64-encoded since neither need be UTF-8. + * The result of a file as a worker sends it to the parent, with what it measured for `--profile`: the output only + * where it differs from the code, which the parent has, the output and the path base64-encoded since neither need + * be UTF-8. * @internal */ final class WorkerCodec { - /** @return array */ - public static function encode(FileResult $result): array + /** + * @param ?array $profile see `Profiler::takeRecords()` + * @return array + */ + public static function encode(FileResult $result, ?array $profile): array { return [ 'path' => base64_encode($result->path), @@ -37,6 +41,7 @@ public static function encode(FileResult $result): array 'cached' => $result->cached, 'removedImports' => $result->removedImports, 'mutatedRules' => $result->mutatedRules, + 'profile' => $profile, ]; } @@ -44,11 +49,12 @@ public static function encode(FileResult $result): array /** * @param array $data as `encode()` made it * @param string $code the content the parent handed the worker + * @return array{FileResult, ?array} the result and what the worker measured with it */ - public static function decode(array $data, string $code): FileResult + public static function decode(array $data, string $code): array { $output = $data['output']; - return new FileResult( + $result = new FileResult( path: (string) base64_decode((string) $data['path'], strict: true), code: $code, output: $output === true ? $code : ($output === null ? null : (string) base64_decode((string) $output, strict: true)), @@ -75,6 +81,7 @@ public static function decode(array $data, string $code): FileResult ], is_array($data['removedImports']) ? $data['removedImports'] : [])), mutatedRules: is_array($data['mutatedRules']) ? array_values(array_filter($data['mutatedRules'], self::isRule(...))) : [], ); + return [$result, is_array($data['profile']) ? $data['profile'] : null]; } diff --git a/src/Engine/WorkerPool.php b/src/Engine/WorkerPool.php index 1859ca0a1..fd5b74f05 100644 --- a/src/Engine/WorkerPool.php +++ b/src/Engine/WorkerPool.php @@ -57,6 +57,9 @@ final class WorkerPool /** @var list */ private array $queue = []; + /** @var array workers asked for what they measured last, by socket id */ + private array $finishing = []; + /** @var array the files the results depend on, as the workers sent them */ private array $dependencies = []; @@ -71,6 +74,8 @@ public function __construct( private readonly int $taskTimeout = 300, /** seconds a worker may take to connect, the first one filling the cache the others share among them */ private readonly int $connectTimeout = 300, + /** with it, every worker hands over what it measured last once nothing is left, one that got no file among them */ + private readonly ?Profiler $profiler = null, /** the first worker fills a cache the others share before it connects, and they start after that */ private readonly bool $warmFirst = false, ) { @@ -181,7 +186,7 @@ public function process(array $paths, \Closure $read, ?\Closure $onProgress = nu $this->workers[] = $this->spawn($address); } - while ($done < count($paths)) { + while ($done < count($paths) || $this->isFinishing()) { foreach ($this->await() as $stream) { foreach ($this->receive((int) $stream) as $path => $result) { $done++; @@ -386,6 +391,12 @@ private function receive(int $id): array while (($end = strpos($buffer, "\n")) !== false) { $data = json_decode(substr($buffer, 0, $end), associative: true); $buffer = substr($buffer, $end + 1); + if (isset($this->finishing[$id]) && is_array($data['profile'] ?? null)) { + $this->profiler?->merge($data['profile']); + $this->disconnect($id); + break; + } + [$path, , $code] = $this->inProgress[$id] ?? [null, null, '']; if (!is_array($data) || $path === null || ($data['path'] ?? null) !== base64_encode($path)) { throw new \RuntimeException('A worker sent an unexpected message.' . $this->collectErrors()); @@ -396,7 +407,11 @@ private function receive(int $id): array $this->dependencies[(string) $file] = true; } - $results[$path] = WorkerCodec::decode($data, $code); + [$results[$path], $profile] = WorkerCodec::decode($data, $code); + if ($profile !== null) { + $this->profiler?->merge($profile); + } + $this->assign($id); } @@ -410,11 +425,15 @@ private function receive(int $id): array } - /** Hands the next path to the worker, or closes its connection when there is none. */ + /** Hands the next path to the worker; when there is none, asks it for what it measured last, or closes its connection. */ private function assign(int $id): void { $path = array_shift($this->queue); - if ($path === null) { + if ($path === null && $this->profiler !== null) { + $this->finishing[$id] = true; + fwrite($this->sockets[$id], json_encode(['finish' => true], JSON_THROW_ON_ERROR) . "\n"); + return; + } elseif ($path === null) { $this->disconnect($id); return; } @@ -440,7 +459,25 @@ private function getInProgress(): array private function disconnect(int $id): void { fclose($this->sockets[$id]); - unset($this->sockets[$id], $this->inProgress[$id], $this->buffers[$id]); + unset($this->sockets[$id], $this->inProgress[$id], $this->buffers[$id], $this->finishing[$id]); + } + + + /** + * Whether the run still waits for what the workers measured last: from one asked for it, and from one still + * starting, which gets nothing to process but has its peak memory. + */ + private function isFinishing(): bool + { + if ($this->profiler === null) { + return false; + } elseif ($this->finishing !== []) { + return true; + } + + // which process a connection belongs to is unknown, so a worker done with its work is waited for too, until it exits + return $this->accepted < count($this->workers) + && array_any($this->workers, fn(array $worker) => proc_get_status($worker[0])['running']); } diff --git a/tests/DressCode/Console/Application.phpt b/tests/DressCode/Console/Application.phpt index 3faf7f730..7383827da 100644 --- a/tests/DressCode/Console/Application.phpt +++ b/tests/DressCode/Console/Application.phpt @@ -967,6 +967,15 @@ test('a worker builds a rule of a plugin by the factory of its manifest, as the }); +test('a worker gets a value starting with a dash as a value', function () use ($root) { + @unlink("$root/-profile.json"); // @ may not exist + [$code, , $err] = runApp($root, ['check', '--config', "$root/jobs.php", '--no-cache', '--jobs', '2', '--profile=-profile.json']); + Assert::same('', $err); + Assert::same(0, $code); + Assert::true(is_file("$root/-profile.json")); +}); + + test('a file a worker cannot write fails alone, the others are fixed', function () use ($root) { file_put_contents( "$root/readonly.php", @@ -1015,6 +1024,52 @@ test('rules that do not fit are a configuration error with workers too', functio }); +test('a profile counts the same with workers as in the process', function () use ($root) { + file_put_contents( + "$root/profile.php", + " ['keyword' => 'lowercase'],'file' => ['trailingWhitespace' => 'forbidden']]);\n", + ); + @mkdir("$root/profile"); // @ directory may already exist + for ($i = 0; $i < 8; $i++) { + file_put_contents("$root/profile/$i.php", " 0); + Assert::same(8, $profile['rules'][DressCode\Rules\Literals\BuiltinCasingRule::class]['mutating']); + Assert::true($profile['phases']['passes']['time'] >= array_sum(array_column($profile['rules'], 'time'))); + $count = [ + array_map(fn($entry) => [$entry['calls'], $entry['mutating']], $profile['rules']), + array_map(fn($entry) => $entry['calls'], $profile['claims']), + array_map( + fn($entry) => $entry['calls'], + array_intersect_key($profile['phases'], ['parse' => 0, 'passes' => 0, 'print' => 0, 'gaps' => 0]), + ), + ]; + array_walk($count, ksort(...)); // the tables are ordered by time, which differs between runs + $counts[] = $count; + } + + Assert::same($counts[0], $counts[1]); + if (findPhpCliBinary() !== null) { + Assert::count(2 + 1, json_decode(FileSystem::read("$root/profile-2.json"), associative: true)['peakMemory']); // the parent and two workers + } + + // writing a baseline is a run of check too, whether it ends with one or with code a fix would change + runApp($root, ['baseline', '--config', "$root/profile.php", '--profile', "$root/profile-baseline.json"]); + Assert::same(8, json_decode(FileSystem::read("$root/profile-baseline.json"), associative: true)['files']); +}); + + test('a worker has the settings of the process the ini file alone would not give it', function () use ($root) { // the configuration turns the rule on by a setting, which a worker has only when it is handed over file_put_contents( diff --git a/tests/DressCode/Engine/Profiler.phpt b/tests/DressCode/Engine/Profiler.phpt new file mode 100644 index 000000000..e2b8722fb --- /dev/null +++ b/tests/DressCode/Engine/Profiler.phpt @@ -0,0 +1,53 @@ +addPhase('parse', 10); + $worker->addRule('a/rule', 5, mutated: true); + $worker->addRule('a/rule', 7, mutated: false); + $worker->addClaim('a/gap', 3); + $worker->addFile(['path' => 'x.php', 'size' => 1, 'time' => 30]); + $records = $worker->takeRecords(); + + $worker->addPhase('parse', 20); + $worker->addFile(['path' => 'y.php', 'size' => 1, 'time' => 40]); + $second = $worker->takeRecords(); + Assert::same(['parse' => [20, 1]], $second['phases']); // taken records are forgotten + + $parent = new Profiler; + $parent->merge(json_decode(json_encode($records), associative: true)); // as it travels between processes + $parent->merge(json_decode(json_encode($second), associative: true)); + $profile = $parent->toArray(); + + Assert::same(Profiler::Schema, $profile['schema']); + Assert::same(2, $profile['files']); + Assert::same(['parse' => ['time' => 30, 'calls' => 2]], $profile['phases']); + Assert::same(['a/rule' => ['time' => 12, 'calls' => 2, 'mutating' => 1]], $profile['rules']); + Assert::same(['a/gap' => ['time' => 3, 'calls' => 1]], $profile['claims']); + Assert::same(['y.php', 'x.php'], array_column($profile['slowestFiles'], 'path')); +}); + + +test('the most expensive come first, the slowest files in detail and every file in short', function () { + $profiler = new Profiler; + $profiler->addRule('cheap', 1, mutated: false); + $profiler->addRule('dear', 100, mutated: false); + for ($i = 1; $i <= 120; $i++) { + $profiler->addFile(['path' => "$i.php", 'size' => $i, 'time' => $i]); + } + + $profile = $profiler->toArray(); + Assert::same(['dear', 'cheap'], array_keys($profile['rules'])); + Assert::same(120, $profile['files']); + Assert::count(50, $profile['slowestFiles']); + Assert::same('120.php', $profile['slowestFiles'][0]['path']); + Assert::same('71.php', $profile['slowestFiles'][49]['path']); + Assert::count(120, $profile['fileTimes']); // every file, for a sample to be drawn from + Assert::same(['1.php', '10.php', '100.php'], array_slice(array_keys($profile['fileTimes']), 0, 3)); +}); diff --git a/tests/DressCode/Engine/WorkerCodec.phpt b/tests/DressCode/Engine/WorkerCodec.phpt index f050bd3dd..87963411a 100644 --- a/tests/DressCode/Engine/WorkerCodec.phpt +++ b/tests/DressCode/Engine/WorkerCodec.phpt @@ -7,14 +7,14 @@ use Tester\Assert; require __DIR__ . '/../../bootstrap.php'; -$roundTrip = function (FileResult $result): FileResult { - $data = json_decode(json_encode(WorkerCodec::encode($result), JSON_THROW_ON_ERROR), associative: true); +$roundTrip = function (FileResult $result, ?array $profile): array { + $data = json_decode(json_encode(WorkerCodec::encode($result, $profile), JSON_THROW_ON_ERROR), associative: true); Assert::type('array', $data); return WorkerCodec::decode($data, $result->code); }; -test('a result survives the way from a worker to the parent and back', function () use ($roundTrip) { +test('a result survives the way from a worker to the parent and back, with what the worker measured', function () use ($roundTrip) { $violation = new Violation( 'test/a', 'A.', @@ -46,15 +46,15 @@ test('a result survives the way from a worker to the parent and back', function ], ); $result->markWritten(); - Assert::equal($result, $roundTrip($result)); + Assert::equal([$result, ['files' => [1]]], $roundTrip($result, ['files' => [1]])); }); test('an output that is not valid UTF-8 survives the way byte for byte', function () { $code = "output, $decoded->output); Assert::same($code, $decoded->code); }); @@ -62,15 +62,15 @@ test('an output that is not valid UTF-8 survives the way byte for byte', functio test('a path that is not valid UTF-8 survives the way byte for byte', function () use ($roundTrip) { $result = new FileResult("\xE9.php", "code); + $json = json_encode(WorkerCodec::encode($result, null), JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE); + [$decoded] = WorkerCodec::decode(json_decode($json, associative: true), $result->code); Assert::same("bad \u{FFFD} byte.", $decoded->violations[0]->message); Assert::same("bad \u{FFFD} byte.", $decoded->remaining[0]->message); }); @@ -78,8 +78,8 @@ test('a message that is not valid UTF-8 gets through the transport with the bad test('an unchanged output, a syntax error and a result from the cache', function () use ($roundTrip) { $clean = new FileResult('a.php', " false, 'removedImports' => [], 'mutatedRules' => [], + 'profile' => null, 'dependencies' => ['stubs/' . (mb_check_encoding($name = (string) base64_decode($path), 'UTF-8') ? $name : 'other') . '.stub'], ], JSON_THROW_ON_ERROR) . "\n"; From 6df1f644b004dd4338e1fc571515fa029b1f1666 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 19 Oct 2026 09:14:40 +0200 Subject: [PATCH 12/13] uses the Runner of nette/command-line WIP --- composer.json | 2 +- src/Console/Application.php | 36 +++++++++++------------------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/composer.json b/composer.json index 12afe1c8b..20e118a8e 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { "php": "8.4 - 8.6", - "nette/command-line": "^2.0", + "nette/command-line": "dev-master", "nette/neon": "^4.0", "nette/schema": "^1.4 || ^2.0", "nette/utils": "^4.1", diff --git a/src/Console/Application.php b/src/Console/Application.php index 6c2cfba00..3ca3fbb8f 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -8,7 +8,7 @@ namespace DressCode\Console; use DressCode\{Config, ConfigurationException, ConvergenceException, RuleException}; -use Nette\CommandLine\{ColorDepth, Command, Console, HelpRenderer, Parser as CommandLineParser, Prompt, UsageException}; +use Nette\CommandLine\{ColorDepth, Command, Console, Prompt, Runner}; use Nette\Utils\FileSystem; use function extension_loaded; @@ -67,28 +67,17 @@ public function __construct( */ public function run(array $argv): int { - [$context, $out, $err] = [$this->context, $this->context->out, $this->context->err]; + [$context, $err] = [$this->context, $this->context->err]; try { - $args = (new CommandLineParser)->parse($this->defineCommandLine(), array_slice($argv, 1)); - if ($args->get('--no-color')) { - $out->setColorDepth(ColorDepth::None); - $err->setColorDepth(ColorDepth::None); - } - - if ($args->get('--version')) { - $out->writeLine(CommandContext::formatName($out)); - return 0; - } elseif ($args->get('--help')) { - $out->writeLine(CommandContext::formatName($out) . "\n"); - new HelpRenderer($out)->render($args->command); - return 0; - } - - return $args->run(); - - } catch (UsageException $e) { - new HelpRenderer($this->context->err)->renderError($e); - return 3; + $runner = new Runner( + $this->defineCommandLine(), + version: CommandContext::formatName(...), + colorFlag: true, + usageExitCode: 3, + out: $context->out, + err: $err, + ); + return $runner->run(array_slice($argv, 1)); } catch (ConfigurationException $e) { $context->writeError($e->getMessage(), $e->docs); @@ -127,9 +116,6 @@ public function run(array $argv): int private function defineCommandLine(): Command { $program = new Command('dresscode', 'A dress code for PHP: checks, fixes and upgrades the code.', commandRequired: true); - $program->addFlag('--no-color', 'Print plain output without colors'); - $program->addFlag('--help', 'Print this help', standalone: true); - $program->addFlag('--version', 'Print the name and the version', standalone: true); $context = $this->context; foreach ([ // in the order the help lists them From d64900a4973bba0b9846e64da4f062b5108e219c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Wed, 28 Oct 2026 23:18:49 +0100 Subject: [PATCH 13/13] phpdocAboveAttributes: a doc comment between two attributes moves above them too, the doc comments of a declaration keeping their order Co-Authored-By: Claude Code Claude-Session: https://claude.ai/code/session_01D1mfZTuCXTAaFC39NM1xdk --- .../PhpDoc/PhpdocAboveAttributesRule.php | 28 +++++---- .../phpdocAboveAttributes/between.code | 60 +++++++++++++++++++ .../phpdocAboveAttributes/between.expected | 60 +++++++++++++++++++ .../phpdocAboveAttributes/between.violations | 11 ++++ 4 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 tests/DressCode/Rules/fixtures/phpdocAboveAttributes/between.code create mode 100644 tests/DressCode/Rules/fixtures/phpdocAboveAttributes/between.expected create mode 100644 tests/DressCode/Rules/fixtures/phpdocAboveAttributes/between.violations diff --git a/src/Rules/PhpDoc/PhpdocAboveAttributesRule.php b/src/Rules/PhpDoc/PhpdocAboveAttributesRule.php index ec47aa1ed..2421cc3d6 100644 --- a/src/Rules/PhpDoc/PhpdocAboveAttributesRule.php +++ b/src/Rules/PhpDoc/PhpdocAboveAttributesRule.php @@ -18,7 +18,7 @@ /** * The doc comment comes first, the attributes after it, right before the declaration. A doc comment found - * between the attributes and the declaration moves above the first attribute. + * between the attributes, or between them and the declaration, moves above the first attribute. */ #[RuleInfo(Stage::Structure, modifiesComments: true)] final class PhpdocAboveAttributesRule extends NodeRule @@ -52,21 +52,25 @@ public function enter(Node|Token $node, RuleContext $context): void return; } - $docComment = null; - foreach ($after->leadingTrivia as $trivia) { - if ($trivia->is(Trivia::DocComment) && !$trivia->inInterpolation) { - $docComment = $trivia; + // the tokens a doc comment below the first attribute stands in front of: every further attribute and the declaration + $tokens = array_map(fn(Node $group) => $group->getFirstToken(), array_slice($attributes->getItems(), 1)); + foreach (array_filter([...$tokens, $after]) as $token) { + foreach ($token->leadingTrivia as $trivia) { + if ( + $trivia->is(Trivia::DocComment) + && !$trivia->inInterpolation + && $context->report($node, 'The doc comment must be above the attributes.', trivia: $trivia) + ) { + $token->removeTrivia($trivia); + $this->placeAbove($first, $trivia, $context); + } } } + } - if ( - $docComment === null - || !$context->report($node, 'The doc comment must be above the attributes.', trivia: $docComment) - ) { - return; - } - $after->removeTrivia($docComment); + private function placeAbove(Token $first, Trivia $docComment, RuleContext $context): void + { $leading = $first->leadingTrivia; $indentation = $leading && $leading[count($leading) - 1]->is(Trivia::Whitespace) ? array_pop($leading) : null; $first->setLeadingTrivia([ diff --git a/tests/DressCode/Rules/fixtures/phpdocAboveAttributes/between.code b/tests/DressCode/Rules/fixtures/phpdocAboveAttributes/between.code new file mode 100644 index 000000000..1e130b9d1 --- /dev/null +++ b/tests/DressCode/Rules/fixtures/phpdocAboveAttributes/between.code @@ -0,0 +1,60 @@ +