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;