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/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..9ff6149b 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;
@@ -151,6 +152,7 @@ final class Formatter implements Buildable, Immutable
PlaceBraces::class,
PlaceComments::class,
PreserveNewlines::class,
+ PlaceBrackets::class,
VerticalSpacing::class,
DeclarationSpacing::class,
StandardIndentation::class,
diff --git a/src/Internal/TokenCollection.php b/src/Internal/TokenCollection.php
index d6ff57b1..67c1e595 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 setWhitespace(int $whitespace): self
+ {
+ foreach ($this->Items as $token) {
+ $token->Whitespace |= $whitespace;
+ }
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function applyWhitespace(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 1698a06b..665a5f25 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -719,13 +719,14 @@ 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]`
*
@@ -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..27e601a2 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->setWhitespace(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..25594be5 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->setWhitespace(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..4f22b561 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->setWhitespace(Space::LINE_AFTER);
+ }
}
}
}
diff --git a/src/Rule/StrictLists.php b/src/Rule/StrictLists.php
index 275fbfcb..842eb8fc 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;
}
@@ -51,7 +51,7 @@ public function processList(Token $parent, TokenCollection $items, Token $lastCh
}
$items->applyWhitespace(Space::LINE_BEFORE);
} else {
- $items->applyWhitespace(Space::NO_BLANK_BEFORE | Space::NO_LINE_BEFORE);
+ $items->setWhitespace(Space::NO_BLANK_BEFORE | Space::NO_LINE_BEFORE);
}
}
}
diff --git a/src/Rule/VerticalSpacing.php b/src/Rule/VerticalSpacing.php
index 71b1450c..8336aa3a 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->setWhitespace(Space::LINE_AFTER);
+ $semicolons->setWhitespace(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->setWhitespace(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->setWhitespace(Space::LINE_BEFORE);
}
}
}
@@ -454,7 +424,7 @@ public function processDeclarations(array $declarations): void
!$this->ListRuleEnabled
&& $commas->tokenHasNewlineBeforeNextCode()
)) {
- $commas->applyWhitespace(Space::LINE_AFTER);
+ $commas->setWhitespace(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;