diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 8db105a9..62b79725 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -67,14 +67,17 @@ 'phpdoc_no_useless_inheritdoc' => true, 'phpdoc_order' => ['order' => [ 'todo', - 'method', 'property', + 'property-read', + 'property-write', + 'method', 'api', 'internal', 'requires', 'dataProvider', 'backupGlobals', 'template', + 'template-covariant', 'extends', 'implements', 'use', @@ -83,18 +86,20 @@ 'readonly', 'var', 'param', + 'param-out', 'return', 'throws', ]], // 'phpdoc_param_order' => true, 'phpdoc_separation' => ['groups' => [ ['see', 'link'], - ['property', 'phpstan-property', 'property-read', 'phpstan-property-read'], + ['property', 'property-read', 'property-write', 'phpstan-property', 'phpstan-property-read', 'phpstan-property-write'], + ['method', 'phpstan-method'], ['requires', 'dataProvider', 'backupGlobals'], ['template', 'template-covariant'], ['extends', 'implements', 'use'], ['phpstan-require-extends', 'phpstan-require-implements'], - ['readonly', 'var', 'phpstan-var', 'param', 'param-out', 'phpstan-param', 'return', 'phpstan-return', 'throws', 'phpstan-assert*', 'phpstan-ignore*', 'disregard'], + ['readonly', 'var', 'param', 'param-out', 'return', 'throws', 'phpstan-var', 'phpstan-param', 'phpstan-return', 'phpstan-assert*', 'phpstan-ignore*', 'disregard'], ['phpstan-*'], ]], 'phpdoc_tag_casing' => true, diff --git a/composer.lock b/composer.lock index 51444696..e93ff680 100644 --- a/composer.lock +++ b/composer.lock @@ -360,16 +360,16 @@ }, { "name": "salient/cli", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-cli.git", - "reference": "e28058395b61777395347e23ada6c55be363eef0" + "reference": "8ed34826739c218c401d7a4a8af3c3c99e660c9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-cli/zipball/e28058395b61777395347e23ada6c55be363eef0", - "reference": "e28058395b61777395347e23ada6c55be363eef0", + "url": "https://api.github.com/repos/salient-labs/toolkit-cli/zipball/8ed34826739c218c401d7a4a8af3c3c99e660c9a", + "reference": "8ed34826739c218c401d7a4a8af3c3c99e660c9a", "shasum": "" }, "require": { @@ -401,20 +401,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-29T09:22:45+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/collections", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-collections.git", - "reference": "c8739544f12bd5880b65fd840e3cd7af559aac53" + "reference": "238bd617c099c5e92043e22682fc0f11c5eadeb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-collections/zipball/c8739544f12bd5880b65fd840e3cd7af559aac53", - "reference": "c8739544f12bd5880b65fd840e3cd7af559aac53", + "url": "https://api.github.com/repos/salient-labs/toolkit-collections/zipball/238bd617c099c5e92043e22682fc0f11c5eadeb1", + "reference": "238bd617c099c5e92043e22682fc0f11c5eadeb1", "shasum": "" }, "require": { @@ -443,20 +443,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-12-09T13:32:56+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/console", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-console.git", - "reference": "8c30d874aab3b08a4b539dc02288d52885352764" + "reference": "10a8c1d24c08ea69e0eac02b11882f1be13d6878" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-console/zipball/8c30d874aab3b08a4b539dc02288d52885352764", - "reference": "8c30d874aab3b08a4b539dc02288d52885352764", + "url": "https://api.github.com/repos/salient-labs/toolkit-console/zipball/10a8c1d24c08ea69e0eac02b11882f1be13d6878", + "reference": "10a8c1d24c08ea69e0eac02b11882f1be13d6878", "shasum": "" }, "require": { @@ -493,20 +493,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-29T09:22:45+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/container", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-container.git", - "reference": "46cef04555303e899772560ce717858b3fb25fc6" + "reference": "718d0f2c4ddcccf5f042fc5f50a5b74aa7ac6c31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-container/zipball/46cef04555303e899772560ce717858b3fb25fc6", - "reference": "46cef04555303e899772560ce717858b3fb25fc6", + "url": "https://api.github.com/repos/salient-labs/toolkit-container/zipball/718d0f2c4ddcccf5f042fc5f50a5b74aa7ac6c31", + "reference": "718d0f2c4ddcccf5f042fc5f50a5b74aa7ac6c31", "shasum": "" }, "require": { @@ -547,20 +547,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-14T04:09:41+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/contracts", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-contracts.git", - "reference": "0180046156ff3a17e6ad39f7688333e0cbf8e306" + "reference": "0a973d13b26db51430e52baf2526f4af77a1c9db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-contracts/zipball/0180046156ff3a17e6ad39f7688333e0cbf8e306", - "reference": "0180046156ff3a17e6ad39f7688333e0cbf8e306", + "url": "https://api.github.com/repos/salient-labs/toolkit-contracts/zipball/0a973d13b26db51430e52baf2526f4af77a1c9db", + "reference": "0a973d13b26db51430e52baf2526f4af77a1c9db", "shasum": "" }, "require": { @@ -596,20 +596,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-12-09T13:32:56+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/core", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-core.git", - "reference": "472f6e1983666bf5702b93a79bf40b3a62133a2f" + "reference": "d1d91ab432c85c3f9589b2965febb64d7ae6d0bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-core/zipball/472f6e1983666bf5702b93a79bf40b3a62133a2f", - "reference": "472f6e1983666bf5702b93a79bf40b3a62133a2f", + "url": "https://api.github.com/repos/salient-labs/toolkit-core/zipball/d1d91ab432c85c3f9589b2965febb64d7ae6d0bb", + "reference": "d1d91ab432c85c3f9589b2965febb64d7ae6d0bb", "shasum": "" }, "require": { @@ -644,20 +644,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-29T09:22:45+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/iterators", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-iterators.git", - "reference": "0666eef3dbed163b94a246d533f1cb45f01b2c5d" + "reference": "88e67e1bd95bf12e2bf539f77b3f121db1218b46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-iterators/zipball/0666eef3dbed163b94a246d533f1cb45f01b2c5d", - "reference": "0666eef3dbed163b94a246d533f1cb45f01b2c5d", + "url": "https://api.github.com/repos/salient-labs/toolkit-iterators/zipball/88e67e1bd95bf12e2bf539f77b3f121db1218b46", + "reference": "88e67e1bd95bf12e2bf539f77b3f121db1218b46", "shasum": "" }, "require": { @@ -686,20 +686,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-29T09:22:45+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/phpdoc", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-phpdoc.git", - "reference": "36bb531ced37c54cbadc6e4d98efecb8ff1ea364" + "reference": "1ce2025b046791436453ce44d0e4e50eff6a8f53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-phpdoc/zipball/36bb531ced37c54cbadc6e4d98efecb8ff1ea364", - "reference": "36bb531ced37c54cbadc6e4d98efecb8ff1ea364", + "url": "https://api.github.com/repos/salient-labs/toolkit-phpdoc/zipball/1ce2025b046791436453ce44d0e4e50eff6a8f53", + "reference": "1ce2025b046791436453ce44d0e4e50eff6a8f53", "shasum": "" }, "require": { @@ -729,20 +729,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-15T09:20:32+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/polyfills", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-polyfills.git", - "reference": "7f1cc7cff859b5fbccc5014fa5025e62f4aa50c4" + "reference": "c02ad4a8e653029eb0ae8cc91c2733ea7a6f5de1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-polyfills/zipball/7f1cc7cff859b5fbccc5014fa5025e62f4aa50c4", - "reference": "7f1cc7cff859b5fbccc5014fa5025e62f4aa50c4", + "url": "https://api.github.com/repos/salient-labs/toolkit-polyfills/zipball/c02ad4a8e653029eb0ae8cc91c2733ea7a6f5de1", + "reference": "c02ad4a8e653029eb0ae8cc91c2733ea7a6f5de1", "shasum": "" }, "require": { @@ -773,20 +773,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-14T04:09:41+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "salient/utils", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-utils.git", - "reference": "62b87c21a28c4990ce731d019a2f0d8250e42a5c" + "reference": "eb08bf882f7d465935df7092326ebd1f995cbe18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-utils/zipball/62b87c21a28c4990ce731d019a2f0d8250e42a5c", - "reference": "62b87c21a28c4990ce731d019a2f0d8250e42a5c", + "url": "https://api.github.com/repos/salient-labs/toolkit-utils/zipball/eb08bf882f7d465935df7092326ebd1f995cbe18", + "reference": "eb08bf882f7d465935df7092326ebd1f995cbe18", "shasum": "" }, "require": { @@ -820,7 +820,7 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-12-09T13:32:56+00:00" + "time": "2025-01-13T06:34:58+00:00" }, { "name": "sebastian/diff", @@ -2587,16 +2587,16 @@ }, { "name": "salient/phpstan", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-phpstan.git", - "reference": "eaf63f2951f0fa44b967b530ceb8cb58d309c56f" + "reference": "2e747960ae45fbc2d4e2e8c8ecda9cd3ff79e64f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-phpstan/zipball/eaf63f2951f0fa44b967b530ceb8cb58d309c56f", - "reference": "eaf63f2951f0fa44b967b530ceb8cb58d309c56f", + "url": "https://api.github.com/repos/salient-labs/toolkit-phpstan/zipball/2e747960ae45fbc2d4e2e8c8ecda9cd3ff79e64f", + "reference": "2e747960ae45fbc2d4e2e8c8ecda9cd3ff79e64f", "shasum": "" }, "require": { @@ -2632,20 +2632,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-16T06:02:30+00:00" + "time": "2025-01-14T04:37:48+00:00" }, { "name": "salient/sli", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-sli.git", - "reference": "8264f3b4343eeeb8fdcdc6d1d2ab9c27cd1a92c5" + "reference": "c61312211228c1901611c4430c726aaf365ca499" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-sli/zipball/8264f3b4343eeeb8fdcdc6d1d2ab9c27cd1a92c5", - "reference": "8264f3b4343eeeb8fdcdc6d1d2ab9c27cd1a92c5", + "url": "https://api.github.com/repos/salient-labs/toolkit-sli/zipball/c61312211228c1901611c4430c726aaf365ca499", + "reference": "c61312211228c1901611c4430c726aaf365ca499", "shasum": "" }, "require": { @@ -2688,20 +2688,20 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-29T09:22:45+00:00" + "time": "2025-01-13T11:49:04+00:00" }, { "name": "salient/sync", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-sync.git", - "reference": "737523b6dcaf3382ed51644e240c5796ee5e0de0" + "reference": "dfd118a56811f660ed7ae1b2c4587fbc3aed8d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/salient-labs/toolkit-sync/zipball/737523b6dcaf3382ed51644e240c5796ee5e0de0", - "reference": "737523b6dcaf3382ed51644e240c5796ee5e0de0", + "url": "https://api.github.com/repos/salient-labs/toolkit-sync/zipball/dfd118a56811f660ed7ae1b2c4587fbc3aed8d6f", + "reference": "dfd118a56811f660ed7ae1b2c4587fbc3aed8d6f", "shasum": "" }, "require": { @@ -2740,11 +2740,11 @@ "issues": "https://github.com/salient-labs/toolkit/issues", "source": "https://github.com/salient-labs/toolkit" }, - "time": "2024-11-29T09:22:45+00:00" + "time": "2025-01-14T03:45:20+00:00" }, { "name": "salient/testing", - "version": "v0.99.65", + "version": "v0.99.68", "source": { "type": "git", "url": "https://github.com/salient-labs/toolkit-testing.git", diff --git a/docs/Rules.md b/docs/Rules.md index 83ffac9c..16cafaf5 100644 --- a/docs/Rules.md +++ b/docs/Rules.md @@ -33,13 +33,14 @@ This file is generated by [generate.sh][], so any changes should be made there o | `PlaceComments` (1) | Y | - | 1 | `processTokens()` | 126 | | `PreserveNewlines` | - | Y | 1 | `processTokens()` | 200 | | `PreserveOneLineStatements` | - | - | 1 | `processStatements()` | 202 | -| `BlankBeforeReturn` | - | - | 1 | `processTokens()` | 220 | -| `VerticalSpacing` (1) | Y | - | 1 | `processTokens()` | 240 | -| `VerticalSpacing` (2) | Y | - | 1 | `processList()` | 240 | -| `VerticalSpacing` (3) | Y | - | 1 | `processDeclarations()` | 240 | -| `StrictLists` | - | - | 1 | `processList()` | 242 | -| `StrictExpressions` | - | - | 1 | `processTokens()` | 244 | -| `SemiStrictExpressions` | - | - | 1 | `processTokens()` | 246 | +| `BlankBeforeReturn` | - | - | 1 | `processTokens()` | 204 | +| `VerticalSpacing` (1) | Y | - | 1 | `processList()` | 220 | +| `VerticalSpacing` (2) | Y | - | 1 | `processDeclarations()` | 220 | +| `StrictExpressions` | - | - | 1 | `processTokens()` | 222 | +| `SemiStrictExpressions` | - | - | 1 | `processTokens()` | 224 | +| `StrictLists` | - | - | 1 | `processList()` | 226 | +| `PlaceBrackets` | Y | - | 1 | `processTokens()` | 240 | +| `VerticalSpacing` (3) | Y | - | 1 | `processTokens()` | 242 | | `AlignChains` (1) | - | - | 1 | `processTokens()` | 280 | | `DeclarationSpacing` | - | Y | 1 | `processDeclarations()` | 299 | | `StandardIndentation` | Y | - | 1 | `processTokens()` | 300 | @@ -165,7 +166,7 @@ Spaces are added before and after operators not mentioned above. ### `StandardSpacing` (1) -(mandatory, `processTokens()`, priority 104, tokens: `,` | `T_DECLARE` | `T_MATCH` | `T_ATTRIBUTE` | `T_OPEN_TAG` | `T_OPEN_TAG_WITH_ECHO` | `T_CLOSE_TAG` | `T_START_HEREDOC` | `T_ATTRIBUTE_COMMENT`) +(mandatory, `processTokens()`, priority 104, tokens: `,` | `T_FOR` | `T_DECLARE` | `T_MATCH` | `T_ATTRIBUTE` | `T_OPEN_TAG` | `T_OPEN_TAG_WITH_ECHO` | `T_CLOSE_TAG` | `T_START_HEREDOC` | `T_ATTRIBUTE_COMMENT`) If the indentation level of an open tag aligns with a tab stop, and a close tag is found in the same scope (or the document has no close tag, and the open tag is in the global scope), a callback is registered to align nested tokens with it. An additional level of indentation is applied if the formatter's `IndentBetweenTags` property is `true`. @@ -177,6 +178,7 @@ Whitespace is applied to other tokens as follows: - **Commas:** leading whitespace is suppressed, and trailing spaces are added. - **`declare` expressions:** whitespace is suppressed between parentheses. +- **`for` loops:** whitespace in empty expressions is suppressed. - **`match` expressions:** newlines are added after delimiters between arms. - **Attributes:** in parameters, property hooks, anonymous functions and arrow functions, spaces are added before and after attributes, and trailing blank lines are suppressed. For other attributes, leading and trailing newlines are added. - **Heredocs:** leading newlines are suppressed in strict PSR-12 mode. @@ -268,66 +270,73 @@ Attributes on their own line are excluded from consideration. ### `BlankBeforeReturn` -(optional, `processTokens()`, priority 220, tokens: `T_YIELD` | `T_YIELD_FROM` | `T_RETURN`) +(optional, `processTokens()`, priority 204, tokens: `T_YIELD` | `T_YIELD_FROM` | `T_RETURN`) Blank lines are added before non-consecutive `return`, `yield` and `yield from` statements. ### `VerticalSpacing` (1) -(mandatory, `processTokens()`, priority 240, tokens: `&` | `?` | `^` | `{` | `|` | `T_LOGICAL_OR` | `T_LOGICAL_XOR` | `T_LOGICAL_AND` | `T_FOR` | `T_BOOLEAN_OR` | `T_BOOLEAN_AND` | `T_OBJECT_OPERATOR` | `T_NULLSAFE_OBJECT_OPERATOR` | `T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG` | `T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG`) +(mandatory, `processList()`, priority 220) -In expressions where one or more boolean operators have an adjacent newline, newlines are added to other boolean operators of equal or lower precedence. - -In `for` loops: +If interface lists break over multiple lines and neither `StrictLists` nor `AlignLists` are enabled, a newline is added before the first interface. -- If an expression with multiple expressions breaks over multiple lines, newlines are added after comma-delimited expressions, and blank lines are added after semicolon-delimited expressions. -- Otherwise, if an expression breaks over multiple lines, newlines are added after semicolon-delimited expressions. -- Otherwise, if the second or third expression has a leading newline, a newline is added before the other. -- Whitespace in empty expressions is suppressed. +Arrays and argument lists with trailing ("magic") commas are split into one item per line. -Newlines are added before open braces that belong to top-level declarations and anonymous classes declared over multiple lines. +If parameter lists have one or more attributes with a trailing newline, every attribute is placed on its own line, and blank lines are added before and after annotated parameters to improve readability. -Newlines are added before both operators in ternary expressions where one operator has a leading newline. +### `VerticalSpacing` (2) -In method chains where an object operator (`->` or `?->`) has a leading newline, newlines are added before every object operator. If the `AlignChains` rule is enabled and strict PSR-12 compliance is not, the first object operator in the chain is excluded from this operation. +(mandatory, `processDeclarations()`, priority 220, declarations: `CONST` | `USE` | `PROPERTY` | `PARAM` | `USE_CONST` | `USE_FUNCTION` | `USE_TRAIT`) -### `VerticalSpacing` (2) +Newlines are added between comma-delimited constant declarations and property declarations. When neither `StrictLists` nor `AlignLists` are enabled, they are also added to `use` statements between comma-delimited imports and trait insertions that break over multiple lines. -(mandatory, `processList()`, priority 240) +If a list of property hooks has one or more attributes with a trailing newline, every attribute is placed on its own line, and blank lines are added before and after annotated hooks to improve readability. -If interface lists break over multiple lines and neither `StrictLists` nor `AlignLists` are enabled, a newline is added before the first interface. +### `StrictExpressions` -Arrays and argument lists with trailing ("magic") commas are split into one item per line. +(optional, `processTokens()`, priority 222, tokens: `T_IF` | `T_ELSEIF` | `T_WHILE` | `T_FOR` | `T_FOREACH` | `T_SWITCH`) -If parameter lists have one or more attributes with a trailing newline, every attribute is placed on its own line, and blank lines are added before and after annotated parameters to improve readability. +Newlines are added before and after control structure expressions that break over multiple lines. In `for` expressions that break over multiple lines, newlines are also added after semicolons between expressions. -### `VerticalSpacing` (3) +### `SemiStrictExpressions` -(mandatory, `processDeclarations()`, priority 240, declarations: `CONST` | `USE` | `PROPERTY` | `PARAM` | `USE_CONST` | `USE_FUNCTION` | `USE_TRAIT`) +(optional, `processTokens()`, priority 224, tokens: `T_IF` | `T_ELSEIF` | `T_WHILE` | `T_FOR` | `T_FOREACH` | `T_SWITCH`) -Newlines are added between comma-delimited constant declarations and property declarations. When neither `StrictLists` nor `AlignLists` are enabled, they are also added to `use` statements between comma-delimited imports and trait insertions that break over multiple lines. +Newlines are added before and after control structure expressions with newlines between siblings. In `for` expressions that break over multiple lines, newlines are also added after semicolons between expressions. -If a list of property hooks has one or more attributes with a trailing newline, every attribute is placed on its own line, and blank lines are added before and after annotated hooks to improve readability. +> Unlike `StrictExpressions`, this rule does not apply leading and trailing newlines to expressions that would not break over multiple lines if tokens between brackets were removed. ### `StrictLists` -(optional, `processList()`, priority 242) +(optional, `processList()`, priority 226) Items in lists are arranged horizontally or vertically by replicating the arrangement of the first and second items. -### `StrictExpressions` +### `PlaceBrackets` -(optional, `processTokens()`, priority 244, tokens: `T_IF` | `T_ELSEIF` | `T_WHILE` | `T_FOR` | `T_FOREACH` | `T_SWITCH`) +(mandatory, `processTokens()`, priority 240, tokens: `(` | `[` | `{` | `T_ATTRIBUTE` | `T_DOLLAR_OPEN_CURLY_BRACES` | `T_CURLY_OPEN`) -Newlines are added before and after control structure expressions that break over multiple lines. +Inner whitespace is copied from open brackets to close brackets. -### `SemiStrictExpressions` +Structural and `match` expression braces are ignored. -(optional, `processTokens()`, priority 246, tokens: `T_IF` | `T_ELSEIF` | `T_WHILE` | `T_FOR` | `T_FOREACH` | `T_SWITCH`) +### `VerticalSpacing` (3) -Newlines are added before and after control structure expressions with newlines between siblings. +(mandatory, `processTokens()`, priority 242, tokens: `&` | `?` | `^` | `{` | `|` | `T_LOGICAL_OR` | `T_LOGICAL_XOR` | `T_LOGICAL_AND` | `T_FOR` | `T_BOOLEAN_OR` | `T_BOOLEAN_AND` | `T_OBJECT_OPERATOR` | `T_NULLSAFE_OBJECT_OPERATOR` | `T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG` | `T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG`) -> Unlike `StrictExpressions`, this rule does not apply leading and trailing newlines to expressions that would not break over multiple lines if tokens between brackets were removed. +In expressions where one or more boolean operators have an adjacent newline, newlines are added to other boolean operators of equal or lower precedence. + +In `for` loops: + +- If an expression with multiple expressions breaks over multiple lines, newlines are added after comma-delimited expressions, and blank lines are added after semicolon-delimited expressions. +- Otherwise, if an expression breaks over multiple lines, newlines are added after semicolon-delimited expressions. +- Otherwise, if the second or third expression has a leading newline, a newline is added before the other. + +Newlines are added before open braces that belong to top-level declarations and anonymous classes declared over multiple lines. + +Newlines are added before both operators in ternary expressions where one operator has a leading newline. + +In method chains where an object operator (`->` or `?->`) has a leading newline, newlines are added before every object operator. If the `AlignChains` rule is enabled and strict PSR-12 compliance is not, the first object operator in the chain is excluded from this operation. ### `AlignChains` (1) @@ -362,7 +371,7 @@ Blank lines are also added before and after each group of declarations. They are (mandatory, `processTokens()`, priority 300, tokens: `*`) -The `Indent` and inner whitespace of each open bracket is copied to its close bracket, and the `Indent` of tokens between brackets with inner newlines is incremented. +`Indent` is copied from open brackets to close brackets, and the `Indent` of tokens between brackets with inner newlines is incremented. ### `SwitchIndentation` @@ -579,64 +588,66 @@ Newlines and spaces are added after tokens that would otherwise fail to parse. T ## `TokenRule` classes, by token -| Token | Rules | -| ------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `*` | `HangingIndentation`, `StandardIndentation` | -| `* (except virtual)` | `PreserveNewlines` | -| `T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG` | `VerticalSpacing` | -| `T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG` | `VerticalSpacing` | -| `T_AND` | `VerticalSpacing` | -| `T_ATTRIBUTE` | `StandardSpacing` | -| `T_ATTRIBUTE_COMMENT` | `StandardSpacing` | -| `T_BACKTICK` | `ProtectStrings` | -| `T_BOOLEAN_AND` | `VerticalSpacing` | -| `T_BOOLEAN_OR` | `VerticalSpacing` | -| `T_CASE` | `SwitchIndentation` | -| `T_CATCH` | `Drupal` | -| `T_CLOSE_TAG` | `StandardSpacing` | -| `T_COALESCE` | `AlignTernaryOperators` | -| `T_COLON` | `StatementSpacing`, `WordPress` | -| `T_COMMA` | `StandardSpacing` | -| `T_COMMENT` | `NormaliseComments`, `PlaceComments`, `WordPress` | -| `T_CONCAT` | `Laravel`, `Symfony` | -| `T_CONSTANT_ENCAPSED_STRING` | `NormaliseStrings` | -| `T_DECLARE` | `StandardSpacing` | -| `T_DEFAULT` | `SwitchIndentation` | -| `T_DNUMBER` | `NormaliseNumbers` | -| `T_DO` | `ControlStructureSpacing` | -| `T_DOC_COMMENT` | `Drupal`, `NormaliseComments`, `PlaceComments`, `WordPress` | -| `T_DOUBLE_QUOTE` | `ProtectStrings` | -| `T_ELSE` | `ControlStructureSpacing`, `Drupal` | -| `T_ELSEIF` | `ControlStructureSpacing`, `Drupal`, `SemiStrictExpressions`, `StrictExpressions` | -| `T_ENCAPSED_AND_WHITESPACE` | `NormaliseStrings` | -| `T_FINALLY` | `Drupal` | -| `T_FN` | `AlignArrowFunctions`, `Laravel`, `Symfony` | -| `T_FOR` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StrictExpressions`, `VerticalSpacing` | -| `T_FOREACH` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StrictExpressions` | -| `T_IF` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StrictExpressions` | -| `T_LNUMBER` | `NormaliseNumbers` | -| `T_LOGICAL_AND` | `VerticalSpacing` | -| `T_LOGICAL_NOT` | `Laravel`, `WordPress` | -| `T_LOGICAL_OR` | `VerticalSpacing` | -| `T_LOGICAL_XOR` | `VerticalSpacing` | -| `T_MATCH` | `StandardSpacing` | -| `T_NULLSAFE_OBJECT_OPERATOR` | `AlignChains`, `VerticalSpacing` | -| `T_OBJECT_OPERATOR` | `AlignChains`, `VerticalSpacing` | -| `T_OPEN_BRACE` | `PlaceBraces`, `VerticalSpacing`, `WordPress` | -| `T_OPEN_BRACKET` | `WordPress` | -| `T_OPEN_PARENTHESIS` | `WordPress` | -| `T_OPEN_TAG` | `StandardSpacing` | -| `T_OPEN_TAG_WITH_ECHO` | `StandardSpacing` | -| `T_OR` | `VerticalSpacing` | -| `T_QUESTION` | `AlignTernaryOperators`, `VerticalSpacing` | -| `T_RETURN` | `BlankBeforeReturn` | -| `T_SEMICOLON` | `StatementSpacing` | -| `T_START_HEREDOC` | `FormatHeredocs`, `ProtectStrings`, `StandardSpacing` | -| `T_SWITCH` | `SemiStrictExpressions`, `StrictExpressions`, `SwitchIndentation` | -| `T_WHILE` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StrictExpressions` | -| `T_XOR` | `VerticalSpacing` | -| `T_YIELD` | `BlankBeforeReturn` | -| `T_YIELD_FROM` | `BlankBeforeReturn` | +| Token | Rules | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `*` | `HangingIndentation`, `StandardIndentation` | +| `* (except virtual)` | `PreserveNewlines` | +| `T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG` | `VerticalSpacing` | +| `T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG` | `VerticalSpacing` | +| `T_AND` | `VerticalSpacing` | +| `T_ATTRIBUTE` | `PlaceBrackets`, `StandardSpacing` | +| `T_ATTRIBUTE_COMMENT` | `StandardSpacing` | +| `T_BACKTICK` | `ProtectStrings` | +| `T_BOOLEAN_AND` | `VerticalSpacing` | +| `T_BOOLEAN_OR` | `VerticalSpacing` | +| `T_CASE` | `SwitchIndentation` | +| `T_CATCH` | `Drupal` | +| `T_CLOSE_TAG` | `StandardSpacing` | +| `T_COALESCE` | `AlignTernaryOperators` | +| `T_COLON` | `StatementSpacing`, `WordPress` | +| `T_COMMA` | `StandardSpacing` | +| `T_COMMENT` | `NormaliseComments`, `PlaceComments`, `WordPress` | +| `T_CONCAT` | `Laravel`, `Symfony` | +| `T_CONSTANT_ENCAPSED_STRING` | `NormaliseStrings` | +| `T_CURLY_OPEN` | `PlaceBrackets` | +| `T_DECLARE` | `StandardSpacing` | +| `T_DEFAULT` | `SwitchIndentation` | +| `T_DNUMBER` | `NormaliseNumbers` | +| `T_DO` | `ControlStructureSpacing` | +| `T_DOC_COMMENT` | `Drupal`, `NormaliseComments`, `PlaceComments`, `WordPress` | +| `T_DOLLAR_OPEN_CURLY_BRACES` | `PlaceBrackets` | +| `T_DOUBLE_QUOTE` | `ProtectStrings` | +| `T_ELSE` | `ControlStructureSpacing`, `Drupal` | +| `T_ELSEIF` | `ControlStructureSpacing`, `Drupal`, `SemiStrictExpressions`, `StrictExpressions` | +| `T_ENCAPSED_AND_WHITESPACE` | `NormaliseStrings` | +| `T_FINALLY` | `Drupal` | +| `T_FN` | `AlignArrowFunctions`, `Laravel`, `Symfony` | +| `T_FOR` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StandardSpacing`, `StrictExpressions`, `VerticalSpacing` | +| `T_FOREACH` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StrictExpressions` | +| `T_IF` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StrictExpressions` | +| `T_LNUMBER` | `NormaliseNumbers` | +| `T_LOGICAL_AND` | `VerticalSpacing` | +| `T_LOGICAL_NOT` | `Laravel`, `WordPress` | +| `T_LOGICAL_OR` | `VerticalSpacing` | +| `T_LOGICAL_XOR` | `VerticalSpacing` | +| `T_MATCH` | `StandardSpacing` | +| `T_NULLSAFE_OBJECT_OPERATOR` | `AlignChains`, `VerticalSpacing` | +| `T_OBJECT_OPERATOR` | `AlignChains`, `VerticalSpacing` | +| `T_OPEN_BRACE` | `PlaceBraces`, `PlaceBrackets`, `VerticalSpacing`, `WordPress` | +| `T_OPEN_BRACKET` | `PlaceBrackets`, `WordPress` | +| `T_OPEN_PARENTHESIS` | `PlaceBrackets`, `WordPress` | +| `T_OPEN_TAG` | `StandardSpacing` | +| `T_OPEN_TAG_WITH_ECHO` | `StandardSpacing` | +| `T_OR` | `VerticalSpacing` | +| `T_QUESTION` | `AlignTernaryOperators`, `VerticalSpacing` | +| `T_RETURN` | `BlankBeforeReturn` | +| `T_SEMICOLON` | `StatementSpacing` | +| `T_START_HEREDOC` | `FormatHeredocs`, `ProtectStrings`, `StandardSpacing` | +| `T_SWITCH` | `SemiStrictExpressions`, `StrictExpressions`, `SwitchIndentation` | +| `T_WHILE` | `ControlStructureSpacing`, `SemiStrictExpressions`, `StrictExpressions` | +| `T_XOR` | `VerticalSpacing` | +| `T_YIELD` | `BlankBeforeReturn` | +| `T_YIELD_FROM` | `BlankBeforeReturn` | ## `DeclarationRule` classes, by declaration type diff --git a/scripts/get-rules.php b/scripts/get-rules.php index ac7acb9d..352ad0df 100755 --- a/scripts/get-rules.php +++ b/scripts/get-rules.php @@ -26,9 +26,9 @@ /** * @param array,is_mandatory:bool,is_default:bool,pass:int,method:string,priority:int,php_doc:PHPDoc|null,tokens:array|array{string}|null,declarations:array|array{'*'}|null}>> $array - * @param-out array,is_mandatory:bool,is_default:bool,pass:int,method:string,priority:int,php_doc:PHPDoc|null,tokens:array|array{string}|null,declarations:array|array{'*'}|null}>> $array * @param class-string $rule * @param array,array>|null $callbackDocs + * @param-out array,is_mandatory:bool,is_default:bool,pass:int,method:string,priority:int,php_doc:PHPDoc|null,tokens:array|array{string}|null,declarations:array|array{'*'}|null}>> $array */ function maybeAddRule( array &$array, diff --git a/src/App/PrettyPHPCommand.php b/src/App/PrettyPHPCommand.php index 583dffc1..6ef6e48a 100644 --- a/src/App/PrettyPHPCommand.php +++ b/src/App/PrettyPHPCommand.php @@ -1394,8 +1394,8 @@ private function getConfigFile(string $dir): ?string /** * @param array $files - * @param-out array $files * @param array $dirs + * @param-out array $files * @param-out array $dirs */ private function addDir(string $dir, array &$files, array &$dirs): void @@ -1434,8 +1434,8 @@ private function addDir(string $dir, array &$files, array &$dirs): void /** * @param SplFileInfo|string $file * @param array $files - * @param-out array $files * @param array $dirs + * @param-out array $files * @param-out array $dirs */ private function addFile($file, array &$files, array &$dirs): void diff --git a/src/Catalog/TokenData.php b/src/Catalog/TokenData.php index b88e2766..fbbf7796 100644 --- a/src/Catalog/TokenData.php +++ b/src/Catalog/TokenData.php @@ -53,50 +53,56 @@ interface TokenData */ public const PROPERTY_HOOKS = 7; + /** + * Collections of tokens associated with a T_FOR loop: expr1, expr2, expr3, + * semicolon delimiters, comma delimiters + */ + public const FOR_PARTS = 8; + /** * The last token of the string opened by the token */ - public const END_STRING = 8; + public const END_STRING = 9; /** * The first object operator in a chain of method calls */ - public const CHAIN = 9; + public const CHAIN = 10; /** * The other T_QUESTION or T_COLON associated with a TERNARY */ - public const OTHER_TERNARY = 10; + public const OTHER_TERNARY = 11; /** * The token ID of the delimiter associated with a LIST_PARENT token */ - public const LIST_DELIMITER = 11; + public const LIST_DELIMITER = 12; /** * A collection of items associated with a LIST_PARENT token */ - public const LIST_ITEMS = 12; + public const LIST_ITEMS = 13; /** * The number of items associated with a LIST_PARENT token */ - public const LIST_ITEM_COUNT = 13; + public const LIST_ITEM_COUNT = 14; /** * The LIST_PARENT of the first token in a LIST_ITEM */ - public const LIST_PARENT = 14; + public const LIST_PARENT = 15; /** * The content of a normalised DocBlock token (T_DOC_COMMENT or T_COMMENT) * after delimiters and trailing whitespace are removed */ - public const COMMENT_CONTENT = 15; + public const COMMENT_CONTENT = 16; /** * An array of closures that align other tokens with the token when its * output column changes */ - public const ALIGNMENT_CALLBACKS = 16; + public const ALIGNMENT_CALLBACKS = 17; } diff --git a/src/Concern/RuleTrait.php b/src/Concern/RuleTrait.php index 547ccaab..a0029e3e 100644 --- a/src/Concern/RuleTrait.php +++ b/src/Concern/RuleTrait.php @@ -56,7 +56,7 @@ private function preserveOneLine( } $start->collect($end) - ->applyInnerWhitespace(Space::CRITICAL_NO_BLANK | Space::CRITICAL_NO_LINE); + ->setInnerWhitespace(Space::CRITICAL_NO_BLANK | Space::CRITICAL_NO_LINE); return true; } diff --git a/src/Formatter.php b/src/Formatter.php index b104cb9c..dec02b5c 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -58,6 +58,7 @@ use Lkrms\PrettyPHP\Rule\NormaliseStrings; use Lkrms\PrettyPHP\Rule\OperatorSpacing; use Lkrms\PrettyPHP\Rule\PlaceBraces; +use Lkrms\PrettyPHP\Rule\PlaceBrackets; use Lkrms\PrettyPHP\Rule\PlaceComments; use Lkrms\PrettyPHP\Rule\PreserveNewlines; use Lkrms\PrettyPHP\Rule\PreserveOneLineStatements; @@ -152,6 +153,7 @@ final class Formatter implements Buildable, Immutable PlaceComments::class, PreserveNewlines::class, VerticalSpacing::class, + PlaceBrackets::class, DeclarationSpacing::class, StandardIndentation::class, SwitchIndentation::class, @@ -168,9 +170,9 @@ final class Formatter implements Buildable, Immutable PreserveNewlines::class, PreserveOneLineStatements::class, BlankBeforeReturn::class, - StrictLists::class, StrictExpressions::class, SemiStrictExpressions::class, + StrictLists::class, AlignChains::class, DeclarationSpacing::class, AlignArrowFunctions::class, diff --git a/src/Internal/TokenCollection.php b/src/Internal/TokenCollection.php index d6ff57b1..c2d1ab93 100644 --- a/src/Internal/TokenCollection.php +++ b/src/Internal/TokenCollection.php @@ -324,7 +324,18 @@ public function toString(string $delimiter = ''): string /** * @return $this */ - public function applyWhitespace(int $whitespace) + public function setTokenWhitespace(int $whitespace): self + { + foreach ($this->Items as $token) { + $token->Whitespace |= $whitespace; + } + return $this; + } + + /** + * @return $this + */ + public function applyTokenWhitespace(int $whitespace): self { // Shift *_BEFORE and *_AFTER to their NO_* counterparts, then clear // other bits @@ -342,7 +353,7 @@ public function applyWhitespace(int $whitespace) /** * @return $this */ - public function applyInnerWhitespace(int $whitespace) + public function setInnerWhitespace(int $whitespace): self { $this->assertCollected(); @@ -354,7 +365,6 @@ public function applyInnerWhitespace(int $whitespace) if ($this->count() < 2) { return $this; } - $remove = $whitespace << 6 & 0b111111000000; $ignore = true; foreach ($this->Items as $token) { if ($ignore) { @@ -364,9 +374,6 @@ public function applyInnerWhitespace(int $whitespace) || $token->Data[Data::BOUND_TO]->index > $token->index ) { $token->Whitespace |= $whitespace; - if ($remove) { - $token->doRemoveWhitespace($remove); - } } } return $this; diff --git a/src/Parser.php b/src/Parser.php index 13888e0b..665a5f25 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -162,10 +162,10 @@ private function linkTokens(array $tokens): void * - `Data[Data::END_STRING]` * * @param non-empty-list $tokens - * @param-out non-empty-list $tokens * @param array>|null $tokensById - * @param-out array> $tokensById * @param Token[]|null $scopes + * @param-out non-empty-list $tokens + * @param-out array> $tokensById * @param-out non-empty-array $scopes */ private function buildHierarchy( @@ -719,20 +719,21 @@ private function parseStatements(array $scopes, ?array &$statements): void } /** - * Pass 4: identify declarations and parse (some) expressions + * Pass 4: parse expressions * * Token properties set: * * - `Data[Data::DECLARATION_PARTS]` * - `Data[Data::DECLARATION_TYPE]` * - `Data[Data::PROPERTY_HOOKS]` + * - `Data[Data::FOR_PARTS]` * - `Data[Data::OTHER_TERNARY]` * - `Data[Data::CHAIN]` * * @param Token[] $statements * @param array|null $declarations - * @param-out array $declarations * @param array>|null $declarationsByType + * @param-out array $declarations * @param-out array> $declarationsByType */ private function parseExpressions( @@ -750,7 +751,7 @@ private function parseExpressions( $end = $statement->EndStatement; $end = $end->OpenBracket ?? $end; - // Detect non-anonymous declarations + // Parse non-anonymous declarations and `for` expressions if ( $idx->AttributeOrDeclaration[$statement->id] && ($first = $this->skipNextSiblingsFrom($statement, $idx->Attribute)) @@ -835,6 +836,39 @@ private function parseExpressions( } } } + } elseif ($statement->id === \T_FOR) { + /** @var Token */ + $open = $statement->NextCode; + /** @var Token */ + $close = $open->CloseBracket; + /** @var Token */ + $first = $open->Next; + /** @var Token */ + $last = $close->Prev; + + $children = $open->children(); + $semicolons = $children->getAnyOf(\T_SEMICOLON); + $commas = $children->getAnyOf(\T_COMMA); + /** @var Token */ + $semi1 = $semicolons->first(); + /** @var Token */ + $second = $semi1->Next; + /** @var Token */ + $semi2 = $semicolons->last(); + /** @var Token */ + $third = $semi2->Next; + + $expr1 = $first->collect($semi1); + $expr2 = $second->collect($semi2); + $expr3 = $third->collect($last); + + $statement->Data[Data::FOR_PARTS] = [ + $expr1, + $expr2, + $expr3, + $semicolons, + $commas, + ]; } $token = $statement; diff --git a/src/Rule/BlankBeforeReturn.php b/src/Rule/BlankBeforeReturn.php index d0b013b5..eb0f412a 100644 --- a/src/Rule/BlankBeforeReturn.php +++ b/src/Rule/BlankBeforeReturn.php @@ -22,7 +22,7 @@ final class BlankBeforeReturn implements TokenRule public static function getPriority(string $method): ?int { return [ - self::PROCESS_TOKENS => 220, + self::PROCESS_TOKENS => 204, ][$method] ?? null; } diff --git a/src/Rule/DeclarationSpacing.php b/src/Rule/DeclarationSpacing.php index 97bcd38d..2bd2e9cc 100644 --- a/src/Rule/DeclarationSpacing.php +++ b/src/Rule/DeclarationSpacing.php @@ -224,7 +224,7 @@ public function processDeclarations(array $declarations): void && !$prev->isMultiLine() && !$decl->isMultiLine() )) { - $prev->End->collect($token)->applyInnerWhitespace(Space::NO_BLANK); + $prev->End->collect($token)->setInnerWhitespace(Space::NO_BLANK); $noBlankApplied = true; } elseif (!$decl->isCollapsible()) { // Apply "loose" spacing to multi-line declarations diff --git a/src/Rule/OperatorSpacing.php b/src/Rule/OperatorSpacing.php index e8002501..f4ce5f9c 100644 --- a/src/Rule/OperatorSpacing.php +++ b/src/Rule/OperatorSpacing.php @@ -133,7 +133,7 @@ public function processTokens(array $tokens): void $token->Whitespace |= Space::NONE_BEFORE | Space::NONE_AFTER; if (!$inTypeContext) { /** @var Token $parent */ - $parent->outer()->applyInnerWhitespace(Space::NONE); + $parent->outer()->setInnerWhitespace(Space::NONE); if ( $parent->PrevCode && $parent->PrevCode->id !== \T_OR diff --git a/src/Rule/PlaceBrackets.php b/src/Rule/PlaceBrackets.php new file mode 100644 index 00000000..8050f779 --- /dev/null +++ b/src/Rule/PlaceBrackets.php @@ -0,0 +1,76 @@ + 240, + ][$method] ?? null; + } + + /** + * @inheritDoc + */ + public static function getTokens(AbstractTokenIndex $idx): array + { + return $idx->OpenBracket; + } + + /** + * @inheritDoc + */ + public static function needsSortedTokens(): bool + { + return false; + } + + /** + * Apply the rule to the given tokens + * + * Inner whitespace is copied from open brackets to close brackets. + * + * Structural and `match` expression braces are ignored. + */ + public function processTokens(array $tokens): void + { + foreach ($tokens as $token) { + if ( + $token->Flags & Flag::STRUCTURAL_BRACE + || ($token->id === \T_OPEN_BRACE && $token->isMatchOpenBrace()) + ) { + continue; + } + + /** @var Token */ + $close = $token->CloseBracket; + if (!$token->hasNewlineBeforeNextCode()) { + $close->Whitespace |= Space::NO_BLANK_BEFORE | Space::NO_LINE_BEFORE; + } else { + $close->Whitespace |= Space::LINE_BEFORE; + if (!$close->hasNewlineBefore()) { + $close->removeWhitespace(Space::NO_LINE_BEFORE); + } + } + } + } +} diff --git a/src/Rule/Preset/Symfony.php b/src/Rule/Preset/Symfony.php index 304b538f..7b6c7919 100644 --- a/src/Rule/Preset/Symfony.php +++ b/src/Rule/Preset/Symfony.php @@ -116,6 +116,6 @@ public function processList(Token $parent, TokenCollection $items, Token $lastCh } } - $parent->outer()->applyInnerWhitespace(Space::NO_BLANK | Space::NO_LINE); + $parent->outer()->setInnerWhitespace(Space::NO_BLANK | Space::NO_LINE); } } diff --git a/src/Rule/SemiStrictExpressions.php b/src/Rule/SemiStrictExpressions.php index 7de8f571..7e726d9e 100644 --- a/src/Rule/SemiStrictExpressions.php +++ b/src/Rule/SemiStrictExpressions.php @@ -2,9 +2,11 @@ namespace Lkrms\PrettyPHP\Rule; +use Lkrms\PrettyPHP\Catalog\TokenData as Data; use Lkrms\PrettyPHP\Catalog\WhitespaceFlag as Space; use Lkrms\PrettyPHP\Concern\TokenRuleTrait; use Lkrms\PrettyPHP\Contract\TokenRule; +use Lkrms\PrettyPHP\Internal\TokenCollection; use Lkrms\PrettyPHP\AbstractTokenIndex; use Lkrms\PrettyPHP\Token; @@ -24,7 +26,7 @@ final class SemiStrictExpressions implements TokenRule public static function getPriority(string $method): ?int { return [ - self::PROCESS_TOKENS => 246, + self::PROCESS_TOKENS => 224, ][$method] ?? null; } @@ -48,7 +50,8 @@ public static function needsSortedTokens(): bool * Apply the rule to the given tokens * * Newlines are added before and after control structure expressions with - * newlines between siblings. + * newlines between siblings. In `for` expressions that break over multiple + * lines, newlines are also added after semicolons between expressions. * * > Unlike `StrictExpressions`, this rule does not apply leading and * > trailing newlines to expressions that would not break over multiple @@ -58,15 +61,20 @@ public function processTokens(array $tokens): void { foreach ($tokens as $token) { /** @var Token */ - $first = $token->NextCode; - if ($first->hasNewlineAfter()) { + $open = $token->NextCode; + if ($open->hasNewlineAfter()) { continue; } - if ($first->children()->pop()->tokenHasNewlineAfter(true)) { + if ($open->children()->pop()->tokenHasNewlineAfter(true)) { /** @var Token */ - $last = $first->CloseBracket; - $first->applyWhitespace(Space::LINE_AFTER); - $last->applyWhitespace(Space::LINE_BEFORE); + $close = $open->CloseBracket; + $open->applyWhitespace(Space::LINE_AFTER); + $close->applyWhitespace(Space::LINE_BEFORE); + if ($token->id === \T_FOR) { + /** @var TokenCollection */ + $semicolons = $token->Data[Data::FOR_PARTS][3]; + $semicolons->setTokenWhitespace(Space::LINE_AFTER); + } } } } diff --git a/src/Rule/StandardIndentation.php b/src/Rule/StandardIndentation.php index 5932c347..276c958a 100644 --- a/src/Rule/StandardIndentation.php +++ b/src/Rule/StandardIndentation.php @@ -2,7 +2,6 @@ namespace Lkrms\PrettyPHP\Rule; -use Lkrms\PrettyPHP\Catalog\WhitespaceFlag as Space; use Lkrms\PrettyPHP\Concern\TokenRuleTrait; use Lkrms\PrettyPHP\Contract\TokenRule; @@ -36,9 +35,8 @@ public static function needsSortedTokens(): bool /** * Apply the rule to the given tokens * - * The `Indent` and inner whitespace of each open bracket is copied to its - * close bracket, and the `Indent` of tokens between brackets with inner - * newlines is incremented. + * `Indent` is copied from open brackets to close brackets, and the `Indent` + * of tokens between brackets with inner newlines is incremented. */ public function processTokens(array $tokens): void { @@ -55,21 +53,11 @@ public function processTokens(array $tokens): void $prev = $token->Prev; $token->Indent = $prev->Indent; - if ($close = $prev->CloseBracket) { - if ($hasNewline = $prev->hasNewlineBeforeNextCode()) { - $token->Indent++; - } - - if (!$this->Idx->Virtual[$prev->id]) { - if (!$hasNewline) { - $close->Whitespace |= Space::NO_BLANK_BEFORE | Space::NO_LINE_BEFORE; - } else { - $close->Whitespace |= Space::LINE_BEFORE; - if (!$close->hasNewlineBefore()) { - $close->removeWhitespace(Space::NO_LINE_BEFORE); - } - } - } + if ( + $prev->CloseBracket + && $prev->hasNewlineBeforeNextCode() + ) { + $token->Indent++; } } } diff --git a/src/Rule/StandardSpacing.php b/src/Rule/StandardSpacing.php index dc1f1286..952aeb69 100644 --- a/src/Rule/StandardSpacing.php +++ b/src/Rule/StandardSpacing.php @@ -48,6 +48,7 @@ public static function getTokens(AbstractTokenIndex $idx): array \T_CLOSE_TAG => true, \T_COMMA => true, \T_DECLARE => true, + \T_FOR => true, \T_MATCH => true, \T_START_HEREDOC => true, ], @@ -110,6 +111,7 @@ public static function needsSortedDeclarations(): bool * added. * - **`declare` expressions:** whitespace is suppressed between * parentheses. + * - **`for` loops:** whitespace in empty expressions is suppressed. * - **`match` expressions:** newlines are added after delimiters between * arms. * - **Attributes:** in parameters, property hooks, anonymous functions and @@ -293,7 +295,23 @@ static function () use ($idx, $innerIndent, $next, $last) { if ($token->id === \T_DECLARE) { /** @var Token */ $nextCode = $token->NextCode; - $nextCode->outer()->applyInnerWhitespace(Space::NONE); + $nextCode->outer()->setInnerWhitespace(Space::NONE); + continue; + } + + if ($token->id === \T_FOR) { + [$expr1, $expr2, $expr3, $semicolons] = $token->Data[Data::FOR_PARTS]; + /** @var TokenCollection $expr */ + foreach ([$expr1, $expr2, $expr3] as $i => $expr) { + $count = $expr->count(); + if ($i < 2 && $count === 1) { + $expr->setTokenWhitespace(Space::NONE_BEFORE); + } elseif ($i === 2 && $count === 0) { + /** @var Token */ + $semi2 = $semicolons->last(); + $semi2->Whitespace |= Space::NONE_AFTER; + } + } continue; } diff --git a/src/Rule/StrictExpressions.php b/src/Rule/StrictExpressions.php index db4ad39e..9f3f1699 100644 --- a/src/Rule/StrictExpressions.php +++ b/src/Rule/StrictExpressions.php @@ -2,9 +2,11 @@ namespace Lkrms\PrettyPHP\Rule; +use Lkrms\PrettyPHP\Catalog\TokenData as Data; use Lkrms\PrettyPHP\Catalog\WhitespaceFlag as Space; use Lkrms\PrettyPHP\Concern\TokenRuleTrait; use Lkrms\PrettyPHP\Contract\TokenRule; +use Lkrms\PrettyPHP\Internal\TokenCollection; use Lkrms\PrettyPHP\AbstractTokenIndex; use Lkrms\PrettyPHP\Token; @@ -24,7 +26,7 @@ final class StrictExpressions implements TokenRule public static function getPriority(string $method): ?int { return [ - self::PROCESS_TOKENS => 244, + self::PROCESS_TOKENS => 222, ][$method] ?? null; } @@ -48,23 +50,29 @@ public static function needsSortedTokens(): bool * Apply the rule to the given tokens * * Newlines are added before and after control structure expressions that - * break over multiple lines. + * break over multiple lines. In `for` expressions that break over multiple + * lines, newlines are also added after semicolons between expressions. */ public function processTokens(array $tokens): void { foreach ($tokens as $token) { /** @var Token */ - $first = $token->NextCode; - if ($first->hasNewlineAfter()) { + $open = $token->NextCode; + if ($open->hasNewlineAfter()) { continue; } /** @var Token */ - $last = $first->CloseBracket; + $close = $open->CloseBracket; /** @var Token */ - $beforeLast = $last->Prev; - if ($first->collect($beforeLast)->hasNewline()) { - $first->applyWhitespace(Space::LINE_AFTER); - $last->applyWhitespace(Space::LINE_BEFORE); + $last = $close->Prev; + if ($open->collect($last)->hasNewline()) { + $open->applyWhitespace(Space::LINE_AFTER); + $close->applyWhitespace(Space::LINE_BEFORE); + if ($token->id === \T_FOR) { + /** @var TokenCollection */ + $semicolons = $token->Data[Data::FOR_PARTS][3]; + $semicolons->setTokenWhitespace(Space::LINE_AFTER); + } } } } diff --git a/src/Rule/StrictLists.php b/src/Rule/StrictLists.php index 275fbfcb..a9b36fe5 100644 --- a/src/Rule/StrictLists.php +++ b/src/Rule/StrictLists.php @@ -23,7 +23,7 @@ final class StrictLists implements ListRule public static function getPriority(string $method): ?int { return [ - self::PROCESS_LIST => 242, + self::PROCESS_LIST => 226, ][$method] ?? null; } @@ -49,9 +49,9 @@ public function processList(Token $parent, TokenCollection $items, Token $lastCh ) { $items = $items->shift(); } - $items->applyWhitespace(Space::LINE_BEFORE); + $items->applyTokenWhitespace(Space::LINE_BEFORE); } else { - $items->applyWhitespace(Space::NO_BLANK_BEFORE | Space::NO_LINE_BEFORE); + $items->setTokenWhitespace(Space::NO_BLANK_BEFORE | Space::NO_LINE_BEFORE); } } } diff --git a/src/Rule/VerticalSpacing.php b/src/Rule/VerticalSpacing.php index 71b1450c..d4add9d4 100644 --- a/src/Rule/VerticalSpacing.php +++ b/src/Rule/VerticalSpacing.php @@ -44,9 +44,9 @@ final class VerticalSpacing implements TokenRule, ListRule, DeclarationRule public static function getPriority(string $method): ?int { return [ - self::PROCESS_TOKENS => 240, - self::PROCESS_LIST => 240, - self::PROCESS_DECLARATIONS => 240, + self::PROCESS_TOKENS => 242, + self::PROCESS_LIST => 220, + self::PROCESS_DECLARATIONS => 220, ][$method] ?? null; } @@ -191,7 +191,6 @@ public function reset(): void * added after semicolon-delimited expressions. * - Otherwise, if the second or third expression has a leading newline, a * newline is added before the other. - * - Whitespace in empty expressions is suppressed. * * Newlines are added before open braces that belong to top-level * declarations and anonymous classes declared over multiple lines. @@ -255,33 +254,14 @@ public function processTokens(array $tokens): void } } } elseif ($token->id === \T_FOR) { - /** @var Token */ - $open = $token->NextCode; - /** @var Token */ - $close = $open->CloseBracket; - /** @var Token */ - $first = $open->Next; - /** @var Token */ - $last = $close->Prev; - - $children = $open->children(); - $commas = $children->getAnyOf(\T_COMMA); - $semicolons = $children->getAnyOf(\T_SEMICOLON); - /** @var Token */ - $semi1 = $semicolons->first(); - /** @var Token */ - $second = $semi1->Next; - /** @var Token */ - $semi2 = $semicolons->last(); - /** @var Token */ - $third = $semi2->Next; - - $expr1 = $first->collect($semi1); - $expr2 = $second->collect($semi2); - $expr3 = $third->collect($last); - + /** + * @var TokenCollection $semicolons + * @var TokenCollection $commas + */ + [$expr1, $expr2, $expr3, $semicolons, $commas] = $token->Data[Data::FOR_PARTS]; $hasNewline = false; $hasNewlineAndComma = false; + /** @var TokenCollection $expr */ foreach ([$expr1, $expr2, $expr3] as $expr) { if ($expr->hasNewlineBetweenTokens()) { $hasNewline = true; @@ -292,20 +272,10 @@ public function processTokens(array $tokens): void } } if ($hasNewlineAndComma) { - $commas->applyWhitespace(Space::LINE_AFTER); - $semicolons->applyWhitespace(Space::BLANK_AFTER); + $commas->setTokenWhitespace(Space::LINE_AFTER); + $semicolons->setTokenWhitespace(Space::BLANK_AFTER); } elseif ($hasNewline || $semicolons->tokenHasNewlineAfter()) { - $semicolons->applyWhitespace(Space::LINE_AFTER); - } - - // Suppress whitespace in empty `for` loop expressions - foreach ([$expr1, $expr2, $expr3] as $i => $expr) { - $count = $expr->count(); - if ($i < 2 && $count === 1) { - $expr->applyWhitespace(Space::NONE_BEFORE); - } elseif ($i === 2 && $count === 0) { - $semi2->Whitespace |= Space::NONE_AFTER; - } + $semicolons->setTokenWhitespace(Space::LINE_AFTER); } } elseif ($token->id === \T_OPEN_BRACE) { if ( @@ -381,7 +351,7 @@ public function processTokens(array $tokens): void $chain = $chain->shift(); } - $chain->applyWhitespace(Space::LINE_BEFORE); + $chain->setTokenWhitespace(Space::LINE_BEFORE); } } } @@ -418,7 +388,7 @@ public function processList(Token $parent, TokenCollection $items, Token $lastCh if ($lastChild->id === \T_COMMA) { $parent->Whitespace |= Space::LINE_AFTER; $items->add($parent->CloseBracket) - ->applyWhitespace(Space::LINE_BEFORE); + ->applyTokenWhitespace(Space::LINE_BEFORE); } if ($parent->id === \T_OPEN_PARENTHESIS && $parent->isParameterList()) { @@ -454,7 +424,7 @@ public function processDeclarations(array $declarations): void !$this->ListRuleEnabled && $commas->tokenHasNewlineBeforeNextCode() )) { - $commas->applyWhitespace(Space::LINE_AFTER); + $commas->setTokenWhitespace(Space::LINE_AFTER); } } diff --git a/src/Token.php b/src/Token.php index 33d07666..c6836dbf 100644 --- a/src/Token.php +++ b/src/Token.php @@ -57,7 +57,7 @@ final class Token extends GenericToken implements HasTokenNames, JsonSerializabl /** * @var array - * @phpstan-var array{self,self|null,self|null,self,bool,TokenCollection,int,TokenCollection,self,self,self,int,TokenCollection,int,self,string,Closure[]} + * @phpstan-var array{self,self|null,self|null,self,bool,TokenCollection,int,TokenCollection,array{TokenCollection,TokenCollection,TokenCollection,TokenCollection,TokenCollection},self,self,self,int,TokenCollection,int,self,string,Closure[]} */ public array $Data; diff --git a/src/TokenUtil.php b/src/TokenUtil.php index 5cf6c6f8..96ba5cf5 100644 --- a/src/TokenUtil.php +++ b/src/TokenUtil.php @@ -506,7 +506,14 @@ public static function serialize(Token $token): array ? self::describe($value) : ($value instanceof TokenCollection ? $value->toString(' ') - : (is_array($value) ? count($value) : $value)); + : (is_array($value) + ? (Arr::of($value, TokenCollection::class) + ? array_map( + fn(TokenCollection $coll) => $coll->toString(' '), + $value, + ) + : count($value)) + : $value)); } } diff --git a/tests/fixtures/Formatter/in/issues/0166-anonymous-functions.php b/tests/fixtures/Formatter/in/issues/0166-anonymous-functions.php new file mode 100644 index 00000000..1dd16af3 --- /dev/null +++ b/tests/fixtures/Formatter/in/issues/0166-anonymous-functions.php @@ -0,0 +1,64 @@ + + function ($bar, + $baz) /* */ use (&$foo, + $qux) {}, +]; + +function ($foo, + $bar, + $baz) use ($a, + $b, + $c) { + quux(); +}; + +$foo = function ($bar // +) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar +/* */) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar, + $baz) // + use ($baz, + $qux) { + quux(); + }; + +$foo = [ + 10 => + function ($bar, + $baz) /* */ use (&$foo, + $qux) { + quux(); + }, +]; diff --git a/tests/fixtures/Formatter/out/01-default/issues/0166-anonymous-functions.php b/tests/fixtures/Formatter/out/01-default/issues/0166-anonymous-functions.php new file mode 100644 index 00000000..9948b5ab --- /dev/null +++ b/tests/fixtures/Formatter/out/01-default/issues/0166-anonymous-functions.php @@ -0,0 +1,64 @@ + + function ($bar, + $baz) /* */ use (&$foo, + $qux) {}, +]; + +function ($foo, + $bar, + $baz) use ($a, + $b, + $c) { + quux(); +}; + +$foo = function ($bar // +) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar +/* */) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar, + $baz) // + use ($baz, + $qux) { + quux(); + }; + +$foo = [ + 10 => + function ($bar, + $baz) /* */ use (&$foo, + $qux) { + quux(); + }, +]; diff --git a/tests/fixtures/Formatter/out/02-aligned/issues/0166-anonymous-functions.php b/tests/fixtures/Formatter/out/02-aligned/issues/0166-anonymous-functions.php new file mode 100644 index 00000000..269545de --- /dev/null +++ b/tests/fixtures/Formatter/out/02-aligned/issues/0166-anonymous-functions.php @@ -0,0 +1,64 @@ + + function ($bar, + $baz) /* */ use (&$foo, + $qux) {}, +]; + +function ($foo, + $bar, + $baz) use ($a, + $b, + $c) { + quux(); +}; + +$foo = function ($bar // + ) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar + /* */) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar, + $baz) // + use ($baz, + $qux) { + quux(); + }; + +$foo = [ + 10 => + function ($bar, + $baz) /* */ use (&$foo, + $qux) { + quux(); + }, +]; diff --git a/tests/fixtures/Formatter/out/03-tab/issues/0166-anonymous-functions.php b/tests/fixtures/Formatter/out/03-tab/issues/0166-anonymous-functions.php new file mode 100644 index 00000000..460dc669 --- /dev/null +++ b/tests/fixtures/Formatter/out/03-tab/issues/0166-anonymous-functions.php @@ -0,0 +1,64 @@ + + function ($bar, + $baz) /* */ use (&$foo, + $qux) {}, +]; + +function ($foo, + $bar, + $baz) use ($a, + $b, + $c) { + quux(); +}; + +$foo = function ($bar // +) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar +/* */) use ($baz, + $qux) { + quux(); +}; + +$foo = function ($bar, + $baz) // + use ($baz, + $qux) { + quux(); + }; + +$foo = [ + 10 => + function ($bar, + $baz) /* */ use (&$foo, + $qux) { + quux(); + }, +]; diff --git a/tests/fixtures/Formatter/out/04-psr12/issues/0166-anonymous-functions.php b/tests/fixtures/Formatter/out/04-psr12/issues/0166-anonymous-functions.php new file mode 100644 index 00000000..e584ee21 --- /dev/null +++ b/tests/fixtures/Formatter/out/04-psr12/issues/0166-anonymous-functions.php @@ -0,0 +1,96 @@ + + function ( + $bar, + $baz + ) /* */ use ( + &$foo, + $qux + ) {}, +]; + +function ( + $foo, + $bar, + $baz +) use ( + $a, + $b, + $c +) { + quux(); +}; + +$foo = function ($bar // +) use ( + $baz, + $qux +) { + quux(); +}; + +$foo = function ($bar +/* */) use ( + $baz, + $qux +) { + quux(); +}; + +$foo = function ( + $bar, + $baz +) // + use ( + $baz, + $qux + ) { + quux(); + }; + +$foo = [ + 10 => + function ( + $bar, + $baz + ) /* */ use ( + &$foo, + $qux + ) { + quux(); + }, +]; diff --git a/tests/unit/Rule/VerticalSpacingTest.php b/tests/unit/Rule/VerticalSpacingTest.php index 2b2179ab..9487279d 100644 --- a/tests/unit/Rule/VerticalSpacingTest.php +++ b/tests/unit/Rule/VerticalSpacingTest.php @@ -264,6 +264,38 @@ function getArray() ->disable([MoveComments::class]) ->build(), ], + 'idempotent brackets' => [ + <<<'PHP' +